Tom Butler's programming blog

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 accountint 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 accountint 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=?'amountid);
    }

    public 
void withdraw(int amount) {
        if  (
this.balance >= amount) {
            
db.query('UPDATE accountBalance set balance = balance + ?  WHERE id=?'amountid);
            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.