Modifying data in a class where they reside is always a good thing. The Tell Don’t Ask principle guides us on how to follow such an approach. Unfortunately, it is widespread to obtain data from object and process it inside some service or manager class. After this article, you will know how to talk with objects and process data inside them instead of pulling their logic out of objects. You will be able to test your new skill with kata exercise.
Benefits of Tell Don’t Ask principle
At the first glance, obeying the Tell Don’t Ask principle might look like a cosmetic change for the code. In fact, it provides a lot more than just a simpler, less coupled clean code. By following this principle, you will follow OOP with ease by:
- The localisation of information — everything related to the object will be kept in one place — its data, ways of modifying it, its flow and ways of processing it
- Limited code duplication, as operations with objects data will be kept in one place. You will not have to keep an eye on DRY principle, as it will be implemented naturally
- Strong object-oriented approach, with a focus on abstraction
- Simplified unit tests. Instead of preparing heavy service classes to test data processing, most of your code could be tested with lightweight unit tests
A little bit of theory
The Tell Don’t Ask is about keeping the data where it belongs – encapsulated inside the object. Encapsulation should prevent us from modifying object internal state without it knowing that. Modifying the object state with setter does not guarantee that, as such the object lacks abstraction. Service classes digging in objects internals are like trying to eat by putting food directly to your stomach, instead of putting the food into your mount.
Sample application used here will simulate a small farm, that has only a few responsibilities. The first task that it will fulfil is to water plants when they need that. The second will be gathering products from the plant, when they are ready, and the last one is to deliver packed products to the customer.
With such an approach, we do not give objects any privacy. Tearing out their internals violates OOP and complicates the service logic. Let’s think how we can refactor it to obey Tell Don’t Ask principle and achieve clean code. Try to find the logic that has been taken out of classes. Spot for the places, where some decision is made. Found it? The first step would be to tell the
Plant to water it using some
WaterValve, instead of service doing that.
By telling the
Plant to water it with some
WaterValve, we encapsulated its logic inside the class. We do not need to make condition outside of the
Plant class, which simplifies service testing by reducing the number of interactions between objects and possible execution paths. Let’s take a look into the practice.
Modeling it in code
Let’s look at the implementation that reflects the first diagram. It is digging deeply into objects internal, fetching plants from
RaisedBed and getting internal properties from those plants. If that wasn’t enough, it is implementing plant-related logic, asking the plant for its values and setting new ones, depending on the
Plant state. Service has huge knowledge about other object internals.
Sample service implementation that does not follow the Tell Don’t Ask principle.
Apart from asking for internal values, it also differentiates the logic depending on the type. Think a little bit about that, how you are going to test this solution? Are you going to check logic for every type of plant? Will you make different tests depending on plant soil moisture? What if there will be no raised bed or valve? Testing every possible path will require a lot of tests. Or, maybe you will decide risk bugs and not test everything?
Getting hands dirty
Consider the same service, but respecting the Tell Don’t Ask principle. Let’s literally tell the raised bed to be watered with some valve, and let it delegate this work to its plants.
Watering service after we applied the Tell Don’t Ask principle into it.
When we applied to Tell Don’t Ask principle, this service simply delegates the request to it. Its only responsibility is to orchestrate invocation and manage the repositories. Testing it will only limit to the integration with repositories, while logic that used to be in it can be checked easily with unit tests. You might struggle, where the logic is now? Take look on a part of refactored
Again, using Tell Don’t Ask, we are not using the internals of
Plant in here – we are only managing specific plant watering, but we let the plant to decide if it needs one. Again, there are not many cases to test in here, so writing unit test should be easy. Next step with this refactoring will be to include the watering logic inside the plant itself. You can consider introducing a Strategy pattern to simplify logic even more.
Dealing with dependencies
Sometimes, to perform an action, you will need to call another service. In our application, this is a situation when we sale products. Plants after gathering are organized into crates. Crates can be packed for the customer, and such a package can be sent to it. However, sending a package is managed by another service.
Service with dependent service usage without refactoring.
There are two most common approaches to refactor that into code obeying Tell Don’t Ask principle.
Service orchestrating invocation
First discussed approach relays on a service (
Sales in this case), that has the knowledge about that when the
Package should be sent.
Sales service is an orchestrator for the process. In this case, we simply pass the package to delivery service so it will be processed by it.
Service orchestrating invocation of another service.
For most people, it is natural to code this way, so the benefit of this approach is the ease of understanding this solution. On the other hand, it creates a little bit of coupling between
Sales service and a
Package class – Sales does need to know when packaging was successful, and when it should be delivered.
Disconnected Domain Model variation
Disconnected Domain Model is was described in Vernon’s “Implementing Domain-Driven Design” book as a technique of injecting repositories into aggregates. Injecting repositories in every aggregate by constructor may cause problems with dependency injection mechanisms due to a large number of aggregates that should be fed with repository dependency. Instead of that, we might try to pass this dependency directly that need to use some service.
DeliveryService to work.
This technique limits the knowledge of
Sales service about
Package class implementation, so it simplifies testing of a service. It keeps the whole logic related to
Package inside class. On the other hand, it usually brings confusion to the developer, who is unfamiliar with it. While using this technique, carefully select services that are injected. Using those services only for invoking processing is usually not a threat, but using a value returned from injected service might lead to Flag Argument antipattern.
The Tell Don’t Ask principle promotes keeping logic inside object instead of service. This increases their cohesion, making the code more maintainable and easier to test. Extracting the logic into service makes them unnecessarily big and complex. Try to speak with your objects, and tell them what to do.
If you feel, like you need some practice in adapting code according to Tell Don’t Ask principle, challenge yourself to do related Code Kata. You will find the description in the Readme file. Try to write as much as you can by yourself. There are some unit tests already written, but you will need much more of those. Sample solution can be found on this branch, but remember, that it is just a sample, and there are multiple ways of doing it.