Encapsulation explained

A few months ago, I have already written a post about encapsulation. Yet I find the subject important enough to give it another try to explain it. Most computer programming courses pay some attention to the concept of encapsulation. Yet I see often that the importance of this concept is not fully understood, as are the consequences of having a design with poor encapsulation. In this post, I want to dive deeper into the reasons why encapsulation is so important.

Read more: Encapsulation explained

What is encapsulation?

If you do a search on the internet, you’ll find quite a number of definitions, many of which are pretty abstract. In my experience, it is a pretty difficult concept to explain well. Yet, I’ll give it a try.

In addition to the mentioned definitions, encapsulation tells how a class exposes itself to its environment. If an object is well encapsulated, then client code can call methods on the object without knowing anything about how it is implemented. The users of the object only know about the what, not about how.

This definition talks about the methods of the object, so encapsulation is all about the object’s public interface: what does it expose but equally important: what does it hide?

What should be on the interface?

Since encapsulation is all about the object’s public interface, then the main question is: what should be on the interface, and what should not be on the interface?

From a design perspective, the public interface of an object consists of all public but non-static members of a class, so this is everything you can do with an object once it has been instantiated.

If you want to design a well encapsulated class, then the interface must not expose anything that reveals how the class does its job. In other words, when developing class A that uses class B, then the code in class A shouldn’t be concerned about the correct functioning of class B.

This can fail in so many ways. For example:

  • Methods to perform technical tasks: validator.LogToDatabase()
  • Method parameters to indicate how something needs to be done: ValidationResult Validate(bool logErrors)

The goal of a validator is validating, this has nothing to do with logging or database interaction. Those are separate responsibilities, and should be done by different classes. The same counts for the second example: now all clients need to think about logging details when they just need to validate something.

It is not to say that logging need not be done, but a Validator class should not expose anything logging related on its public interface.

Why encapsulation?

In the previous example, why is it important to remove logging related functionality from the Validator class? Let’s first consider a few consequences of the current situation:

  • The class becomes easier to use. No client code needs to think about logging anymore when it wants to perform validation.
  • Logging and validation activities become separated. This means that it is easier to reason about either of the two, because you can be sure that the other will not be affected.
  • If something about logging changes, then the Validator class needs to be changed, and chances are that also the users of Validator must be changed, because they are involved in logging too, even if they don’t want to. So changing a tangled responsibility may require making changes all over the place.
  • This also runs in the next problem: how would you know that the application still runs correctly after such a change? That can only be done by testing the application as a whole.
  • Instantiating a Validator object requires also providing logging related parameters, which you otherwise wouldn’t need.
  • Where would you look when something is not logged correctly?

All in all, it just complicates things. It makes the code more brittle, more difficult to change and very difficult to validate.

Proper encapsulation, on the other hand, improves the code.

  • In case of a bug, it makes it possible to go straight to the class where the bug is. Is there a bug in the validation? Then look in the Validator class. Or do you find a logging issue? In that case, go to the Logger class, you do not need to consider code in the Validator class to be related to the issue.
  • Since your code is not riddled with implementation details of other classes, you can much easier read and understand it. It is also much easier to make changes.
  • Responsibilities are combined into classes, not spread out over multiple classes. So one class is responsible for validation, another class is responsible for logging, and they have no direct interaction. This makes it possible to apply the divide and conquer strategy: divide the problem into smaller problems on a lower abstraction level and then combine those into an integrated solution. Lower level problems are easier to solve.
  • The public interface of a class is smaller so will require fewer changes. For our Validator example, I can imagine there would be only a single method: public ValidationResult Validate(thingToValidate). Also, this method is very likely not to change, because it describes the class’ responsibility.

Encapsulated logging validator

Maybe you ask, what would a design of the above example look like, in case of proper encapsulation? It may look like this:

public interface IValidator {
    ValidationResult Validate(object thingToValidate);
}

public class Validator : IValidator {
    ...
    ValidationResult Validate(object thingToValidate) {
        ....
    }
}

public class LoggingDecorator : IValidator {
    ...
    public LoggingValidator(ILogger logger, IValidator inner) { ... }
    ValidationResult Validate(object ThingToValidate) {
        var result = mInner.Validate(thingToValidate);
        LogErrors(result);
        return result;
    }
}

There are two classes now. Validator is responsible for carrying out actual validation. LoggingDecorator is responsible for integrating validation and logging: it logs validation errors, but the errors come from the inner validator.

Client code expects an IValidator, but it does not know whether it gets passed a LoggingDecorator or a Validator. It also does not care, so client code is not responsible for logging anymore. Note that this moves the decision about logging or not up the call tree, much closer to the initialization stage. This means that the decision about logging or not is taken in a single place, instead of being scattered all over the code.

This example also shows that the class’ constructor receives implementation details. For example, the LoggingDecorator gets the needed ILogger passed to it, it is not passed to the Validate method just because a single implementation needs it. Also, if the need arises to store validation results in the database, then it is simple to implement: instead of adding additional parameters to the Validate method, you can just create a new DatabaseDecorator class that implements IValidator and pass it where needed. You only need to test the proper functioning of the new class, all existing classes remain the same.

In the previous post I wrote about code smells and anti-patterns. Many code smells relate to cases where encapsulation is not properly applied.

Leave a Reply

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