What Is The Tell Don’t Ask Principle?
One of the most basic concepts of OOP is encapsulation, which allows us to create modules that contain both data and the behavior that operates on that data. The tell don’t ask principle emphasizes the importance of using the methods defined in a class to operate on its data, instead of directly checking the values of the data within the class. In other words, we should maximize the use of a module’s behaviors in our code and minimize the use of the module’s internal state to make decisions about code execution.
The tell don’t ask principle can be violated in two ways:
We ask for the internal state of an object and, depending on that state, call a behavior on that object.
We ask for the internal state of an object and, depending on that state, call a behavior on another object.
While limiting the questions asked (checking the data) of an object is desirable, the first way of violating the tell don’t ask principle almost always leads to more complicated code and should be fixed. The second way can sometimes lead to more complicated code by not following the principle, and other times, it can lead to more complicated code and more dependencies between our classes by following it. As with all principles, the programmer must make a judgment call on whether its application will have a positive effect or not.
Let’s explore the two different ways this principle can be applied.
Asking About The Internal State Of The Object
The first way this principle can be violated is by asking an object about its internal state and, depending on the result, either executing a behavior if the result is within a certain range or executing different behaviors of the same object for different values of the result.
These two cases are typically represented by an if statement for the first scenario and an if-else statement for the second. Let’s start with an example by refactoring an if statement that violates the tell don’t ask principle.
Replacing An If Statement
Let’s look at the following class:
public class Person(string name, int age)
{
private string Name { get; } = name;
public int Age
{
get => age;
set => age = value;
}
public void DrinkWine(int numberOfGlasses)
{
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of wine at age {Age}");
}
}
In our program we want to create a loop the simulates that a year has passed for a person and if that person is over 18 years old, then he is allowed to DrinkWine
.
Person mike = new("Mike", 15);
for (int age = mike.Age; age < 22; age++)
{
mike.Age++;
Console.WriteLine($"Happy birthday Mike! You are {mike.Age} years old!");
if (mike.Age >= 18)
mike.DrinkWine(2);
}
This is a violation of the tell don’t ask principle. We are asking about the internal state of our object (Mike’s age) and based on that result, we may execute the DrinkWine
behavior. Although we have exposed the Age
property not just for the sake of the check but also to increase it with every iteration representing a passing year, why is this bad?
There are two reasons: First, in this example, we have only one if statement checking the age, but in our code, we might now or in the future need to call the DrinkWine
behavior in different places. If the requirements change and the drinking age becomes 21 because Mike is now in another country, we have to find and update all these checks.
Someone might suggest a naive solution that still violates the tell don’t ask principle but addresses the problem of changing requirements. We could have a static field in the Person
class that represents the drinking age, like this:
public static int DrinkingAge => 18;
This means that all our if statements can now be:
if (mike.Age >= Person.DrinkingAge)
mike.DrinkWine(2);
So any change in the requirements now only has to happen in one place, but there is another problem here. Not only do we now have two violations of the tell don’t ask principle, because we are effectively asking two questions (Mike’s Age
and the Person’s DrinkingAge
), but we also have a case of temporal coupling. Every time someone wants to use the DrinkWine
method, will have to remember to perform the necessary checks before using it.
If we apply the tell don’t ask principle, both these problems are resolved. The DrinkingAge
will exist in the Person class, where it is relevant, and any checks will also be inside the relevant method:
public class Person(string name, int age)
{
private static int DrinkingAge => 18;
private string Name { get; } = name;
public int Age
{
get => age;
set => age = value;
}
public void DrinkWine(int numberOfGlasses)
{
if(Age >= DrinkingAge)
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of wine at age {Age}");
}
}
Now any requirement change in the drinking age only has to happen in one place and any time anyone wants to call the DrinkWine
method, can do it without the need to remember to check anything:
for (int age = mike.Age; age < 22; age++)
{
mike.Age++;
Console.WriteLine($"Happy birthday Mike! You are {mike.Age} years old!");
mike.DrinkWine(2);
}
Replacing An If-Else Statement
This is good for an if statement that acts as a guard, but what if we need to execute different behaviors based on the object’s state. For example let’s suppose that the Person
class has two methods: DrinkWine
and DrinkOrangeJuice
and depending on the age a different one is called each time:
public class Person(string name, int age)
{
private string Name { get; } = name;
public int Age
{
get => age;
set => age = value;
}
public void DrinkWine(int numberOfGlasses)
{
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of wine at age {Age}");
}
public void DrinkOrangeJuice(int numberOfGlasses)
{
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of orange juice at age {Age}");
}
}
And
for (int age = mike.Age; age < 22; age++)
{
mike.Age++;
Console.WriteLine($"Happy birthday Mike! You are {mike.Age} years old!");
if(mike.Age >= 18)
mike.DrinkWine(2);
else
mike.DrinkOrangeJuice(2);
}
We have different options here depending on our use case:
The first is to encapsulate the two behaviors by making them private and creating a public behavior that represents our desired action:
public class Person(string name, int age)
{
private static int DrinkingAge => 18;
private string Name { get; } = name;
public int Age
{
get => age;
set => age = value;
}
public void DrinkBeverage(int numberOfGlasses)
{
if(Age >= DrinkingAge)
DrinkWine(numberOfGlasses);
else
DrinkOrangeJuice(numberOfGlasses);
}
private void DrinkWine(int numberOfGlasses)
{
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of wine at age {Age}");
}
private void DrinkOrangeJuice(int numberOfGlasses)
{
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of orange juice at age {Age}");
}
}
So that we can use:
for (int age = mike.Age; age < 22; age++)
{
mike.Age++;
Console.WriteLine($"Happy birthday Mike! You are {mike.Age} years old!");
mike.DrinkBeverage(2);
}
If we can’t do that, because there is a need for one or both behaviors to be public then we must also add guards inside those behaviors:
public void DrinkBeverage(int numberOfGlasses)
{
if(Age >= DrinkingAge)
DrinkWine(numberOfGlasses);
else
DrinkOrangeJuice(numberOfGlasses);
}
public void DrinkWine(int numberOfGlasses)
{
if(Age >= DrinkingAge)
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of wine at age {Age}");
}
public void DrinkOrangeJuice(int numberOfGlasses)
{
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of orange juice at age {Age}");
}
If there is still a problem, for example, we may need to call the two behaviors in our code based on external data values other than age, then we should create a method that removes the duplicate check and make it public, so it can be called by the users of our class:
public class Person(string name, int age)
{
private static int DrinkingAge => 18;
private string Name { get; } = name;
public int Age
{
get => age;
set => age = value;
}
public void DrinkBeverage(int numberOfGlasses)
{
if(IsInLegalDrinkingAge())
DrinkWine(numberOfGlasses);
else
DrinkOrangeJuice(numberOfGlasses);
}
public void DrinkWine(int numberOfGlasses)
{
if(IsInLegalDrinkingAge())
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of wine at age {Age}");
}
public void DrinkOrangeJuice(int numberOfGlasses)
{
Console.WriteLine($" {Name} drinks {numberOfGlasses} glasses of orange juice at age {Age}");
}
public bool IsInLegalDrinkingAge() => Age >= DrinkingAge;
}
In this case, IsInLegalDrinkingAge()
could be a property, but I made it a method because, in real code, we may have more complicated checks.
Finally, depending on our needs, the DrinkWine
method can be turned into a TryDrinkWine
method that returns false
if the guard fails. This way, users can determine if the method call succeeded instead of having it fail silently.
Asking About Another Object
The Tell Don’t Ask principle can also apply when we ask about the state of one object to call a behavior on another object. This situation requires judgment from the programmer about how and if it can benefit the codebase, as it can create more dependencies between types.
Let’s suppose we have three classes: Player, a collection of players (this could be a custom class or a BCL class like List), and Enemy. The Enemy class has an Attack method that takes a Player object as an argument.
Now, let’s assume we want our enemy to attack the player closest to a point in our level, such as the center.
One way to achieve this is to create a class with a method that calculates the closest player to the center by taking the collection of players as an argument and then passing that player as a parameter to the Attack method of the Enemy. This way, we ask one class to find the nearest player and then tell the Enemy class to execute a behavior based on the answer.
Another approach is to have a method in our player collection class that returns the nearest player to a point. We pass the center of our level as a point, and the result is then passed to the Enemy’s Attack method. Here, the responsibility for the calculation lies within the player collection class. We still ask that class for a result and use that result to execute a behavior of another class.
Finally, we can omit asking who is the nearest player to the center and create a new method in the Enemy class that takes a collection of players and a point in our level as arguments. This method calculates the nearest player to that point and internally calls the Attack method with that player as a parameter. In this case, we have followed the Tell Don’t Ask principle.
Which method is correct depends on the context. Following the Tell Don’t Ask principle doesn’t always mean it’s the best approach, as it may violate other principles, create more dependencies, and complicate our code.
Some factors that may influence our decision include:
Do we need to calculate distances for other elements besides players? If so, a dedicated class for distance calculations might be more appropriate.
Do other classes need to know the distance information about the players? If yes, a method in the Player collection class might be suitable.
Is this functionality only used by the Enemy class? If so, it might be the responsibility of the Enemy class.
When we need to ask about the internal state of another object to decide our code’s behavior, the Tell Don’t Ask principle may not always be appropriate. If the answer is needed by multiple objects, it might be best to encapsulate the logic in a method and pass the result as a parameter without overcomplicating dependencies. Sometimes, the simplest solution is to ask for a result and then tell the code to perform an action based on that result.
Conclusion
The Tell Don’t Ask principle, like any other principle, is not an absolute rule. The importance of principles lies not in the principles themselves but in the problems they aim to solve. It’s crucial to understand the problem a principle addresses so that we can identify these issues in our code and make informed decisions about whether this problem might have future consequences.
When making decisions, we need to balance the benefits of applying a principle against its potential drawbacks and the benefits of adhering to one principle against the negatives of violating another, as principles can sometimes conflict.
For example, applying the Tell Don’t Ask principle can sometimes create unnecessary dependencies, violate The Single Responsibility Principle, lead to methods that do too many things, create god classes with numerous methods, or result in many small methods that merely call other methods and contain only an if statement as a guard. Finding the right balance, as with any architectural decision, is the challenging part.
Thank you for reading, and as always, if you have any questions or comments you can use the comments section, or contact me directly via the contact form or by email. Also, if you don’t want to miss any of the new blog posts, you can subscribe to my newsletter or the RSS feed.