Categories
Clean Code

Are you sure you write object-oriented code?

Most of us take for granted that object-oriented paradigm is not only mature, but the “good” way of developing applications. Despite the fame of functional programming which, as time goes by, becomes a real power, most of software is created in the object-oriented way. Or at least we believe so.

But following the principles is not easy.

How to fail and come up with anemic domain model

Let’s say you have two objects, one of class Fridge and one of class Person. One would expect from the fridge to have methods like .Open(), .Close() and we believe that our person should be able to collect things from Fridge and store it in its state, don’t we?

namespace AnemicToRich.Before
{
   class Program
   {
       static void Main(string[] args)
       {
           var fridge = new Fridge();
           var person = new Person();
 
           PassMilkBetween(fridge, person);
       }
 
       static void PassMilkBetween(Fridge fridge, Person person)
       {
           foreach (var milk in fridge.Milk)
           {
               person.Milk.Add(milk);
           }
 
           fridge.Milk.Clear();
       }
   }
 
   class Fridge
   {
       public ICollection<Milk> Milk { get; }
 
       public Fridge()
       {
           this.Milk = new List<Milk>()
           {
               new Milk()
           };
       }
   }
 
   class Milk
   {
 
   }
 
   class Person
   {
       public ICollection<Milk> Milk { get; }
 
       public Person()
       {
           this.Milk = new List<Milk>();
       }
   }
}

Programming languages are, unfortunately, versatile. As they let us write low-level code most of us begin with when learning to code, we often follow this opportunity and, with a bunch of if statements and public setters, it’s easy to create a working solution and, for instance, take some milk from the Fridge and put it in the hands of our Person object. But that’s exactly what you should not do!

The most common mistake I notice when reviewing developers with little experience is taking the position of god and changing the state of objects themselves instead of letting the objects do this to one another.

This problem has plenty of names – original OOP programming handbooks would call it “lack of encapsulation”, DDD schools call it “anemic domain model” and I call it “human-mediator antipattern”.

What does it mean “easier to reason about”?

Creating a world in which Fridge has only data accessors and Person has only data accessors as well and the code in between deals with transferring the data (or changing state) between objects is easy. Maintaining it is not. Here’s why.

The code “in between” is an operation. An operation that:

  • contains some logic,
  • is nearly impossible to be reused,
  • adds a redundant actor.

And the last bullet is in my opinion crucial. If you want two or more objects to interact in some way, just let them do it. The redundant actor in this scene is like you had not only a fridge and a person, but also you standing in between them, taking milk from the fridge and passing it directly to the person’s hand.

There should be no developers in the code

Have you wondered why is it tempting to leave the imperative code as it is? You’ve put literally yourself in the method and you directly put the “ifs” where the business wanted you to do. That’s why fixing imperative code with anemic domain model is not easy.

Nonetheless, as we believe we work in object-oriented paradigm, it is necessary to refactor our code and write it from completely different point of view – pure interaction between objects.

namespace AnemicToRich.After
{
   class Program
   {
       static void Main(string[] args)
       {
           var fridge = new Fridge();
           var person = new Person();
 
           person.TakeMilkFrom(fridge);
       }
   }
 
   class Fridge
   {
       private ICollection<Milk> Milk { get; }
 
       public Fridge()
       {
           this.Milk = new List<Milk>()
           {
               new Milk()
           };
       }
 
       public IEnumerable<Milk> WithdrawMilk()
       {
           foreach (var milk in this.Milk.ToList())
           {
               this.Milk.Remove(milk);
               yield return milk;
           }
       }
   }
 
   class Milk
   {
 
   }
 
   class Person
   {
       private ICollection<Milk> Milk { get; }
 
       public Person()
       {
           this.Milk = new List<Milk>();
       }
 
       public void TakeMilkFrom(Fridge fridge)
       {
           foreach (var milk in fridge.WithdrawMilk())
           {
               this.Milk.Add(milk);
           }
       }
   }
}

What we achieved has plenty of names, just as the problem we’ve originally encountered. Above code is called “rich domain model” or “well-encapsulated”.

The difference between getter and behavior

I expect many questions about the method responsible for emptying fridge of milk. What’s the deal and what’s the difference between such an implementation and exposed getter?

Well, actually it’s not about encapsulation of removing milk from the collection – it’s… about the name! What we have here is a business behavior which has a name of business operation (as far as emptying a fridge is related to any business at all).

As a result, as long as this behavior is present in domain model, you can change the implementation anytime you want without breaking the way of invoking this behavior. A name is a vital part of encapsulation and without proper name there’s no reason to encapsulate at all, because there will be changes – at least change of the name.

Private setter is no encapsulation

One minor thing I’d like you to notice is the presence of private setter of ICollection<Milk> in the “before” section. As you can see, it didn’t stop us from using .Clear() method from the outside of an object. Conclusion is that private accessors are by no means enough to provide encapsulation and the reason is that encapsulation is not about language features like private readonly fields and private properties – it’s about the way we structure our model. Object has state and behaviors – methods and fields are implementation detail and can differ in various languages, whereas object-oriented paradigm can be applied everywhere!

Leave a Reply

Your email address will not be published. Required fields are marked *