TDD and legacy code: moving behavior back to where it belongs

Anemic domain models and the tell-don’t-ask principle

Zeger Hendrikse
NS-Techblog
5 min readJan 29, 2024

--

Introduction

In the world of software development, quite a few anti-patterns are observed so ubiquitously, that they often aren’t identified as such anymore!

Some well-known examples are:

In this post, we discuss the latter by analyzing and solving the tell-don’t-ask kata, an exercise in improving a poorly implemented domain model by applying the tell-don’t-ask principle in small steps.

The tell-don’t-ask principle will be applied to move away from an anemic domain model.

Taking small steps is important because dealing with legacy code and improving it in small steps while keeping tests green, is an essential skill that all TDD practitioners should be familiar with.

Anemic domain models and tell-don’t-ask

The notion of an anemic domain model was first introduced by Martin Fowler:

The basic symptom of an Anemic Domain Model is that at first blush it looks like the real thing. There are objects, many named after the nouns in the domain space, and these objects are connected with the rich relationships and structure that true domain models have. The catch comes when you look at the behavior, and you realize that there is hardly any behavior on these objects, making them little more than bags of getters and setters. — Martin Fowler

Since there is no behavior in the domain objects, we need access to almost all of their fields everywhere (using the getters and setters) to do something useful.

This violates all criteria for high-quality software. High-quality software

  • Is modular.
  • Is loosely-coupled.
  • Has high cohesion.
  • Has a good separation of concerns.
  • Exhibits information hiding, i.e. encapsulates its state.

The tell-don’t-ask principle reminds us to tell objects what to do instead of asking for their state. The state within an object should remain encapsulated as much as possible, and should only be manipulated by the methods, that are a reflection of the object’s behavior.

Information hiding (data encapsulation) is one of the hallmarks of proper object-oriented programming. Unfortunately, nowadays many IDEs and frameworks can often generate getters and setters for all data fields.

By applying the tell don’t ask principle to anemic domain models, we move behavior back into the domain objects, thereby making the setters redundant. Moreover, in this way, we restore data encapsulation, one of the fundamental hallmarks of true object-oriented programming.

The tell-don’t-ask kata

The kata starts with a legacy code base, that implements a simple order flow application. It’s able to create orders, do some calculations (totals and taxes), and manage them (approval/rejection and shipment ).

At first glance, the code may give the impression of a thorough DDD design as we see use cases, a domain model, repositories, and such. However, upon closer scrutiny, it turns out that we are dealing with a so-called anemic domain model (see the code in the domain package): all domain objects are anemic and consist of getters and setters only. On the plus side, there are tests for us!

Introduction of value objects

Let’s start by introducing value objects where we can, as they offer complete immutability by definition. The Python dataclass construct is very well suited to implementing such value objects, as it allows us to “freeze” the data fields, i.e. to enforce immutability.

Let’s start with the OrderItem object. Next, we meed to modify the OrderCreationUseCase class and its tests accordingly.

Implementation of an OrderItem value object using the Python data class construct.

Meanwhile, we try to keep our tests green as much as possible, by keeping our changes as small as possible. We recommend that you use pytest-watch to continuously keep an eye on the status of your tests. The ultimate challenge is to change just one or two lines of code, and the tests should be green again!

Next, we repeat the same process for the Category and Product classes mutatis mutandum.

Introduction of entity objects

Turning our attention to the Order class, we notice that it has an ID field, so it must be an entity object. In fact, it turns out that orders are retrieved from and stored by repositories.

So let’s apply the tell-don’t-ask principle here by moving the logic from the OrderApprovalUseCase to the Order domain object:

In this snippet, we have already added a constructor to the class. You might want to move the duplicate lines 13, 14, and 22, 23 into a separate function, but we decided not to do that here.

This way, instead of asking Order for its OrderStatus, we tell it to change its OrderStatus depending on whether the order is approved or rejected. As a consequence, OrderApprovalUseCase is simplified to

The code expresses its intent much more clearly after we have moved the behavior to the location where it naturally belongs: the domain.

When we try to remove the setter for the order status field in the Order object, we find that the test cases are still using this setter to create an order for testing. This is easily to fix: the creation status is already set in the constructor, so we can remove those lines, and we can set the status field to either approved or rejected by calling our newly created methods!

Finally, in the OrderShipmentUseCase the setter for the order status field is used when an order is shipped. Again we use the tell-don’t-ask principle, this time by adding a ship() method to the order class: we tell the order to ship, we don’t ask for its shipment status. In this kata, there seems to be no logic involved when shipping an order, so the ship() implementation contains just one statement: status = OrderStatus.SHIPPED.

Now the setter for the order status field in the Order object be can safely removed. This means that only the object itself can change its status, and limits both the time and place in the code execution where and when the order status can be changed!

How to continue from here

Using the same principle and techniques as above, we can continue to work our way through the kata. For example, the calculation of the various taxes still needs to be brought into the domain.

A full description would make this post far too long, but the interested reader can either try it themselves and/or look at the hints in the readme file.

Conclusion

In this article, we have discussed what an anemic domain model is, and what makes it such an ugly beast. We have also explained how to identify such an anemic model. Finally, we have shown how we can gradually enrich such a domain model again with business behavior, by applying the tell-don’t-ask principle.

Last but not least, we can have shown how to move behavior back to the domain model in very small steps, i.e. on a field-by-field basis. This allows developers to apply the boy scout rule, and improve the domain model step by step.

--

--