1. Overview
In this article, we’ll explore the differences between anemic and rich domain models. We’ll begin by defining what a rich object is and contrasting it with anemic objects. From there, we’ll examine a practical code example and progressively enhance its design by encapsulating data and establishing a robust API for our domain model.
2. Anemic vs. Rich Objects
Let’s start by understanding what rich and anemic objects are. On his personal blog, Robert C. Martin, the author of Clean Code, discusses the concept of anemic objects, referring to them as “data structures”. He highlights the fundamental difference between data structures and objects by stating: “Classes make functions visible while keeping data implied. Data structures make data visible while keeping functions implied”.
Simply put, rich objects conceal their underlying data and only expose a set of public methods to interact with it. In contrast, anemic objects and data structures reveal their data and rely on external components for operations.
2.1. Rich Objects
In an OOP context, an object is a set of functions that operate on encapsulated data. A common mistake is to perceive an object as a mere collection of elements and breach its encapsulation by directly manipulating its fields to meet business needs.
In order to understand the domain deeper and construct a rich domain model, we should encapsulate the data. As a result, we’ll treat the objects as autonomous entities, focusing on their public interface to fulfill the business use cases.
2.2. Anemic Objects
In contrast, an anemic object only exposes a set of data elements that are intended to be manipulated by implicit functions. For instance, we can think of a DTO (Data Transfer Object): it exposes its fields through getters and setters, but it does not know to perform any operation on them.
For the code examples in this article, let’s assume we’re developing an application for simulating a tennis game. Let’s see what an anemic domain model for this application can look like:
public class Player {
private String name;
private int points;
// constructor, getters and setters
}
As we can see, the Player class does not provide any method helpful method, and it exposes all its fields through getters and setters. As the article progresses, we’ll gradually enrich our domain model, encapsulating its data.
3. Encapsulation
The lack of encapsulation is one of the main symptoms of anemic models. Suppose the data is exposed through getters and setters. In that case, we’re at risk of spreading the logic related to our model all over the application and potentially duplicating it in different domain services.
Therefore, the first step for enriching the Player model will be to question its getters and setters. Let’s look at some simple usages of the Player class and understand how this data is used for a game of tennis:
public class TennisGame {
private Player server;
private Player receiver;
public TennisGame(String serverName, String receiverName) {
this.server = new Player(serverName, 0);
this.receiver = new Player(receiverName, 0);
}
public void wonPoint(String playerName) {
if(server.getName().equals(playerName)) {
server.setPoints(server.getPoints() + 1)
} else {
receiver.setPoints(receiver.getPoints() + 1);
}
}
public String getScore() {
// some logic that uses the private methods below
}
private boolean isScoreEqual() {
return server.getPoints() == receiver.getPoints();
}
private boolean isGameFinished() {
return leadingPlayer().getPoints() > Score.FORTY.points
&& Math.abs(server.getPoints() - receiver.getPoints()) >= 2;
}
private Player leadingPlayer() {
if (server.getPoints() - receiver.getPoints() > 0) {
return server;
}
return receiver;
}
public enum Score {
LOVE(0, "Love"),
FIFTEEN(1, "Fifteen"),
THIRTY(2, "Thirty"),
FORTY(3, "Forty");
private final int points;
private final String label;
// constructor
}
}
3.1. Questioning the Setters
To begin with, let’s consider the setter methods in our code. Currently, the player names are passed as constructor arguments and never changed afterward. As a result, we can safely make them immutable by removing the corresponding setter methods.
Next, we observe that the players can only gain one point at a time. Thus, we can replace the existing setter methods with a more specialized wonPoint() method that increments the player’s score by one:
public class Player {
private final String name;
private int points;
public Player(String name) {
this.name = name;
this.points = 0;
}
public void wonPoint() {
this.points++;
}
// getters
}
3.2. Questioning the Getters
The getter for the number of points is used several times to compare the score difference between the two players. Let’s introduce a method that returns the point difference between the current player and his opponent:
public int pointsDifference(Player opponent) {
return this.points - opponent.points;
}
In order to check if a player is in “advantage” or if he won the game, we’ll need an additional method for checking if a player’s score is bigger than a given value:
public boolean hasScoreBiggerThan(Score score) {
return this.points > score.points();
}
Now, let’s remove the getter and use the enriched interface on the Player object instead:
private boolean isScoreEqual() {
return server.pointsDifference(receiver) == 0;
}
private Player leadingPlayer() {
if (server.pointsDifference(receiver) > 0) {
return server;
}
return receiver;
}
private boolean isGameFinished() {
return leadingPlayer().hasScoreBiggerThan(Score.FORTY)
&& Math.abs(server.pointsDifference(receiver)) >= 2;
}
private boolean isAdvantage() {
return leadingPlayer().hasScoreBiggerThan(Score.FORTY)
&& Math.abs(server.pointsDifference(receiver)) == 1;
}
4. Low Coupling
A rich domain model leads to a design with low coupling. By removing the getPoints() and setPoints() methods and enhancing the object’s API, we successfully concealed the implementation details. Let’s take another look at the Player class:
public class Player {
private final String name;
private int points;
public Player(String name) {
this.name = name;
this.points = 0;
}
public void gainPoint() {
points++;
}
public boolean hasScoreBiggerThan(Score score) {
return this.points > score.points();
}
public int pointsDifference(Player other) {
return points - other.points;
}
public String name() {
return name;
}
public String score() {
return Score.from(points).label();
}
}
As we can see, we can change the internal data structure with ease. For example, we can create a new class to store a player’s score instead of relying on the int primitive without impacting any of the clients that use the Player class.
5. High Cohesion
A rich model can also increase the cohesion of our domain and comply with the Single Responsibility Principle. In this instance, the Player instance is responsible for managing the number of points it won, while the TennisGame class takes on the responsibility of coordinating the two players and tracking the game’s overall score.
Nevertheless, we should be careful when moving these small bits of logic from the use-case implementation into our model. As a rule of thumb, we should only move the use-case independent functions to maintain a high cohesion.
In other words, we might be tempted to add to the Player class a method such as “*hasWonOver(Player opponent)*“, but this rule only makes sense if the players are playing against each other. Moreover, this is not a use-case independent rule: the condition of winning a game might be different based on the format of the match (for example, when playing singles, doubles, best of three sets, best of five sets, or other formats).
6. Increased Expressiveness
Another benefit of enriching the domain model is that it allows us to reduce the complexity of the domain service or use-case class. In other words, the TennisGame class will now be more expressive allowing the developers to focus on the business rules by hiding the Player-related details.
The process of questioning the use of getters and setters and making changes to the public API of the Player class has challenged us to gain a deeper understanding of our domain model and its capabilities. This is an important step that is often overlooked because of the convenience of using tools like IDEs or Lombok to automatically generate getters and setters.
7. Conclusion
In this article, we discussed the concept of anemic objects and the advantages of employing a rich domain model. Following this, we provided a practical example of how to encapsulate an object’s data and offer an improved interface. Finally, we discovered the numerous benefits of this approach, including increased expressiveness, heightened cohesion, and reduced coupling.
As usual, the complete code is available over on GitHub.