Encapsulation: Don't Look Up
This is an article I've drafted a couple of times over the last few years and never been able to quite get my thoughts in order enough to publish. However, here's an attempt to get something online with a view to refining it later.
Firstly, I quickly want to demonstrate what broken encapsulation is and why breaking encapsulation is seen as "a bad thing".
We often see encapsulation described in the terms of an object gaining access to another object's state. The classic and simplest example is public properties:
class BankAccount {
public int balance;
public int id;
public void deposit(int amount) {
this.balance += amount;
}
public void withdraw(int amount) {
if (this.balance >= amount) {
this.balance -= amount;
return true;
}
else {
return false;
}
}
}
Because the property balance
is public, any code using the BankAccount
class can avoid the check and withdraw any amount:
class Bank {
public void chargeAccount(BankAccount account, int amount) {
account.balance -= amount;
//Instead of account.withdraw(amount);
}
}
As the Bank
class knows about and can access the balance property, the checks in place in the withdraw method can be avoided entirely. The intention of the author of the BankAccount
class can be avoided and the class is not in control of its own state.
This is trivial and a well known, easy to fix issue.
I call this looking down. The Bank
class is looking down within its current scope into its collaborator: The BankAccount
class and has knowledge of the implementation of the BankAccount
class and encapsulation has been broken.
This kind of broken encapsulation is well documented and very easy to identify.
Before moving on, I want to reiterate why breaking encapsulation like this is a bad thing.
class Bank {
public void chargeAccount(BankAccount account, int amount) {
account.balance -= amount;
//Instead of account.withdraw(amount);
}
}
In the code above, broken encapsulation is a problem because the implementation has become part of the API. You cannot remove the public balance
property from the class because any code dependent on a BankAccount
object may be reading or writing from this property.
You could not, for example, rewrite the class to store the balance in a database:
class BankAccount {
private Database db;
public int id;
public void deposit(int amount) {
db.query('UPDATE accountBalance set balance = balance + ? WHERE id=?', amount, id);
}
public void withdraw(int amount) {
if (this.balance >= amount) {
db.query('UPDATE accountBalance set balance = balance + ? WHERE id=?', amount, id);
return true;
}
else {
return false;
}
}
}
Broken encapsulation is bad
Again, fairly simple but the point I want begin with is why breaking encapsulation "is a bad thing".
Broken encapsulation is bad because one part of the code knows too much about the implementation details of another part of the code. The relationship is intimate and difficult to break up: You cannot easily replace the use of BankAccount
in the Bank
class with an alternative.
This intimate relationship tightly couples the components and makes polymorphism impossible. One component needs a specific implementation of the other in order to work because the relationship is intimate.
In the case above, there is an intimate relationship between the Bank
class and the BankAccount
class. The Bank
class needs this exact implementation of the BankAccount
class to work correctly. Instead, if BankAccount
were an interface it could be substituted with any implementation.
At a very generic level, broken encapsulation can be defined as a component knowing more than it needs to about another part of the codebase.
Looking Up
That's the accepted, well documented view of encapsulation. I want to describe a second type of broken encapsulation that's often discussed in terms of the problems it causes but not specifically labeled as broken encapsulation.
Global variables.
Global variables are the first bad practice most programmers come across. They're easy to use but very quick to cause problems. They've been derided as bad practice since at least 1977!
I won't bore you with examples of global variables and the problems they cause, however, one of the reasons they cause problems is because to use a global variable you have to look outside the current scope.
This is my definition of looking up. By looking outside the current scope you are effectively breaking encapsulation: Whatever reads or writes to a global variable knows that the global variable is being used and what it's being used for. Any time you look outside the current scope you are making assumptions about the environment in which the code is being used.
Code making use of global variables has intimate knowledge of parts of the system outside its own scope. Let me give you one quick example.
On a website I'd worked on, someone had created the global variable Layout::$title
to store the web page's title. It looked something like this:
class ProductController {
public function searchAction() {
Layout::$title = 'Search for a product';
//...
}
public function addAction() {
Layout::$title = 'Add a new product';
//...
}
}
This class knows a lot about the system it's going to be used in. The ProductController
class knows it's being used in an environment where there is a Layout
class with a static property $title
that's going to be used for the page's title.
This is the same as broken encapsulation in the traditional sense, but we don't often see encapsulation being discussed in this way.
Looking up in this sense, is accessing anything that's not within its current scope, which requires making assumptions about where or how the current code is being used.
Some other examples of looking up are:
Static methods
By calling a static method you're looking out of the current scope with the prior knowledge that a class and static method exist in the environment.
Consider the difference between:
class Calculator {
public int addAbs(int num1, int num2) {
return num1.abs() + num2.abs();
}
}
and
class Calculator {
public int addAbs(int num1, int num2) {
return Math.abs(num1) + Math.abs(num2);
}
}
I've used this example before when discussing static methods but the difference is in what's being expressed.
In the first example, we know nothing about the world outside the Calculator
class. In the second, we rely on there being a class called Math
with a static abs
method. The calculator class knows a bit about the environment in which it is being used.
In terms of portability, if your class doesn't look up, it can be moved between projects on its own. It doesn't rely on other classes (other than those it explicitly mentions as part of its API as arguments) existing somewhere in the codebase.
Inheritance
"Inheritance breaks encapsulation" is thrown around a lot but this is especially true in terms of looking up. A subclass by definition can look outside its own scope up into the base class. Inheritance introduces tight coupling because the subclasses is looking up. It has intimate knowledge of the base class.
Annotations
You know the ones I hate @Inject
, @Route
etc. Any annotation which is used to configure the application. I've talked about the problems annotations cause in the past but this is one of the worst offenders of looking up. If you include an annotation you are making a huge assumption about the environment in which the code is being run.
By including @Inject
you are assuming that the code is being run in a environment where there is a dependency injection container that specifically looks for that annotation. Any annotation which is read externally suffers the same issue: Your code is targeted to a very specific environment. You are creating a relationship between the code you wrote and the code that reads the annotation, a relationship which does not need to exist.
Empty interfaces
Empty interfaces were one of the first bad practices I talked about on this blog. And the reason they are bad practice is that they look up.
Empty interfaces are used to signal to some external class, that this class should be treated in some manner. Some other part of the system can then run the code:
if ($obj instanceof EmptyInterface) {
//treat this class specially somehow
}
But by implementing the empty interface just to trigger this external code, your class must know it's being run in an environment where that check is happening.
Conclusion
I'm sure there are a lot more examples of this but a lot of bad practices are caused by the same root issue: Code knowing about implementation details of code outside its own scope.
Nothing I have talked about here is new. Static methods, annotations, inheritance and global variables are frequently described as being bad practice. However, by thinking in terms of encapsulation it's easier to see why.
By thinking about encapsulation in broader terms of knowledge about implementation outside the current scope it's easier to quickly identify practices which are likely to cause problems with portability and flexibility.
If your code knows nothing about the environment surrounding it, your class can easily be taken and moved between projects. If your class is expecting a specific Dependency Injection framework or global variables to exist, you limit the usefulness of your class.
looking up and looking down are the same: Whether your code knows about implementation details of an instance it was passed as an argument or implementation details of a class somewhere higher up the object graph that's reading its annotations, the result is the same: Your code knows too much about other parts of your codebase.
Code should not know about anything outside its own scope. People seem to understand this when it comes to looking down.