The Tell Don’t Ask Principle Explained

This post will look at the principle of Tell Don’t Ask. The reason I wanted to write this was that for the longest time the naming of it confused me. I saw objects asking things of other objects all the time. However, it was a square peg round hole situation for me. It wasn’t until I learned about the Feature Envy code smell and had watched Allen Holub’s Java Fundamentals: Object-oriented Design course on Pluralsight that it became a little clearer.

Tell Don’t Ask and Object Design

Tell Don’t Ask is the practice that prevents Feature Envy. It says that objects shouldn’t burden themselves with the knowledge of other object’s internals. Objects shouldn’t ask for data from other objects in order to accomplish something. They should tell those objects what they want doing via methods. Put more succinctly:

 

The object that has the data does the work. – Allen Holub

 

This is one of the best quotes I’ve seen about Object-oriented design, which I heard on the Allen Holub Pluralsight course above. It’s a deceptively simple yet powerful statement. I began to view designs differently through the lens of this quote. I found myself questioning my own designs more. There’s another good quote I saw from Steve Freeman and Nat Pryce in Growing Object-Oriented Software Guided by Tests:

 

Ask the question we really want answered, instead of asking for the information to help us figure out the answer ourselves. – Growing Object Oriented Software Guided by Tests

 

When I first heard about Tell Don’t Ask I thought, “that doesn’t make any sense, how do you get any work done?”. However, the don’t ask part doesn’t mean you can’t call query methods on an object. Telling another object you want the output of one of its methods is fine. This could technically be viewed as a form of asking, but the distinction is a matter of coupling.

Query methods hide details. With a query method client code has no knowledge of how the answer comes into existence. The collaborator’s public interface hides the details of answer creation. The collaborator could hold the state required to create the answer or another object could have helped it. What’s important is that the client code doesn’t know anything about this. The client shouldn’t know or care. All the client should know of its collaborator is the public interface that it presents to it.

When objects expose their internal state they invite clients to consume it. This leads to coupling.

What Happens When Bananas Change?

For example, let’s say we have an object called Banana that has public state that other objects can access. Also Banana has 3 clients who all use this state in different ways. If we want to make a change to Banana we also have these 3 clients to consider who may be impacted. We end up with a coupling issue.

To mitigate the risk of changes rippling through the code we should constrain the clients by having them communicate with Banana via method calls. This allows Banana to collaborate with its clients but without them knowing about the internals of Banana itself. In this situation, it’s much easier to change the design or internal structure of Banana.

As long as Banana’s interface stays the same we can change its implementation without clients being impacted. Conversely, if clients are dependent on the internal state of Banana and we decide to make a change to its internal design then we have a bigger maintenance issue to deal with as the clients will need to change too.

Tell Don’t Ask in Pictures

That was a bit wordy so let’s look at some code and images illustrating it. Below we have two objects, the client who is the object that requires the help of another object and the collaborator who is the object that can provide that help.

Above the black circles represent the public interfaces of each object. This is the boundary that other objects shouldn’t be able to see passed, they can see up to it but what happens inside the circle is a mystery to it, it’s a black box.

 

Above we can see that collaborator has some internal state which is represented by the coloured shapes. Here we still have a happy situation as the client can only see the public interface of the collaborator and it has no idea that it holds the shapes within.

 

 

Now we have a situation where the client can see the shapes inside of collaborator, it can see its internal state. A client shouldn’t know about the internal structure of a collaborator. The shapes would be public properties on the collaborator in a language like C# or Java.

 

The client’s goal is to get some work done with the help of the collaborator. However, in this situation, it could see the internals of the collaborator and circumvented its public interface. The client reached into it and used its state directly in order to do the work itself. Talk about violation!

Maybe the client took the state and implemented the work itself because there was no appropriate method on the collaborator. Or even worse maybe there was an appropriate method but now the client has duplicated rework of what the collaborator could have done. Either way, this leads to a break in encapsulation. The client is asking for information from the collaborator when it could have told it to do the work for it. Remember, the object that has the data does the work.

This is the situation where you see the Feature Envy code smell. In this example, the client is more interested in the state of the collaborator than its own internal state. The appropriate refactoring is to move the envied behaviour onto the envied class. The behaviour implemented in the client should be moved over to the collaborator. The collaborator is the one with the state therefore it should have the behaviour.

Tell Don’t Ask and Value Objects

Naturally, there are exemptions to this. For example, Value Objects which are used to represent a measure or description of something. A good reason to use them is they help express a domain better than primitive types. It’s encouraged to have behaviourally rich and immutable Value Objects.

It’s also easy to overthink these things too. Sometimes it’s just easier to use public properties rather than create a method. The things to consider are the stability of the abstraction, its rate of change and number of usages. I favour creating methods until I feel a strong sense of, “come on, this is overkill”. The type of feeling that comes from the conflict created by best practice colliding with pragmatism.

Everyone’s context is slightly different so it’s best to be deliberate and pragmatic rather than dogmatic. Ultimately if you have lots of anaemic objects and you’re smelling Feature Envy then it’s a good indicator that you have misplaced behaviour. Fix this by extracting methods and moving them on to the objects with the envied state.

If you’d like to learn more about Value Objects I’d recommend Vladimir Khorikov’s Domain-Driven Design in Practice course on Pluralsight. And if you don’t have Pluralsight there’s some nice free DDD material on Microsofts docs here.

Asking, Not Telling

Let’s look at an example of Tell Don’t Ask violation. Don’t pay much attention to the details of the code, it’s the Feature Envy in Client’s doSomethingElse method we’ll focus on.

In the Client class below there are two methods. The doSomething method makes use of all of Client’s fields. However the doSomethingElse method doesn’t use any, but it’s very interested in Collaborator’s state. This is where Client is asking and not telling. It’s asking Collaborator for state, not telling it to perform a behaviour on its behalf.

// all of collaborator's properties are publicly exposed
public class Collaborator {
    public int property1;
    public int property2;
    public int property3;
    
    public int doSomething() {
       return (property3 * 100) + (property1 - property2);
    }
}

public class Client {
    private final Collaborator collaborator;
    private int clientInternalDetail1;
    private int clientInternalDetail2;

    public Client(Collaborator collaborator) {
        this.collaborator = collaborator;
        clientInternalDetail1 = 10;
        clientInternalDetail2 = 20;
    }

    // this method makes use of client's internal state
    public int doSomething() {
        return clientInternalDetail1 + clientInternalDetail2;
    }

    // this method uses none of client's internal state,
    // but it is interested in all of collaborator's state.
    // client is doing a lot of "asking" when it could be "telling" instead.
    public int doSomethingElse() {
        return (collaborator.property1 * collaborator.property2)
                + collaborator.property3;
    }
}

To refactor this we extract the body of doSomethingElse to a new method called calculateSomething. The intention is to move this newly extracted method onto Collaborator.

public class Client {
    private final Collaborator collaborator;
    private int clientInternalDetail1;
    private int clientInternalDetail2;

    public Client(Collaborator collaborator) {
        this.collaborator = collaborator;
        clientInternalDetail1 = 10;
        clientInternalDetail2 = 20;
    }

    // this method makes use of client's internal state
    public int doSomething() {
        return clientInternalDetail1 + clientInternalDetail2;
    }

    // extract the behaviour that is doing the asking.
    // this will be moved onto collaborator
    public int doSomethingElse() {
        return calculateSomething();
    }

    // next we use move method and move the extracted method to collaborator
    private int calculateSomething() {
        return (collaborator.property1 * collaborator.property2)
                + collaborator.property3;
    }
}

Telling, Not Asking

The calculateSomething method is then moved onto Collaborator. The client now delegates the doSomethingElse method to Collaborator’s calculateSomething. By doing this Client no longer needs to ask Collaborator for its state in order to do the work. Instead, it asks Collaborator to do the work for it. Remember, “the object that has the data does the work“.

// now collaborator has 3 private fields and 2 public methods
public class Collaborator {
    private int property1;
    private int property2;
    private int property3;

    public int doSomething() {
       return (property3 * 100) + (property1 - property2);
    }

    public int calculateSomething() {
        return (property1 * property2) + property3;
    }
}

public class Client {
    private final Collaborator collaborator;
    private int clientInternalDetail1;
    private int clientInternalDetail2;

    public Client(Collaborator collaborator) {
        this.collaborator = collaborator;
        clientInternalDetail1 = 10;
        clientInternalDetail2 = 20;
    }

    // this method makes use of client's internal state
    public int doSomething() {
        return clientInternalDetail1 + clientInternalDetail2;
    }

    // now client delegates the calculation to collaborator.
    // it no longer knows about property1, 2 and 3 of collaborator.
    public int doSomethingElse() {
        return collaborator.calculateSomething();
    }
}

Changes to Collaborator’s Internal Design

Now Collaborator has changed. Two fields were removed and the calculateSomething method now delegates to another object called Thing. However, notice that Client hasn’t had to change as the public API of Collaborator hasn’t changed, only its internal implementation. In the first example, Client knew all about the internals of Collaborator. If things were still the same then these changes would have impacted it also.

public class Thing {
    private int property1;
    private int property2;

    public int someOtherCalculation() {
        return property1 + property2;
    }
}

// now collaborator has changed how it does its business
public class Collaborator {
    private Thing thing;
    private int property3;

    public Collaborator(Thing thing) {
        this.thing = thing;
    }

    public int doSomething() {
       return (property3 * 100) + thing.someOtherCalculation();
    }

    public int calculateSomething() {
        return thing.someOtherCalculation() + property3;
    }
}

public class Client {
    private final Collaborator collaborator;
    private int clientInternalDetail1;
    private int clientInternalDetail2;

    public Client(Collaborator collaborator) {
        this.collaborator = collaborator;
        clientInternalDetail1 = 10;
        clientInternalDetail2 = 20;
    }

    public int doSomething() {
        return clientInternalDetail1 + clientInternalDetail2;
    }

    // client knows nothing about the internal change to collaborator
    public int doSomethingElse() {
        return collaborator.calculateSomething();
    }
}

In conclusion, keep a lookout for Feature Envy. This is a sign that objects are asking and not telling. When you do need to ask, ask a question via a method and not for an object’s internals. Sometimes you may need to ask but use your better judgement, be immutable, pragmatic and not dogmatic.

Buy me a coffee Buy me a coffee

😍 ☕ Thank you! 👍

Share

Leave a Reply

Your email address will not be published. Required fields are marked *

Post comment