DEV Community

loading...
Cover image for Builder-like object creation without a Builder

Builder-like object creation without a Builder

christianblos profile image Christian Blos Updated on ・3 min read

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;
}
Enter fullscreen mode Exit fullscreen mode

(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));
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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);
    })
));
Enter fullscreen mode Exit fullscreen mode

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.

Discussion (1)

pic
Editor guide
Collapse
pjotre86 profile image
pjotre86

I like it. In general I'd probably still go for the @Builder Lombok provides (especially when you're already using Lombok). But this can be a helpful thing in cases where can't or shouldn't alter existing code. E.g. when dealing with legacy code or 3rd party libraries which provide no builder this looks like an interesting alternative compared to writing own builders!