Unit 6: Tell, Don't Ask
Learning Objectives
After this unit, students should be able to:
- explain the role of accessors and mutators, and why they are not always desirable in object-oriented design
- identify how accessors and mutators can increase coupling and leak implementation details
- apply the Tell, Don’t Ask principle to redesign client–class interactions
- refactor client code that relies on getters into object-oriented method calls
- reason about encapsulation trade-offs when deciding whether to expose object state
Overview
In earlier units, we learned how to define classes with private fields and public methods, and how encapsulation helps protect an object’s internal state.
In this unit, we examine a common but subtle design mistake: exposing an object’s internal state through accessors and mutators, and pushing logic into client code. Although getters and setters may seem harmless—or even “good practice”—they can quietly weaken encapsulation and increase coupling.
We introduce the Tell, Don’t Ask principle, which encourages clients to tell objects what to do, rather than asking for their internal data and operating on it externally. This shift helps us design objects that are more robust, flexible, and easier to change.
Accessors and Mutators
Similar to providing constructors, a class can also provide methods to retrieve or modify the properties of the object. These methods are called the accessor (or getter) or mutator (or setter). While accessors and mutators are common, using them indiscriminately can undermine encapsulation and lead to fragile designs.
The example below shows a Circle class with accessor and mutator methods for its fields.
| Circle v0.4 | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | |
In the code above, we can categorize the accessor and mutator of each field as follows.
| Fields | Accessors | Mutators |
|---|---|---|
x |
getX |
setX |
y |
getY |
setY |
r |
getR |
setR |
Do note that using the prefix get or set for accessor and mutator is optional, although it is a good practice. For instance, we could name a mutator that sets the radius as resize instead of setRadius.
Accessor/Mutator vs Public
Instead of providing an accessor method and a mutator method for every private field, why don't we just declare the fields as public and access them directly?
Having both an accessor and a mutator for a private field is still better than setting the field public. By having an accessor and a mutator, we are adding a layer of abstraction. For instance, we can still rename a field without affecting the client.
Another advantage is that we may be able to perform some checks on the mutator and prevent certain invalid values from ever being assigned to the field. Consider the method setR in our Circle v0.4 above. A slightly better approach is to implement it with a check to prevent setting the radius to a non-positive value.
1 2 3 4 5 6 7 | |
How Accessor/Mutator Can Be Harmful
Since having accessors and mutators is better than having public fields, does it mean that we should always provide an accessor and a mutator for every private field?
Let's consider a slightly different version of Circle where the center coordinate and radius are integers, perhaps because the implementer does not have the foresight that they need to be floating-point numbers.
| Circle v0.4a with Integer Coordinates | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | |
Suppose a client of the class wishes to check if, given a point (x,y), does it lie within the circle c? One approach would be:
| Client Code v1 (Ask) | |
|---|---|
1 2 3 4 | |
One day, the implementer realized that the coordinates and radius should be floating-point numbers and changed the fields to double, and revised the Circle implementation to v0.4 shown earlier. The client code above will break! Since the accessors now return double values, but the client assigned the returned values to int.
Thus, having accessors and mutators in a class can inevitably leak some information about the class's internal representation to the client. This is a form of coupling between the client and the class. In the example above, the client code depends on the fact that the center coordinates and radius are represented as int. Any change to the internal structure will break the client code.
Thus, we should think carefully if an accessor or a mutator is really needed for a field, i.e., does the client really need to access or modify something that is internal to the class?
The "Tell, Don't Ask" Principle
One guiding principle to whether the implementer should provide and whether the client should call the accessor and mutator is the "Tell, Don't Ask" principle. This principle suggests that we should tell an object what to do, instead of asking an object for its state and then performing the task on its behalf.
Let's revisit the example above. This time, the (disgruntled) client has updated the code to use floating-point coordinates and radius.
| Client Code v2 (Ask) | |
|---|---|
1 2 3 4 | |
Here the client calls the accessor methods to ask for the values of the fields x, y, and r of the Circle object c, and then performs the computation to check if the point (x,y) is within the circle.
sequenceDiagram
participant c as Circle
participant client
c->>client: getX()
c->>client: getY()
c->>client: getR()
client->>client: perform computation
Applying the "Tell Don't Ask" principle, a better approach would be to add a new boolean method in the Circle class,
1 2 3 | |
and let the client tell the Circle object to check if the point is within the circle.
| Client Code v3 (Tell) | |
|---|---|
1 | |
sequenceDiagram
participant c as Circle
participant client
client->>c: contains(x, y)
c->>client: answer
This better approach involves writing a few more lines of code to implement the method, but it keeps the encapsulation intact, leading to less coupling between the client and the class. The client does not need to know the internal representation of the Circle class, and the Circle class can change its internal structure (e.g., the type of the fields) without affecting the client.
In general, a task that is performed only on the fields of a class should be implemented in the class itself. As a rule of thumb, if client code calls two or more accessors on the same object and combines those values to compute a result, then that computation likely belongs inside the object.
While there are situations where we cannot avoid using an accessor or a mutator in a class, for beginner OO programmers like yourselves, relying on accessors and mutators as defaults indiscrminately can hinder the development of good OO design instinct. Using accessors and mutators heavily often turns objects into passive data holders, shifting logic into client code. This style resembles procedural programming more than object-oriented design. As such, you are encouraged to avoid defining accessors and modifiers to private fields, and instead focus on designing methods within the class that tell an object what task to perform, and allowing clients to simply request those tasks.
Further Reading
- Tell Don't Ask by Martin Fowler
- Why getters and setters are evil by Allen Holub
- Getters and setters are evil. Period, by Yegor Bygayenko.