What is the problem
Suppose we have domain models like this:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Person {
ID id;
PersonName name;
Gender gender;
}
class Mail {
Address address;
MailGroup group;
ID? idOfOwner;
}
class MailGroup {
String name;
Person[] admin;
}
One day, product owner asked me to do a story with the following requirement:
Given a mail group, list female owners of mail addresses in this group. The list should exclude the admin of this mail group for whatever reason.
A straightforward implementation
I check the code base and find the following repositories for mail and person:1
2
3
4
5
6
7
8@service class MailRepository {
List<Mail> mailsFromGroup(String groupName) {...}
MailGroup load(String name) {...}
}
@service class PersonRepository {
List<Person> load(List<ID> idList) {...}
}
Here is the a new method in repository of mail group:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21@service class MailGroupRepository {
@Inject MailRepository mailRepo;
@Inject PersonRepository personRepo;
List<Person> femaleMemberOfGroup(String groupName) {
List<ID> idOfAdmins = mailRepo.load(groupName).admin.stream()
.map(admin -> admin.id);
Predicate<ID> isAdmin = id ->
idOfAdmins.stream().anyMatch(adminId -> adminId != id);
List<ID> idOfOwners = mailRepo.mailsFromGroup(groupName).stream()
.map(mail -> mail.idOfOwner.orElse(null))
.filter(id -> isAdmin.andThen(ID::isNotEmpty).test(id)));
return personRepo.load(idOfOwners).stream()
.filter(person -> person.gender == FEMALE);
}
}
So far so good until later product owner asked me to only filter out group admins who work for police station. Then I have the chance to review the method femaleMemberOfGroup. Isn’t it too complicated as a service method? And who knows what kind of logic I would be asked to add to this method. It looks like I was working in the old days as a C programmer. All I had were two things: data container (struct) and procedures. Now I’m programming in the same way with an object-oriented language.
What’s more, I’m told to create unit test to cover the method. How to deal with the injected services that are used as dependency of this method? I tend not to use mock objects for its horrible API. Can I do it better?
A domain model
Domain model is not just a data container as the above models. Here I see a domain model of GroupFemaleMember:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50class GroupMember {
// type alias
static interface GroupAdminsFunc extends Function<String, MailGroup> {}
static interface GroupMailsFunc extends Function<String, List<Mail>> {}
static interface PersonsFunc extends Function<List<ID>, List<Person>> {}
// fields
private final GroupFunc loadGroup;
private final GroupMailsFunc getMails;
private final PersonsFunc getPersons;
private final String groupName;
// constructor ...
// business method
public Supplier<List<Person>> femaleMembers() {
return () -> {
List<ID> idOfAdmins = loadGroup.apply(groupName).admin.stream()
.map(admin -> admin.id);
Predicate<ID> isAdmin = id ->
idOfAdmins.stream().anyMatch(adminId -> adminId != id);
List<ID> idOfOwners = getMails.apply(groupName).stream()
.map(mail -> mail.idOfOwner.orElse(null))
.filter(id -> isAdmin.andThen(ID::isNotEmpty).test(id)));
return getPersons.apply(idOfOwners).stream()
.filter(person -> person.gender == FEMALE);
};
}
}
public final class GroupMemberObject {
public static GroupMember apply(
final MailRepository mailRepo,
final PersonRepository personRepo,
final String groupName
){
return new GroupMember(
name -> mailRepo.load(name),
name -> mailRepo.mailsFromGroup(name),
idList -> personRepo.load(idList),
groupName
);
}
}
GroupMemberObject knows about how to create the domain model GroupMember from other services. It can be put in the layer above domain model so that there is no dependency issue (high layer can see the low level, not the opposite way). Unit test can rely on GroupMember itself, which has no dependency to external services.
Unit test of domain model
1 | public GroupMemberTest { |
There is no mock objects since GroupMember is self-contained after the initialization. With the new domain model, the service method is simplified a lot.
How to use domain model
1 | Supplier<List<Person>> femaleMemberOfGroup(String groupName) { |
It doesn’t matter whether we need to cover it with unit test since there is almost no logic inside. Also note I adapt the return value from materialized list to a lazy-evaluated supplier, which can postpone the execution of the real service method until the moment when it’s used by application code. A potential advantage is I can add cache support (memorize computational result, maybe as the next topic) or even I can find a way to group multiple callings into one for better performance.
To sum up, domain model is everywhere. It’s invisible to the eyes only with CRUD and get/set.