A class is an unit bundling together data (fields, properties) and behavior (methods).
Many OO languages offer what is called “visibility” for properties and methods.
Encapsulation is exactly these two things: bundling of data and behavior, and not allowing open access to everything.
Why would you want to encapsulate? The primary reason to do so is to keep your object always in a valid state.
Looking at code often written, we see things like “setters”. Just to be on the same page, a setter is a public method which accepts a value and sets one corresponding private property, if valid.
The problem with this is that such code claims to encapsulate the private field, but very often it doesn't, because while the field is indeed encapsulated, the means to call the setter from anywhere is still given, which means object integrity is not guaranteed.
Of course, the setter might validate the input and reject it, possibly by throwing an exception. This will work if you look at the class in isolation. The problem though is that you will have to handle exceptions every time you call the setter, which leads to a lot of duplicated code throughout the project. In practice, this means a lot of
try catch blocks with identical code.
So what alternatives are there?
First of all, do not use setters as defined above. The immediate question which you might ask: but how do I set values without setters? It's the main complaint I hear, but let's recall – there is another way to set values in an object. It's called a constructor. A constructor is nothing else than a grand setter, with the difference that it sets multiple fields, not just one.
A follow-up complaint about this which I often hear is: but then it means that my constructor will have a huge number of parameters.
Luckily, no, it doesn't mean that. One way to look at architecture is like a tree: at the top sit very abstract classes (think for example about a facade from the facade design pattern), then underneath different services, and the lower you go in this view of the architecture, the lower-level components you'll see.
If you decompose your architecture in this tree-like manner, you'll realize that:
- you can group things together, and those “many parameters” to the constructor become just a few; example: a
Personconstructor does not need to get per constructor the age, the name of the person, the city, the zipcode, but can instead receive just age, name, and address, the latter encompassing all other fields
- other objects can create objects of different types; example: a class called “Vienna” can create an instance of a new address within Vienna, passing itself or its name into the address. The factory method
Vienna::createAddress()does not need to receive the name of the city, because the object of type
Viennaalready knows it, so it can pass it into the constructor of
Address, without it being passed from the outside - leading to less parameters at each step
- creating objects becomes an important activity in your system, factories gain importance
- your constructors can throw exceptions if any parameter is not correct
- because of the previous point, less validation in total in the remaining code – since all objects are always valid
If you look at all these points in correlation to each other, you can describe the design like this: classes serve as blueprints for always-valid objects; it is hard work to create always-valid objects, but once created, everything “just works” – you put the objects together like cogwheels running into each other, then you call methods on one of the objects at the “top” of the architecture, and the system just spins all the necessary cogwheels, giving you the correct result, or a runtime exception for truly exceptional cases which can only be detected at runtime (for instance, because of the interaction with an external system, a database, a filesystem, etc).