Friday 7 March 2008

Balancing Greed with Anemia

I am many things to many people. If I go to the doctors I am a patient, when at work I am an employee, when at home, with my wife, I am a husband. I cannot be all these things at the same time and I need to behave differently with different people and what's more my doctor isn't interested in me as an employee, my employee cares not for my detailed medical history and my wife certainly doesn't care what languages I can program in let alone the content of my blog!

Readers of Domain Driven Design will be familiar with the concept of Aggregates: simply put an aggregate provides a boundary around concepts which are closely related and provides an entry point to those other entities. If you were modelling people the Person class would be an aggregate, within the person class would be other classes and methods to do with my medical state, languages I can program in and package private methods between my wife and I. Using a behaviour centric domain finding we can write code like this:

class Client 
def do
john = employeeRespotory.find("John")

if(john.is_not_skilled_in(:AnyJava)) do
john.train(:BeginnersJavaCourse)
end

john.can_do_job(Job.new(:SqlServer, :DotNet))
end
end

class Person < Skilled
skills

def is_skilled_in(skill)
if(skills.not_contains(skill.type)) do
return false
end

return skills.get(skill.type).is_at_level(skill.level)
end

def is_not_skilled_in(skill)
!is_skilled_in(skill)
end

boolean train(Training)
if(skills.not_contains(Training.skill) do
skills.add(Training.skill)
else
skills.get(Training.skill).upgrade(Training.level)
end
end

def can_do_job(Job)
Job.matches_skills(this)
end
end

class Job
def matches_skills(skilled)
Skills.each | skill | do
return false if skilled.is_not_skilled_in(skill)
end
end
end

class Test
def can_be_skilled_in_something
skills = Skills{:BasicJava}
testPerson = new TestPerson(skills)

assert_true(testPerson.is_skilled_in(:BasicJava))
end

def can_be_trained_in_something
testPerson = TestPerson.new(:NoSkills)
skill = Skill.new(Java, Basic)

testPerson.train(Training.new(skill))
assert_true(testPerson.is_skilled_in(skill))
end
... plus all the other tests
end

The problem is if we placed all of this behaviour and data in one Person class it will quickly get on the big side. This is what I call a Greedy Aggregate: in the same way I don't try to be all things to all people a class shouldn't try to be all things to all clients.

The biggest criticism I have heard about placing behaviour on Domain objects is the problem of Greedy Aggregates: huge classes that end up with bags of methods and test classes as long as your arm. The solution often presented is to move to a Service paradigm instead. I agree with the complaint of Greedy Aggreates but not the solution as the service paradigm moves code away from being object orientated and towards procedural. The above in service orientated code would be:

class Client 
def do
john = employeeRespotory.find("John")

if(employeeService.is_not_skilled_in(john, :Java)) do
employeeService.train(john, :BeginnersJavaCourse)
end

employeeService.can_do_job(john, Job.new(:SqlServer, :DotNet))
end
end

class EmployeeService
skillsRespository
def is_skilled_in(person, skill) do
if(skillsRespository.not_contains(person.id, skill.type)) do
return false
end

return skillsRespository.get(skill.type).is_at_level(skill.level)
end

def is_not_skilled_in(person, skill)
!is_skilled_in(person, skill)
end

def train(person, training) do
if(skillsRepository.not_contains(person.id, training.skill)) do
skillsResponsity.add(person.id, Training.skill)
else
skillsResponsity.get(prson.id, taining.skill).upgrade(training.level)
end
end

boolean can_do_job(person, job) {
job.eachSkill |skill| do
return false if is_not_skilled_in(person, skill)
end

return true
end
end

def Test
def canBeSkilledInSomething
testPerson = TestPerson.new
skill = new Skill(Java, Basic)
MockRepository {
expect.contains(testPerson.id, skill.type)
will.return(true)
expect.get(testPerson.id, skill.type)
will.return(new Skill(:Java, :Basic))

}

assert_true(employeeService.is_skilled_in(testPerson, :BasicJava))
end

def can_be_trained_in_something
testPerson = TestPerson.new
Skill = Skill.new(:Java, :Beginner)

MockRepository {
expect.contains(testPerson.id, skill.type)
will.return(false)
expect.add(testPerson.id, skill.type)
expect.get(testPerson.id, skill.type)
will.return(new Skill(:Java, :Basic))

}

employeeService.train(testPerson, Training.new(skill))
assert_true(employeeService.is_skilled_in(skill))
end
... plus all the other tests
end

The service client code feels more difficult to read: it flows less as a sentance making the Service the subject and not John who has disapeared somewhere in the parantheses. The Service code itself is more difficult to read than the behaviour class and the service test class is significantly more complex. Also the service requires a more complex repository and the Person class is reduced to being nothing more than a data holder. Another problem with service based is you often have to break encapsulation to get it to work and, as a result, you can end up with a lot of repetation in code.


I believe the issue with Greedy Aggregates mainly stems from making code centric to O/R mapping tools. To simply model our Person domain we end up with:

  • A Person object which maps to a Person table

  • A Medical History class with a table with a Person_Id foreign key

  • A Skill class with a table with a Person_Id foreign key


This makes it difficult to split the Person into smaller more specific classes as the O/R mapper requires a definate class to map to. In the above code we may want to split Person into two classes: one which represents the core aspects of a Person (name, age etc.) and another for their skills lets call it PersonWithSkills. In many O/R mappers this is difficult because we cannot create a mapping for PersonWithSkills as two classes cannot map to the same table. However we can create a repository which ties them together:

class PersonWithSkillsRepository
def get(id)
person = personRepository.get(id)
skills = skillsRepostitory.get(person)
return PersonWithSkills.new(person, skills)
end
end

This is how I've tended to advocate it in the past but recently I was playing with Ruby, and using its cool dynamic powers I approached it in a different way: essentially I merley extended the Person class using a module like so:

class Person 
has_many :skills

def with_skills
self.extend PersonWithSkills
end
end

module PersonWithSkills
def is_skilled_in
... # all skills based methods here
end

The only issue with this method is all of the ActiveRecord declarations have to be on the Person class - there may be a way around this but I don't know Ruby well enough to say for sure. It also has the disadvantage of not working for any classes outside of the core package (though knowing Ruby there may be a way around this to). However if you want to do more complex logic between role based aggregates you can. For example:

class Person
def as_traveller
self.extend PersonAsTraveller
end
end

module PersonAsTraveller < Locatable ... end

module TravellingPersonWithSkills < Skilled
def can_do_job(job)
job.matches_location(self.as_traveller) && job.matches_skills(skills)
end
end

class Job
def matches_location(locatable)
return locatable.matches(location)
end
end

Dynamic languages such as Ruby make it nice and easy to do the above as it has the ability to mix in methods. That doesn't mean we can't do something similar in a static langauge such as Java or C# it simply means that we have to jump through a few more hoops and use delegation instead.

So now that we are using behaviour based domain objects and moved away from services and have managed to cut those Greedy Aggregates down to size when is it right to use a service? A service will need to be introduced when there is something normal domain objects cannot be trusted to deal with something by themselves and require some co-ordination or greater authority to do it for them. The service must be stateless. A good example of this is transferring money between accounts: you don't want to leave the accounts to sort it amongst themselves so you will need a service to deal with the interaction:

class AccountTransferService
def transfer(account_from, account_to, amount)
if(account_from.has_funds(amount)) do
account_from.debit(amount)
account_to.credit(amount)
end
end
end

There are a number of ways of dealing with Greedy Aggregates in both dynamic and static langauges. What is important is being able to clearly identify aggregates and not be to restricted by the O/R mapper or simply give up and rely on services.

No comments:

About Me

My photo
West Malling, Kent, United Kingdom
I am a ThoughtWorker and general Memeologist living in the UK. I have worked in IT since 2000 on many projects from public facing websites in media and e-commerce to rich-client banking applications and corporate intranets. I am passionate and committed to making IT a better world.