Minimize Knowledge

Reduce your knowledge of other objects and their internal structure

I've told it many times and i'll say it again.

What you know can and will be held against you.

The burden of knowledge is a weight that killed many people and many projects.

It's especially true in programming. We've learned how coupling is a dangerous thing, because coupling reduces the degree at which components can evolve without impacting the others.

Turns out, coupling is a synonym for knowledge. The more you know about a class, the less that class has the possibility to change those bits you know about.

And by "you", I mean your object.

Thus, we want to strive to minimize knowledge as much as possible and free our collaborators of any additional duty. After all, they have enough to do !

This idea is called the principle of least knowledge and is linked to concepts such as Tell don't Ask and the Law of Demeter. They all boil down to the same idea : Minimize Knowledge.

Example

Let's simulate a user with a wallet.

class User {
    private wallet: Wallet;
}

We're asked to be able to add some coins inside that user's wallet. What will we do ?

A naive implementation may look like this.

class User {
    private wallet: Wallet;
    
    getWallet() {
        return this.wallet;
    }
}

// Some code later
user.getWallet().add(new Coins(10));

What's wrong with that ? Everything, including many things we've already covered in No Getters & Setters.

Here, we're not giving coins to the user. We're forcefully taking that damn wallet out of his pocket, inserting coins inside it and throwing it back to their face.

Even animals would be flabbergasted.

The proper behavior would be to gently ask that user to open their hand and deposit the coins.

class User {
    private wallet: Wallet;
    
    deposit(coins: Coins) {
        this.wallet.add(coins);
    }
}

user.deposit(new Coins(10));

Isn't that much better ?

Of course, it is.

But what did we really earn with this move ?

A lot of flexibility.

See, the User currently holds coins inside a wallet. What if they want to keep it directly inside their pocket ? Or to take a payment with their phone ? What if that specific user refuses coins and only accepts dollar bills ?

Suddenly, all that logic can be encapsulated into the "deposit" method and the power be given back to the user.

Thus, we can freely adjust the internal design of that class, by sacrificing some external design. That's fine, because the other name of external design is contract, and it's the minimum set of capability we're forced to implement, voluntarily. That's also called design by contract.

Counter Example

Let's take a simple example of an image containing variants. A variant is the same image but with a different url, it can be a thumbnail or an optimized format such as webp for example.

class Image {
    private variants = new VariantCollection();
}

class VariantCollection extends Collection<Variant> {
    byName(name: string): Option<Variant> {
        // logic
    }
}

class Variant {
    private name: string;
    private url: string;
    
    getUrl() {
        return this.url;
    }
}
    

We need to add some behavior to this Image class : we want to add another variant. How shall we proceed ?

The naive, straightforward way would be to do as follows :

class Image {
    private variants = new VariantCollection();
    
    add(variant: Variant) {
        this.variants.add(variant);
    }
}

By doing so, we expose the Variant object and make clients aware of it.

Is it a good thing ? I don't know, what do we earn by opening Variant to the public ? I don't see much benefit in doing so.

However, we lose a lot : now, our Image is forced to deal with the Variant object. It can't change its internal implementation anymore. What if it wanted to implement the variants using another set of classes & pattern ? What if it just wanted to use a stupid simple array for performance reasons ?

All of that freedom has been traded away from them.

What about this ?

class Image {
    private variants = new VariantCollection();
    
    add(name: string, url: string) {
        this.variants.add(new Variant(name, url));
    }
}

Image regained the control of its internals and is now free to use whatever implementation they desire. That's an happy object.

However, is that free ? What did we trade to reduce that coupling ?

We lost some expressiveness.

Imagine that Variant grows with a lot of capabilities.

export class Variant {
    private name: string;
    private url: string;
    private height: number;
    private width: number;
    private pixelDensity: number;
    private size: Bytes;
}

Do you imagine yourself using a 7-argument method ? That's Hell on Earth.

But if we follow the Minimize Knowledge rule strictly, that's the way to go. So we have to think a little bit harder about it.

Inevitably, our Add method is tightly coupled to the Variant object no matter what we do. Everytime the Variant object moves, we'll have to reflect that change on the add method. So what did we earn by removing the knowledge out of clients ?

Absolutely nothing.

That Variant class is the very definition of what makes a variant inside of an image, so there's nothing we can do to avoid it. We're bound to expose that object to the outside, either voluntarily by exposing the object itself, or accidentally by making the add method uselessly more complex.

Thus, for this specific problem, we can't minimize the knowledge more. That Variant thing is part of the contract of the Image, and not its internal representation as we may have thought initially.

The rules

Sometimes, you need to know, and some other times, you don't.

That doesn't help you much, right ?

So here's a list of guidelines to help you decide :

  • Avoid talking to the neighbour of your neighbour. That is, avoid acting on an object that is part of another object's internal representation

  • You can know about the objects you create and inject onto other objects; Generally speaking, it's fine to know about the object's direct collaborators since they're injected most of the time.

  • Chaining more than two getters (or two dots) is a signal that you're probably doing something wrong, and that your calling object knows too much.

  • You may think of your objects as acting like Facades, the underlying concept of the Law of Demeter, but be wary of objects with too many methods

Last updated