Pragmatic Objects
  • Pragmatic Objects
  • What's an Object ?
  • Thinking Object
  • Domain Modeling
  • Information Management
  • Practices
    • The Pragmatic Practices
    • No Getters & Setters
    • Inherit Wisely
    • Wrap Null
    • Wrap Primitives
    • Wrap Collections
    • Expose Snapshots
    • Abandon Composed Names
    • Don't Check Types
    • Minimize Knowledge
    • Immutability
    • Separate Commands & Queries
    • Abandon Statics
    • Invert Dependencies
  • Patterns
    • Always Valid
    • Wrapper
    • Command
    • Procedural Object
    • Compute Object
    • Snapshot
    • Value Object
    • Observability
  • Examples
    • Celsiuses / Fahrenheits
Powered by GitBook
On this page
  • Example
  • Counter Example
  • The rules
  1. Practices

Minimize Knowledge

Reduce your knowledge of other objects and their internal structure

PreviousDon't Check TypesNextImmutability

Last updated 7 days ago

I've talked at length about Information Managementand how important it is to keep knowledge minimal.

Coupling is a dangerous thing, because it reduces the degree at which components can evolve without impacting the others.

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 and the . 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));

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.

And 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.

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 ? It depends on many things, including how stable that Variant object is. However, the Variant object becomes exposed to the external world. Is it an internal detail of the Image class or is it a proper citizen of the neighborhood of classes ? In the last case, this implementation is fine, especially if it makes sense for Variants to exist on their own. In the first case, we might want to keep that knowledge inside.

What about this ?

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

With this implementation, Variants are no longer exposed. They remain a detail of the Image class.

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;
}

Adding properties to the Variant object may and probably will impact the construction of the object and thus the objects that depend on it. If the height and width properties are required, then they must also be provided as part of the method add.

So, what do we do ? We have two choices.

  • Keep control of the Variant because we believe that encapsulation is much more important than repetition

  • Expose the Variant to the world and expose it to possibly countless clients, making it more rigid and losing our capacity to change it at will

How to decide which approach is the best ? Ask yourself.

  • Is the Variant object stable ?

  • Will we need to change it in a way that may break client code ?

  • Does it make sense to talk about Variants outside of the Image object ? Should outsiders be able to inspect and work with Variants outside of the Image ?

If the object is stable, has very little reason to change and if you can make the argument that it can be used outside the object that manipulates it, then it makes sense to expose it.

Otherwise, since we favor encapsulation, we'd prefer to keep control.

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.

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 .

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

Tell don't Ask
Law of Demeter
design by contract
Facades