zondag 25 maart 2012

Domain Driven Development - Business Logic in Domain Objects


Introduction
One of the recurring questions in developing software in C# is where to put Business Logic. It's quite easy to scatter Business Rules throughout the system, in such a way that it will be very hard to maintain the code.


Let's take for example this Business Rule:
When a Phase is closed, no more Tasks can be added to it.


If we build a simple system these two objects can look like this in code:


public class Phase
{
   public int Id { get; set; }
   public bool IsClosed {  get  set ; }
   public List<Task> Tasks {  get  set ; }
}


public class Task
{
    public  string Name {  get   set ; }
}


And we can interact with Phase and Task like this:


var phase = _phaseRepository.GetById(1);


if(!phase.IsClosed)
{
   phase.Tasks.Add(new Task());
}



What is the problem with this code?
The problem with the code above is that if in one place we forget to check if the Phase is closed, we can add Tasks to a closed Phase. So, mistakes are quite easy to make in this case.
Another problem is that we have to copy the check for IsClosed every time we want to add Tasks to a Phase in another part of the system.
And a third problem is that whenever we add another condition to the Business Rule, we have to go through all code where we Add Tasks and add code for the new condition.


What is the solution?
It's better to move this Business Rule to the Phase object, like this:


public class Phase
{
   public int Id { getset; }
   public bool IsClosed { getset; }
   public List <Task> Tasks { getset; }


   public void AddTask(Task task)
   {
      if(IsClosed)
      {
         throw new Exception("Can't add tasks to closed Phase.");
      }
      Tasks.Add( task );
   }
}

Now we can simply use the code above like this:

var phase = PhaseRepository.GetById(1);
phase.AddTask(new Task());




This looks a lot better. And if we add another property to the Phase which has to be checked when we add a Task, we only have to change the Phase object, and not the code in the controller.


We're not there yet
Although we now have a nice AddTask method, we can still easily get around it, because the List<Task> Tasks on Phase is public and has an Add method. So we can do this:


var phase = PhaseRepository.GetById(1);
phase.AddTask(new Task());
phase.IsClosed = true;
phase.Tasks.Add(new Task()); 


We can solve this by making the Tasks list readonly:


public class Phase
{
   public int Id { getset; }
   public bool IsClosed { getset; }

   private List<Task> _tasks;
   public  ReadOnlyCollection<Task> ReadOnlytasks
   {
      get
      {
         return new ReadOnlyCollection<Subforum>(_tasks);
      }
   }
}


And you will see that the Add methods isn't supported anymore. So everytime we want to add a Task to a Phase we have to use the AddTask method.


var phase = PhaseRepository.GetById(1);
phase.AddTask(new Task());
phase.Tasks.Add(new Task()); 


Entity Framework
So what if we use Entity Framework, does this solution work? No it doesn't work completely, because Entity Framework doesn't support ReadOnlyCollection<T>. But I have a solution for that, as you can read in the blog post I wrote:


Readonly collections with Entity Framework

Geen opmerkingen:

Een reactie posten