Three Wise Men on Tell, Don't Ask
Here we examine a hypothetical, but very possible example where Tell, Don't Ask and the SRP collide. See what the moral is when it comes to applying principles.
Join the DZone community and get the full member experience.
Join For FreeJohn, Doe, and Marcus are three good friends. They have over 20 years of experience Java/Java EE stack work at IBM, Cognizant, and TCS respectively. They have immense experience in design patterns and all the new technologies and are respected by their colleagues for their exceptional insight into the technology stack.
They are planning to go for a vacation during the upcoming weekend and want to enjoy their spare time with lots of burgers, whiskey, and cooking. John has a house in a nearby village, so they planned to drive there.
At last, the day came. They packed up their beer, whiskey, and burgers and headed toward John's house. They got there in the evening and prepared some snacks, then sat at a roundtable to enjoy their food and drinks.
Suddenly, the power goes out.
The room is so dark that no one can see anything. From outside, they can hear the call of crickets, and Marcus turns on a flashlight so they can see each other.
Doe breaks the silence by saying, “Well, this ambiance is perfect for a horror story. Can anyone share any real life experiences?”
Marcus replied dryly, “Umm no. We're all from the same town and busy with work. No horror stories, but I can tell you a Java story that frightens me to this day. "
John and Doe’s architect instincts flared with this proposal.
The Problem
"As an architect, when I design a solution for a problem, encapsulation always frightens me. What portion do we expose to our client program?
John and Doe nod their heads, and Marcus continues.
"There are lots of OOP principles that say how you can judiciously encapsulate your classes or APIs from the outside world."
He then goes into an exmple, the Tell, Don’t Ask principle. It says to always instruct objects what to do. Never query them for an internal state and make decisions based on that. You lose control over your objects.
Let's look at a simple example. Suppose I want to write a Parcel Delivery service, and there are two domain objects — Parcel and Customer. So, how should we design it?
If I write following code fragments...
package com.example.basic;
public class PercelDeliveryService {
public void deliverPercel(Long customerId){
Customer cust = customerDao.findById(customerId);
List<Percel> percelList = percelDao.findByCustomerId(customerId);
for(Percel percel : percelList){
System.out.println("Delivering percel to " + cust.getCustomerAddress());
//do all the stuff related to delivery
}
}
}
According to Tell, Don’t Ask, that is a violation and should be avoided. In ParcelDeliveryService, I try to fetch or query for the CustomerAddress so I can perform the delivery operation. So, here, I query the internal state of the Customer.
Why Is That Dangerous?
Say, later, the delivery functionality changes. It says now to also include an email address or mobile number, so I have to expose these details. Now I'm exposing more and more of the internal state. Think about the other services. They may also use the same email address or customer address. Now, if I want to change the Customer Address return type (String) to an Address object, then I need to change all the services where it has been used.
That's a gigantic task to perform and increases the risk factor of breaking the functionality. Another point is, as the internal state is exposed to more services, there is always a risk of polluting the internal state with a service, and it is hard to detect which service changed the state.
In short, I lose control over my object, as I don’t know which services use/modify my object's internal state. Now, if my object is used by the internal services of a monolith application, I can search the usage in my IDE and refactor them. But If the Object is exposed through an API, and this API is used by other organizations, then I am history. It really hurts our company's reputation and, as an architect, I would be fired.
So, I can say:
Action: Expose more of the internal state
Outcome: Increase coupling, increase risk, and increase rigidity
Now, again, look at the solution I provided as we go back to the story...
Doe chuckles and guesses the point Marcus is trying to make. He interrupts him, saying, "So, Marcus, you want to see if we follow the Tell, Don’t Ask principle. There are a couple of ways we can refactor the problem."
Solution 1: Association
Make an association between Customer and Parcel. It would be lazily loaded, and the delivery method should be in the Customer object, so, from the service, we call for a delivery. Then, the delivery method fetches the parcel list and delivers it. If the internal return type changes from String to Address, only the method should be affected.
Like this:
package com.example.basic;
public class ParcelDeliveryService {
public void deliverParcel(Long customerId){
Customer cust = customerDao.findById(customerId);
cust.deliver();
}
}
Public class Customer{
public void deliver(){
List<Percel> percelList = getPercelList();
for(Percel percel : percelList){
System.out.println("Delivering percel to " + this.getCustomerAddress());
//do all the stuff for delivery
}
}
}
By doing this, we can maintain Tell, Don’t Ask properly and decrease the risk of exposing the internal state. Also, we're free to modify my class attributes, as all behaviors are tied locally with attributes — a nice way to achieve encapsulation.
Solution 2: Command Objects
We can create a command object, where we pass the Parcel details and the command object to the Customer model. The delivery method extracts the Parcel list and delivers it to the respective customer.
But both policies break another principle — the Single Responsibility Principle. The SRP says A class should have only one reason to change.
But If I think in a reverse way, why do we write services? Because each service does one job — like the Person Delivery service is responsible for parcel-related delivery operations, so it maintains the SRP, and this service only changes if there are any changes in the parcel delivery mechanism. If it breaks, other services will not be affected unless other services depend on it.
But according to Tell, Don’t Ask, all Customer-related behaviors should be moved into the Customer class so we can tell/instruct/command the Customer class to do a task. So, now, the Customer class has more responsibility because all Customer-related service code now goes into the Customer model. So, Customer has more reason to change, increasing the risk factor of failing.
Now we are back to the same problem: risk factor.
If you're exposing internal state, then when modifying attributes, if you move all behaviors into a class, the risk of modifying functionality breaking the system increases.
So, the SRP and Tell, Don’t Ask contradict in this context.
John nods his head and starts, "Yes, that's a problem. But not only that, what if we want to implement a cache in a service? Or what if we want to implement an aggregation function — like parcel delivery charge depending on distance or account type. To find the most parcels sent to a locality, we use an aggregator service, where we ask for the internal state and compute the result."
With that in mind, we often we break the Tell, Don’t Ask principle. Even, as per the current trend, the Model objects should be lightweight and should not be coupled with each other. If the business needs information that distributes over multiple models, we can write an aggregator service and query each model and compute the result, so we're querying internal state. Think about Spring Data.
Now, if we look at it in another perspective, according to Domain-Driven Design, in a Parcel Delivery context (bounded context), is Customer responsible for delivering the parcel?
Absolutely not.
In that context, the delivery boy is responsible for delivering the parcel to the customer. For that, the delivery boy needs the Parcel and Customer models, and in that context, only the Customer's name and address details are required. And for the parcel ID, the parcel name will be required. So, as per DDD, we create an aggregate model, DeliveryBoy, where we have two slick models, Customer and Parcel.
In this context, we don’t need Customer. Context-wise, the model has changed, so there's no one big model for Customer, where all attributes and behavior resides. Rather, we use two small, slick models based on bounded contexts and an aggregate model querying these slick models to perform the work.
By doing this, we can mix and match SRP and Tell, Don’t Ask. From a service perspective, we only tell/command the DeliveryBoy aggregate model to do something, so the service maintains both the SRP and Tell, Don’t Ask. Meanwhile, our aggregate model also maintains SRP, querying the Customer and Parcel models to do the operation.
Like:
package com.example.basic;
public class ParcelDeliveryService {
public void deliverParcel(Long customerId){
DeliveryBoy boy = new DeliveryBoy();
boy.deliver(customerId);
}
}
class DeliveryBoy {
Customer cust;// Context driven model
Percel percel;
public void deliver(Long id){
//do stuff
//load customer slick model
//load Percel slick model
//Deliver the same by quering those models
}
}
Marcus joins in and says that in the context of DDD and aggregator services, microservices often break the SRP. Doe joins in and says, "So, there is no silver bullet and not all principles are good in all contexts. Based on the context, you have to judge which principles you follow. Sometimes, you need to compromise."
It might be that, for a specific principle, your code is bad but for a given context, it is optimum.
Principles are generic and they are context-free. But real-life solutions are based on context, so fit principles based on the context, not the reverse.
In the meantime, the lights come on! In a flash, all three start in on their whiskey and food.
Conclusion
As a narrator, my question to all viewers is, what do you think about the talk they had? Are there any points they left off that need attention while designing?
Published at DZone with permission of Shamik Mitra, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments