Encapsulation revisited

The third most important software development principle is Encapsulation.

Encapsulation: the principle

Encapsulation is all about hiding information accidental to the clients' needs. The principle basically states the following:

  • The data and the logic operating this data should be packed together in a single cohesive class.

  • The collaboration surface (interface) of a class should be as small as possible.

  • Classes can only communicate with their immediate neighbors.

Keeping the data and the logic together prevents duplication and helps maintaining invariants. Without it, logic quickly spreads across the application, making it extremely hard to maintain.

Encapsulation allows us to decrease coupling throughout the code and thus make it simpler. Also, it helps creating intention revealing interfaces which give us high-level information about the methods' behavior.

There are two other principle which I personally consider parts of the overall Encapsulation principle: Tell Don’t Ask and Law of Demeter. They both state the rules we described before, although Law of Demeter does it in a more formal way.

Often, the lack of encapsulation goes side by side with Anemic Domain Model. A tendency to move all the logic operating the data to some external services breaks encapsulation and often leads to code duplication and increase of accidental complexity.

Refactoring towards better encapsulation: an example

Let’s take some code and see how we can refactor it towards better encapsulation.

Here’s a code sample:

public class Person
{
    public Employer Employer { get; set; }
    public string JobTitle { get; set; }
    public string City { get; set; }
}
 
public class Employer
{
    public string Name { get; set; }
    public string City { get; set; }
}

What problems do you see here? The classes only contain data with no methods, which makes them anemic. It also means that the logic working with this data resides somewhere else. That breaks the first rule of the Encapsulation principle which states that we should pack the data and the logic together.

Let’s take a look at one of the client methods:

public double GetManagerCoderRatio(IList<Person> persons)
{
    int coders = persons.Count(person => person.JobTitle == "Programmer"
        || person.JobTitle == "Software Developer"
        || person.JobTitle == "Coder");
            
    int managers = persons.Count(person => person.JobTitle == "CTO"
        || person.JobTitle == "CFO"
        || person.JobTitle == "Manager");
 
    return managers / (double)coders;
}

This code sample has another problem: the method makes decisions based entirely upon the data of a single object. Namely, it decides whether a person a manager or a coder. Clearly, such decisions should be made by the Person class itself. Let’s change our code:

public class Person
{
    public Employer Employer { get; set; }
    public string JobTitle { get; set; }
    public string City { get; set; }
 
    public bool IsCoder
    {
        get
        {
            return JobTitle == "Programmer"
                || JobTitle == "Software Developer"
                || JobTitle == "Coder";
        }
    }
 
    public bool IsManager
    {
        get
        {
            return JobTitle == "CTO"
                || JobTitle == "CFO"
                || JobTitle == "Manager";
        }
    }
}
public double GetManagerCoderRatio(IList<Person> persons)
{
    int coders = persons.Count(person => person.IsCoder);
    int managers = persons.Count(person => person.IsManager);
 
    return managers / (double)coders;
}

As you can see, now the decision-making process based on the Person’s state encapsulated with that state.

Alright, let’s look at another client method:

public void AcquareCompany(IList<Person> employees, Employer newEmployer)
{
    foreach (Person employee in employees)
    {
        employee.Employer = newEmployer;
        employee.JobTitle = "Consultant"; // All employees now independent consultants with x3 wage
        employee.City = newEmployer.City; // Remote work is not allowed
    }
}

As we can see here, the Person class has an invariant: no person can work remotely, meaning that their city should be the same as the employer’s one.

Although the AcquareCompany method maintains this invariant, the invariant itself can be easily broken. To do it, we just need to assign a person a new employer without changing the person’s city.

It is a good practice to make code easy to use correctly and hard to use incorrectly. The Person class doesn’t adhere to this practice, so let’s refactor it:

public class Person
{
    public Employer Employer { get; private set; }
    public string JobTitle { get; private set; }
    public string City { get; private set; }
 
    public void ChangeEmployer(Employer newEmployer, string jobTitle)
    {
        Employer = newEmployer;
        City = newEmployer.City;
        JobTitle = jobTitle;
    }
 
    /* IsCoder, IsManager properties */
}
public void AcquareCompany(IList<Person> employees, Employer newEmployer)
{
    foreach (Person employee in employees)
    {
        employee.ChangeEmployer(newEmployer, "Consultant");
    }
}

Note the following implications from the change made:

  • We decreased the collaboration surface of the Person class, and thus decreased coupling between the class and the client method. Instead of 3 properties (Employer, JobTitle, City) we now have a single ChangeEmployer method. All the properties' setters are made private.

  • We created intention revealing interface. Instead of a set of low-level operations, Person now has a high-level method (ChangeEmployer) which fully conveys its purpose.

  • The Person’s invariant is now protected and can’t be broken.

Summary

Encapsulation is another principle that helps decrease accidental complexity and keep software maintainable.

Next time, we’ll look at what coupling and cohesion actually mean (with examples), as well as some common misunderstandings of the DRY principle. Stay tuned!

Subscribe


I don't post everything on my blog. Don't miss smaller tips and updates. Sign up to my mailing list below.

Comments


comments powered by Disqus