Anaemic Domain Models
An anaemic domain model is one typified by the Java BeansTM style of programming: simple Java domain classes containing just fields and setter/getter methods for those fields; logic for manipulating the domain objects is contained in higher level classes (typically a service layer).
For example, consider the following simple anaemic domain model consisting of a Person and a list of Addresses associated with that person:
1: public class Person {
2:
3: private Long id;
4: private String forename;
5: private String surname;
6: private Date dob;
7: private List<Address> addresses;
8:
9: public Person() {
10: }
11:
12: public Long getId() {
13: return id;
14: }
15:
16: public void setId(Long id) {
17: this.id = id;
18: }
19:
20: public String getForename() {
21: return forename;
22: }
23:
24: public void setForename(String forename) {
25: this.forename = forename;
26: }
27:
28: public String getSurname() {
29: return surname;
30: }
31:
32: public void setSurname(String surname) {
33: this.surname = surname;
34: }
35:
36: public Date getDob() {
37: return dob;
38: }
39:
40: public void setDob(Date dob) {
41: this.dob = dob;
42: }
43:
44: public List<Address> getAddresses() {
45: return addresses;
46: }
47:
48: public void setAddresses(List<Address> addresses) {
49: this.addresses = addresses;
50: }
51:
52: @Override
53: public String toString() {
54: ...
55: }
56:
57: @Override
58: public boolean equals(Object o) {
59: ...
60: }
61:
62: @Override
63: public int hashCode() {
64: ...
65: }
66: }
67:
68:
69:
70: public class Address {
71:
72: private String line1;
73: private String line2;
74: private String line3;
75: private String town;
76: private String county;
77: private String postcode;
78: private String countryCode;
79:
80: public Address() {
81: }
82:
83: public String getLine1() {
84: return line1;
85: }
86:
87: public void setLine1(String line1) {
88: this.line1 = line1;
89: }
90:
91: public String getLine2() {
92: return line2;
93: }
94:
95: public void setLine2(String line2) {
96: this.line2 = line2;
97: }
98:
99: public String getLine3() {
100: return line3;
101: }
102:
103: public void setLine3(String line3) {
104: this.line3 = line3;
105: }
106:
107: public String getTown() {
108: return town;
109: }
110:
111: public void setTown(String town) {
112: this.town = town;
113: }
114:
115: public String getCounty() {
116: return county;
117: }
118:
119: public void setCounty(String county) {
120: this.county = county;
121: }
122:
123: public String getPostcode() {
124: return postcode;
125: }
126:
127: public void setPostcode(String postcode) {
128: this.postcode = postcode;
129: }
130:
131: public String getCountryCode() {
132: return countryCode;
133: }
134:
135: public void setCountryCode(String countryCode) {
136: this.countryCode = countryCode;
137: }
138:
139: @Override
140: public String toString() {
141: ...
142: }
143:
144: @Override
145: public boolean equals(Object o) {
146: ...
147: }
148:
149: @Override
150: public int hashCode() {
151: ...
152: }
153: }
154:
Then, we define some services on top of the domain model that obtain, use and update the domain objects to implement the functionality required by the business:
1: public class PersonService {
2:
3: private final PersonRepository repository;
4:
5: public PersonService() {
6: repository = new PersonRepository();
7: }
8:
9: public Long getPersonId(String surname, String forename) {
10: return repository.findPerson(surname, forename);
11: }
12:
13: public Person getPerson(Long personId) {
14: return repository.getPerson(personId);
15: }
16:
17: public void addAddress(Long personId, Address newAddress) {
18: Person person = repository.getPerson(personId);
19:
20: List<Address> addresses = person.getAddresses();
21: if ( addresses == null ) {
22: addresses = new ArrayList<Address>();
23: person.setAddresses(addresses);
24: }
25: addresses.add(newAddress);
26: }
27:
28: public void makeDefaultAddress(Long personId, Address defaultAddress) {
29: Person person = repository.getPerson(personId);
30:
31: List<Address> addresses = person.getAddresses();
32: if ( addresses == null || !addresses.contains(defaultAddress) ) {
33: throw new IllegalArgumentException();
34: }
35:
36: // Default address is always the first address in the list
37: addresses.remove(defaultAddress);
38: addresses.add(0, defaultAddress);
39: }
40: }
41:
42:
43: public class MailShotService {
44:
45: public void sendMailShot(Person person, Long mailShotId) {
46: List<Address> addresses = person.getAddresses();
47: if ( addresses == null || addresses.isEmpty() ) {
48: // No mailshot can be sent
49: return;
50: }
51:
52: Address sendTo = addresses.get(0);
53:
54: // Code here to locate the mailshot and call the printing routine!
55: }
56: }
57:
The above code is overly simplistic (and somewhat contrived), but it demonstrates a key problem with this approach, namely that the encapsulation of the addresses property is broken. Specifically:
1) The fact that addresses are stored as a list is exposed to the service layer (and beyond). In fact, the PersonService is even responsible for creating the list instance. Changing the way addresses are stored in Person would mandate changing all the services (and perhaps controllers, pages and so on) that work with Person objects.
2) The knowledge that the first address in the list is the default address has escaped the domain model into the service layer. In particular there are two different services that both contain this knowledge. Should we want to change this approach we have to change and test code in two places (or more likely we change it in one place, forget the other and then wonder why our app behaves inconsistently).
Now, many proponents of the anaemic domain approach will tell you that the above problems can be avoided by correctly implementing your service layers. For example, only one service class is ever used to deal with Person. Any other services, controllers or whatever that need to access Person must use this service to do so. For example, the PersonService could have a new method: getDefaultAddress which would be called by the MailShotService. However, in my experience this never works for the following reasons:
1) Unless your developers are INCREDIBLY disciplined then this approach will always be violated. It's right before a deadline and a developer needs to access the default address from some controller in the system. Will they do all the work to inject the PersonService or will they just pull the first element off the address list? Most likely the second, and as soon as it's been done once then you can guarantee that that code will at some time be reused as a template for other code and the problem just proliferates from there. In 15 years I have never seen an anaemic domain pattern where this hasn't happened.
2) You end up with the higher level services and the controllers all having to inject large numbers of other services in order to get anything done. This results in a tighter coupling of the system and significantly increases the complexity of unit and integration testing (unit: need to define many mocks; component: need to pull in almost the whole system to test just one component). In every case I've seen, the anaemic domain pattern done in this way results in a small handful of controllers or services that pull in almost every other service in the system, which makes them really difficult to test and even more difficult to modify.
In my humble opinion, the anaemic domain model should be considered one of the most destructive anti-patterns of our time. It breaks the concept of good object oriented design and encapsulation and leads to service layers (and above) that become difficult to maintain and overly complex.
Rich Domain Models
An alternative is the rich domain model, where we attempt to encapsulate as much information about the domain inside the actual domain classes. We then expose these rich objects to higher levels, which can utilise the domain objects directly with less need for services containing arbitrary domain and business logic.
Looking at our RICH Address and Person objects:
1: public class Person {
2:
3: private Long id;
4: private String forename;
5: private String surname;
6: private Date dob;
7: private final List<Address> addresses;
8:
9: public Person(final String forename, final String surname, final Date dob) {
10: this.forename = forename;
11: this.surname = surname;
12: this.dob = new Date(dob.getTime());
13: this.addresses = new ArrayList<Address>();
14: }
15:
16: public Long getId() {
17: return id;
18: }
19:
20: public void setId(final Long id) {
21: this.id = id;
22: }
23:
24: public String getForename() {
25: return forename;
26: }
27:
28: public void setForename(String forename) {
29: this.forename = forename;
30: }
31:
32: public String getSurname() {
33: return surname;
34: }
35:
36: public void setSurname(String surname) {
37: this.surname = surname;
38: }
39:
40: public Date getDob() {
41: return new Date(dob.getTime());
42: }
43:
44: public void setDob(Date dob) {
45: this.dob = new Date(dob.getTime());
46: }
47:
48: public void addAddress(final Address address) {
49: addresses.add(address);
50: }
51:
52: public void removeAddress(final Address address) {
53: addresses.remove(address);
54: }
55:
56: public Collection<Address> getAllAddresses() {
57: return Collections.unmodifiableCollection(addresses);
58: }
59:
60: public void makeDefaultAddress(final Address address) {
61: if ( !addresses.contains(address) ) {
62: throw new IllegalArgumentException();
63: }
64:
65: addresses.remove(address);
66: addresses.add(0, address);
67: }
68:
69: public Address getDefaultAddress() {
70: if ( addresses.isEmpty() ) throw new IllegalStateException();
71: else return addresses.get(0);
72: }
73:
74: @Override
75: public String toString() {
76: ...
77: }
78:
79: @Override
80: public boolean equals(Object o) {
81: ...
82: }
83:
84: @Override
85: public int hashCode() {
86: ...
87: }
88: }
89:
90:
91: public class Address {
92:
93: private final String line2;
94: private final String line1;
95: private final String line3;
96: private final String town;
97: private final String county;
98: private final Postcode postcode;
99: private final Country country;
100:
101: public Address(final String line1, final String line2, final String line3,
102: final String town, final String county, final Postcode postcode,
103: final Country country) {
104: this.line1 = line1;
105: this.line2 = line2;
106: this.line3 = line3;
107: this.town = town;
108: this.county = county;
109: this.postcode = postcode;
110: this.country = country;
111: }
112:
113: public String getLine1() {
114: return line1;
115: }
116:
117: public String getLine2() {
118: return line2;
119: }
120:
121: public String getLine3() {
122: return line3;
123: }
124:
125: public String getTown() {
126: return town;
127: }
128:
129: public String getCounty() {
130: return county;
131: }
132:
133: public Postcode getPostcode() {
134: return postcode;
135: }
136:
137: public Country getCountry() {
138: return country;
139: }
140:
141: @Override
142: public String toString() {
143: ...
144: }
145:
146: @Override
147: public boolean equals(final Object o) {
148: ...
149: }
150:
151: @Override
152: public int hashCode() {
153: ...
154: }
155: }
156:
157:
158:
From the above you can see a couple of significant changes. Firstly, I've made as much of the data as possible immutable. In particular I've made addresses immutable and thus to change an address you have to remove the old one and insert a new one. This stops any users of the domain model from making changes to objects that should always be managed within the domain. Secondly, the addresses field is fully encapsulated. Users of the domain model know none of its implementation detail and they cannot manipulate the contents of the underlying collection as this is never exposed in a mutable form.
Additionally, I've added specific types for Postcode and Country which will encapsulate all the conversion and validation logic for converting between user entered Strings and their actual meaning - logic that would normally be in a controller or service in an anaemic domain model.
This approach greatly simplifies the services layer. The PersonService needs only to provide methods to get the Person and the MailShotService can just call a simple get method on the Person object:
1: public class PersonService {
2:
3: private final PersonRepository repository;
4:
5: public PersonService() {
6: repository = new PersonRepository();
7: }
8:
9: public Long getPersonId(final String surname, final String forename) {
10: return repository.findPerson(surname, forename);
11: }
12:
13: public Person getPerson(final Long personId) {
14: return repository.getPerson(personId);
15: }
16: }
17:
18:
19: public class MailShotService {
20:
21: public void sendMailShot(final Person person, final Long mailShotId) {
22: Address sendTo = person.getDefaultAddress();
23:
24: // Code here to locate the mailshot and call the printing routine!
25: }
26: }
27:
However, this is not the end of the story as there are still improvements to be made. In particular, exposing the ability to change a domain object in a layer above the services still is problematical: needs session in view pattern; may result in multiple places needing modification if a new cross-cutting concern is required (e.g. audit changes). So, it still has some of the weaknesses of the anaemic model.
We can therefore refine the domain objects even further through introduction of an interface to represent the public face of our main domain entities that expose only the accessor functionality:
1:
2: public interface Person {
3:
4: Long getId();
5:
6: String getForename();
7:
8: String getSurname();
9:
10: Date getDob();
11:
12: Collection<Address> getAllAddresses();
13:
14: Address getDefaultAddress();
15: }
16:
17:
18: public class BasicPerson implements Person {
19:
20: ...
21: }
22:
Now we can return a Person instance that clients of the domain object can use to access the domain details, but they cannot mutate them via this interface. Then, I can update the PersonService to contain all the methods for modifying Person instances:
1: public class PersonService {
2:
3: private final PersonRepository repository;
4:
5: public PersonService() {
6: repository = new PersonRepository();
7: }
8:
9: public Long getPersonId(final String surname, final String forename) {
10: return repository.findPerson(surname, forename);
11: }
12:
13: public Person getPerson(final Long personId) {
14: return repository.getPerson(personId);
15: }
16:
17: public void modifyPerson(final Long personId, final String forename,
18: final String surname, final Date dob) {
19: BasicPerson person = repository.getBasicPerson(personId);
20: person.setForename(forename);
21: person.setSurname(surname);
22: person.setDob(dob);
23: }
24:
25: public void addAddress(final Long personId, final Address newAddress) {
26: BasicPerson person = repository.getBasicPerson(personId);
27: person.addAddress(newAddress);
28: }
29:
30: public void removeAddress(final Long personId, final Address newAddress) {
31: BasicPerson person = repository.getBasicPerson(personId);
32: person.removeAddress(newAddress);
33: }
34:
35: public void makeDefaultAddress(final Long personId, final Address defaultAddress) {
36: BasicPerson person = repository.getBasicPerson(personId);
37: person.makeDefaultAddress(defaultAddress);
38: }
39: }
40:
Finally, I make the constructor of the BasicPerson protected and add a factory so that there is now only one place that Person instance can be created (I also did the same for address, but it's pretty simple so I haven't shown it here):
1: public class PersonFactory {
2:
3: private final PersonRepository repository;
4:
5: public PersonFactory() {
6: repository = new PersonRepository();
7: }
8:
9: public Person create(final String forename, final String surname, final Date dob) {
10: BasicPerson person = new BasicPerson(forename, surname, dob);
11: repository.add(person);
12: return person;
13: }
14: }
15:
Thus, I have clean, well encapsulated domain objects that don't leak details of their internal implementation to the outside world. Wherever possible data has been made immutable to avoid accidental change. Any clients of the domain model can access its state via the exposed interface, but only the service can modify this state - thus making a single location for adding cross-cutting concerns (such as audit). I can therefore be certain that any code using my domain model will not be dependent on implementation details and will not be impacted by changes (provided I ensure the Person interface contract doesn't change).
You just don't get these benefits from an anaemic model without taking incredibly great care and ensuring that every developer who uses your code in the future also takes the same level of care. With a rich, well encapsulated domain model you protect yourself and those who use your code in the future by preventing the bad usage patterns from ever happening (even accidentally).
No comments:
Post a Comment