Written on
Domain Patterns in Enterprise Projects
If I had to separate projects I've been in, i would difference between data manipulation and enterprise projects. Data manipulation projects mainly consist of a set of forms needed to alter data which is stored in some persistent store (most of the time a relational database). In these projects, there is not much domain logic to be found. There might be some validation logic, some jobs running in the background, but that's mainly it with domain logic. Enterprise projects are all about domain logic and integration of external systems. Most of the time a project starts with implementing all the views and domain classes which are considered to be simple, that is, not much domain knowledge has to be available in order to implement those views. But as time goes by, domain logic creeps in and classes which seemed pretty small and granular for data manipulation style grow to be oversized monsters.
The Birth of Objects
The birth of an object is a special event: it is the time the object's data starts to exist in memory. In order to create an object, one of the object's constructors is used. A constructor is a special creation procedure which encapsulates object initialization and which ensures that at the end of the constructor call an object is in a state that satisfies the class invariant. Okay, that is rather academic you might think, which is true for classes which resemble active records [0] - but not for real domain classes found in larger projects. If you are writing an enterprise project with Grails, you have to know that Grails per se is not ment to be used in projects like that, but rather in data manipulation projects. Let's assume one of our domain classes is a Customer domain class. If we create a Customer class with grails create-domain org.ast.domain.entity.Customer we'll get a class that looks like
class Customer {
static constraints = {
}
}
class Customer {
String name
Status status
Account account
static constraints = {
name(blank: false)
status(nullable: false)
account(nullable: false)
}
}
def c = new Customer()
def c = new Customer(name: 'Max')
def c = new Customer(status: null)
def c = new Customer(status: Status.NEW, account: null)
// ...
@Invariant({ name?.size() > 0 && status && account })
class Customer {
String name
Status status
Account account
def Customer(String name, Account account) {
this.name = name
this.status = Status.NEW
this.account = account
}
void setName(final String other) { name = other }
void setStatus(final Status other) { status = other }
void setAccount(final Account other) { account = other }
// Grails Specific
private def Customer() {}
static constraints = {
name(blank: false)
status(nullable: false)
account(nullable: false)
}
}
@Invariant({ name?.size() > 0 && status && account })
class Customer {
// ...
def static Customer create(final String name, final Account account) {
return new Customer(name, account)
}
// ...
}
Domain Logic comes in...
In the previous section, the Customer class has been modified to be created with a custom constructor. Let as assume that the following business requirement comes in:After 30 days, if the customer is in state REGISTERED, the application needs to send an e-mail to the customer, telling that the test-period ends in 5 days.Is is pretty obvious that we need to have a job that checks for customer accounts existing >= 30 days. In addition, the application needs to have a way to integrate with an external SMTP server, which is a rather technological issue we don't care about now (although I did in the last blog post [3]). What's interesting in this place is where to put the additional domain logic. A first naive approach would be to create a service class CustomerService which implements the business requirement:
class CustomerService {
static transactional = true
def void sentTestPeriodEndsMail(final Customer customer) {
if (customer.state != State.REGISTERED) return
if (!(customer.created + 30 >= new Date())) return
customer.state = State.REGISTERED_WARNED
customer.save()
sendMailMessage(customer)
}
// ...
}
class CustomerService {
static transactional = true
def void sentTestPeriodEndsMail(final Customer customer) {
if (customer instanceof FriendOfMineCustomer) return // <-- more domain logic comes in...
if (customer.state != State.REGISTERED) return
if (!(customer.created + 30 >= new Date())) return
customer.state = State.REGISTERED_WARNED
customer.save()
sendMailMessage(customer)
}
// ...
}
@Invariant({ name?.size() > 0 && status && account })
class Customer {
def boolean isTestPeriodEmailCandidate() {
return state == State.REGISTERED && created + 30 >= new Date()
}
// ...
}
class FriendOfMinCustomer extends Customer {
def boolean isTestPeriodEmailCandidate() {
return false
}
// ...
}
class CustomerService {
static transactional = true
def void sentTestPeriodEndsMail(final Customer customer) {
if (!customer.isTestPeriodEmailCandidate()) return
customer.state = State.REGISTERED_WARNED
customer.save()
sendMailMessage(customer)
}
// ...
}
class CustomerWorkflow {
State state
Customer customer
// ...
}
class CustomerService {
static transactional = true
def void sentTestPeriodEndsMail(final Customer customer) {
if (!customer.isTestPeriodEmailCandidate()) return
customer.workflow.mailMessageSent()
sendMailMessage(customer)
}
// ...
}
Entities vs. Value Objects
Another thing which is important for larger Grails projects is to recognize the difference between entities and simple value objects. In Grails, each generated domain class is an entity - it can be uniquely identified. On the other side, value objects do not have an identity, are immutable and interchangeable. Value object classes could be enumerations, but also separate GORM domain classes. GORM supports embedding domain classes within other domain classes. The decision whether a certain class is a value object class or entity class can change from domain to domain. It could be that an address is a value object, but put in another context an address is an entity. Defining value objects with domain classes is usually done with the static embedded property:
class Customer {
// ...
Address homeAddress
static embedded = ['homeAddress']
}
class Address {
String street
// ...
}
The Role of Repositories
A repository is responsible for loading persistent objects from arbitrary data stores. In Grails, this functionality is mostly encapsulated with GORM generated methods, available as static methods in each domain class:
def customers = Customer.findAllByState(State.NEW, [max: 10])
// ...
