Writing understandable code is one of the most important aspects, especially when working in a team. In this article, I want to show you a simple alternative to the Builder pattern that makes your code more readable.
(Code examples are written in Java, but the idea can be adopted in other languages as well)
The scenario
Imagine we are working on a game where a player can fight against enemies. The enemies are configured in a JSON file. A JSON parser parses the file and creates enemy objects out of it. Here's how the Enemy class looks like:
@NoArgsConstructor
@Getter
@Setter
class Enemy {
private String name;
private int health;
private int exp;
private BattleAttributes attributes;
private List<Attack> attacks;
}
@NoArgsConstructor
@Getter
@Setter
class BattleAttributes {
private int strength;
private int defense;
}
@NoArgsConstructor
@Getter
@Setter
class Attack {
private String name;
private int strength;
}
(The empty constructor and setters are a requirement of the JSON parser in this example)
Having all enemies configured as JSON, we don't need to create these objects manually in the game code, because the JSON parser does it for us. But we also want to write unit tests for our game. There we have to create Enemy objects by hand to provide mock data for our tests.
Creating the objects
Here's an example how we could create an Enemy object in our test:
BattleAttributes attributes = new BattleAttributes();
attributes.setStrength(5);
attributes.setDefense(4);
Attack bite = new Attack();
bite.setName("Bite");
bite.setStrength(1);
Attack slash = new Attack();
slash.setName("Slash");
slash.setStrength(4);
Enemy enemy = new Enemy();
enemy.setName("Some Monster");
enemy.setHealth(100);
enemy.setExp(5);
enemy.setAttributes(attributes);
enemy.setAttacks(List.of(bite, slash));
Since the Enemy depends on BattleAttributes
and Attack
objects, we have to create these objects first. But this makes the code less readable in my opinion. When we read the code from top to bottom, we don't see at first glance which object we want to create at the end. And sometimes we have to look twice to see which object is being used at which point. It might take a while until you really understand what we are doing here.
This problem can be solved by using the Builder pattern:
Enemy enemy = Enemy.builder()
.name("Some Monster")
.health(100)
.exp(5)
.attributes(BattleAttributes.builder()
.strength(5)
.defense(4)
.build())
.attacks(List.of(
Attack.builder()
.name("Bite")
.strength(1)
.build(),
Attack.builder()
.name("Slash")
.strength(4)
.build()
))
.build();
Now when we read the code from top to bottom, we immediately see that an Enemy object is being created. The BattleAttributes
and Attack
objects are created at the point where we need them. This makes the code easier to understand.
However, there are cases when a Builder is not possible or not desirable. One argument against a Builder in this case is that we don't want to implement things in our actual game code just for testing.
So, how can we improve the readability of the object creation without a Builder? The answer is a simple helper method:
public static <T> T build(T obj, Consumer<T> consumer) {
consumer.accept(obj);
return obj;
}
This build()
method can be implemented either in the base test class or in a separate helper class. It accepts any kind of object as the first argument and passes it to the Consumer that is given as the second argument. In the Consumer we can then configure this object (A Consumer is just a function with one argument and no return value). Finally the build method returns the object. Let's see it in action:
Enemy enemy = new Enemy();
enemy.setName("Some Monster");
enemy.setHealth(100);
enemy.setExp(5);
enemy.setAttributes(build(new BattleAttributes(), attributes -> {
attributes.setStrength(5);
attributes.setDefense(4);
}));
enemy.setAttacks(List.of(
build(new Attack(), attack -> {
attack.setName("Bite");
attack.setStrength(1);
}),
build(new Attack(), attack -> {
attack.setName("Slash");
attack.setStrength(4);
})
));
Final words
With a simple build()
method you can create objects in a builder-like fashion without creating a Builder class. Since a Builder is not always possible, this approach could be a good alternative to make your code more readable.
Top comments (0)