Encapsulation and class design
Every now and then, I need to have my hair cut. So I go to the barber to have it done for me. I get to sit in a nice comfy chair and she does things with scissors and hair clippers that I know very little about. I basically just have to wait until it’s done.
You may be wondering, what does this have to do with software development? I think it demonstrates a nice concept that we need to use every time we design a solution, an interface, a new class. It shows the concept of encapsulation.
Read more: Encapsulation and class designIf I get my hair cut in a situation like this, then I am the client and the barber is providing a service to me. I don’t have to know how she does it, I only need to know what service I want, possibly along with some details about how I would like the result to be. To me, this is the essence of encapsulation: separating what is done from how it is done.
As you can see from the introductory example, encapsulation as a concept is not only important for software development, but it plays a much wider role. Basically, it is important everywhere an interface is defined. That being said, for the purpose of this post, we will focus on encapsulation at class level. Just remember that the principles of encapsulation can be applied much wider than just class design.
If you design a class with good encapsulation, then the services your class offers (its public members) don’t reveal what’s going on inside, or in other words, how it achieves its task. Take for example the following method from the .NET API. It has the following signature (namespace is omitted for brevity):
File.WriteAllText(string path, string? contents);
It writes the text in contents
to the file denoted by path
. As a user, I don’t need to know how it does its job. I don’t need to know anything about file handles or low-level I/O and I still can get my contents written to the file. This also applies to error handling: if the method fails for whatever reason, I don’t have to be concerned with cleaning up stale file descriptors and things like that. So we can say that the method provides a service to me, the client, by encapsulating how it does its job.
Example of code without encapsulation
I remember that, when I was learning programming, I found encapsulation to be a difficult topic. I used to design my classes with all fields to have public setters and getters, so that at least the fields were encapsulated, and I reasoned that it would be convenient that I could access the fields whenever I would need them. I also reasoned that I could always remove a field’s setter or getter to hide it again. Yeah right…
What I forgot about was that encapsulation is not about getters and setters, but about the act of exposing the data itself, regardless through what way it is done. A client class should normally not ask for the data of its service, it should ask the service to do something. It is as if I go to the barber just to ask for scissors, comb and hair clippers to do my own hair cut. Or that the barber would do the most important part of the job, then hand me the tools so that I can finish it myself.
What is encapsulation
In the previous parts, I gave some examples. But what is encapsulation exactly? If you search the internet, then you’ll see different definitions.
I prefer to use a more abstract definition: “a class that exhibits good encapsulation exposes the what without exposing the how of its responsibility”. This means that the clients do not need to be concerned about the class’ implementation details.
Why is encapsulation important
There are several reasons why encapsulation is important. For example, have a look at this (made up) class definition:
class CarRepository {
Car FindById(int id);
}
It allows its clients to find a car based on an identifier. However, identifiers are specific to relational databases. Object oriented languages use object references, not identifiers. So this method exposes the fact that the repository is implemented by means of a relational database. This is a bad thing, as it now forces the clients to record ids as well. If, for whatever reason, the relation database needs to be replaced with something else, then a large part of the code must be changed.
This demonstrates an important aspect of encapsulation: it makes the code more flexible. If the CarRepository
does not expose the id, then replacing the relational database with something else only involves changing the CarRepository
and no other parts of the program.
Or, when viewed from the other side, not exposing internals encapsulates the responsibility of database interaction instead of spreading it over all client code. And a better separation of concerns improves maintainability: only in the CarRepository
implementation will the database mechanics be important. Outside of the class, the code does not need to take into account that there is database interaction at all. This gives more flexibility.
Encapsulation is closely related to abstractions: both together enable a class to have a public interface that is understandable and reusable. Encapsulation (not exposing the how) is the tool that makes it possible to define a good abstraction.
Signs of good and poor encapsulation
How would you know if you have a good level of encapsulation for your code? It is often difficult to see the distinction between what and how. This requires some skill. But I think there are several heuristics for that. Remember that those are heuristics: they apply in the majority of the cases, but not always does violation of a heuristic mean that encapsulation is bad.
- A class with good encapsulation has a smaller interface than a class with poor encapsulation. This just follows from the fact that not all internals are exposed.
- If an interface struct does not expose implementation details, then it becomes much easier to add another implementation for it. So another sign of good encapsulation is that interfaces can have multiple implementations, and that it is easy to add more implementations for an interface.
- Encapsulation takes a complete responsibility including error handling. This means that methods that return a boolean flag to indicate that something else needs to be done break encapsulation. The same is valid for methods that take boolean flag arguments: this would be like me telling the barber how she would need to use her tools. If I’d have to do that, then I’d question the skills of the barber.
- Client code has to perform checks before calling a method:
if(car.IsStarted()) { car.Move() }
. The class should protect its own invariants instead of letting its clients do that. This will result in a class design with very few test methods (methods that do nothing but just query, likeIsAvailable
orCanSave
) and allows you to focus on methods that actually do things. - Data and operations on the data are bundled together within the class. The class has no getters or its internals, but only functions and methods that do actual work.