Factories and packages

Exquisite corpse
Exquisite corpse, 1938 - Yves Tanguy

A few weeks ago Uncle Bob wrote a blog post about if/else/switch being a dependency magnet. He recommended reversing dependencies by using polymorphism, and he chose a factory pattern to do that. It was a very useful post, and I completely agree with its main point, but it got me thinking - do we really need factories for that? Even though I have used them occasionally, or at least something structurally similar to them, I never actually liked factories, they belong in a domain model as much as services do, i.e. not at all, unless maybe your domain is manufacturing agricultural machinery. Can we avoid them and keep the domain model clean? Let’s review Uncle Bob’s post.

Mapping genders

It all started by somebody asking how best to implement the following mapping:

0 -> "male"
1 -> "female"

Uncle Bob rightly observed that most likely this will be used to make policy decisions, and the mapping rule will need to be used all over the place, therefore the best thing to do is encapsulate it in a domain entity. He didn’t give a concrete example, but it is reasonable to infer it could look something like the following. We could have a Human interface with relevant methods for decision making, maybe something like this:

interface Human {
    int averageHeight();
    int averageLifespan();
}

Since males and females differ in average height and lifespan, these differences could be encapsulated in particular Male and Female objects. And then the factory:

class DefaultHumanFactory implements HumanFactory {
    @Override
    public Human build(int code) {
        switch (code) {
            case 0: return new Male();
            case 1: return new Female();
            default: throw new IllegalArgumentException();
        }
    }
}

The whole domain model looks like this:

1-Uncle-Bob-diagram

One thought arises immediately - what the hell is a HumanFactory? Is our domain “Dr. Frankenstein goes industrial”? Can there be other ways to model this mapping? Sure.

Map

If we need to model some mapping, which object is the most natural fit? A Map of course.

Map.of(
    0, Male::new,
    1, Female::new
)

There can be nothing clearer and simpler than this. Look at how our model is simplified: instead of two high level interfaces, one of which was an unnatural abomination, we now have just one - the essential one.

2-Simple-map-diagram

However, there is a point where simple becomes just overly simplistic. If creating an instance of Human involves more than just invoking a zero parameter constructor, if the implementations need a non-trivial structure of dependencies, then we might need some object to encapsulate the map in.

class DefaultGenderMapping implements GenderMapping {
    private final Map<Integer, Supplier<Human>> map;

    public DefaultGenderMapping(dependencies...) {
        this.map = Map.of(
            0, () -> new Male(dependencies...),
            1, () -> new Female(dependencies...)
        );
    }
        
    @Override
    public Human resolve(int code) {
        return map.get(code).get();
    }
}

3-Custom-map-type-diagram

Aren’t we back at just having a factory? Structurally yes, the high level structure is completely the same. However, conceptually this is different. HumanFactory is a Frankensteinian horrorshow, while GenderMapping is a valid concept implied by the requirements. It makes the architecture “scream” a little more, and in a different way than someone would upon encountering a human factory.

We can go even further. We don’t strictly need the additional high level interface. We can just make a custom map, e.g. by extending the MapEnvelope from Cactoos.

class GenderMapping extends MapEnvelope<Integer, Supplier<Human>> {
    public GenderMapping(dependencies...) {
        super(Map.of(
            0, () -> new Male(dependencies...),
            1, () -> new Female(dependencies...)
        ));
    }
}

Then it can be injected into the HighLevel simply as a Map<Integer, Supplier<Human>>. Now the model is much better.

4-Cactoos-map-diagram

Additional implementation of the same type

If we want to better understand the fundamental principles of this design, we should ask a seemingly stupid question - if we need a Human, why do we need an indirection (factory / map), why can’t we just inject or create a Human in the “high level” directly? I must admit I am strongly influenced by Yegor and his tendency to use decorators for everything, because it reduces the number of high level types. We can have a single interface representing a domain concept, and different implementations for each specific responsibility or need. So the first though I had when reading Uncle Bob’s post was - why factory? Why not just HumanFromCode implements Human? Why have two interfaces when you can have just one? (I had similar ideas here.)

class HumanFromCode implements Human {
    private final Human origin;

    public HumanFromCode(int code) {
        this.origin = Map.of(
            0, new Male(),
            1, new Female()
        ).get(code);
    }
    
    @Override
    public int averageHeight() {
        return origin.averageHeight();
    }

    @Override
    public int averageLifespan() {
        return origin.averageLifespan();
    }
}

I think it is obvious why we cannot simply inject it into the “high level” and use it there. Multiplicity between HighLevel and Human is 1..n. HighLevel receives codes and has to convert them to Human objects - since a single HighLevel can receive multiple codes, it relates to multiple Humans. Object field models a 1..1 multiplicity, however, not 1..n.

5-Diagram-code-HighLevel-Human

Therefore, this will not do:

class HighLevel {
    private final Human human;

    // HumanFromCode is injected here.
    public HighLevel(Human human) {
        this.human = human;
    }

    public void highLevelLogic() {
        System.out.println(human.averageHeight());
    }
}

1..n multiplicity can be modelled by a list, map (see above), repository, or even an object creating method. This is indeed possible:

class HighLevel {
    public void highLevelLogic(int code) {
        System.out.println(
            new HumanFromCode(code).averageHeight()
        );
    }
}

This, however, does not solve the problem Uncle Bob posited in the first place, namely that HighLevel depends on lower level: HighLevel depends on the concrete HumanFromCode class. Even if we put HumanFromCode on the higher level, this doesn’t solve the problem because HumanFromCode itself depends on Male and Female.

6-Diagram-HighLevel-HumanFromCode-Male-Female

To solve this problem we need to reverse the dependency, and the only acceptable1 way to reverse a dependency is dependency injection. Yegor himself notes this when he argues that using new in methods is evil. So what can we do? We cannot create a concrete object because that would couple high level to low level, and we cannot inject a concrete object because that only works for 1..1 multiplicities. What we can do is combine these approaches: model the 1..n multiplicity with an appropriate type, simplifying it to a 1..1 multiplicity, and then inject an object of that type. For example, HighLevel 1-n-> Human can be converted into HighLevel 1-1-> Humans, and then an implementation of Humans can be injected into HighLevel. For Uncle Bob’s example, the most appropriate type is some kind of map, as we already saw above.

7-Diagram-HighLevel-GenderMapping-Male-Female

More complex package dependencies

So far we have analysed dependencies for two packages: high level and low level, and how we can make low level depend on high level and not the other way around. Let’s see how this can be applied for three packages. In order not to lose ourselves in details, let’s abstract from concrete types (e.g. Human) and just use letters. With two packages, we had an entity A and a collection entity AN on the high level, and two implementations of A: A1 and A2 plus an implementation of AN - AN1, on the low level. Let’s call these packages root (high level) and a (low level).

8-Diagram-full-a-a1

If we want to add a third package, it can be either of an even lower level, i.e. a subpackage of a, in which case we would designate it a1, or of the same level as a, in which case we would designate it b. If it is a subpackage of a, then everything is straightforward, and we don’t need to look into it any further: 1a -> a -> root.

If it is on the same level as a, then we would have something like this:

10-Diagram-full-a-root-b

In the above diagram, there is no relation between a and b, because there is no relation between A and B. If, however, we introduce a relation A -> B, then we also have to introduce relationships A1 -> B and A2 -> B, because, as a great philosopher once wrote, there is nothing in the genus which is not also in the species, even if in the genus it is there indeterminately. For example, let’s say A -> B is Dog -> Human, which models this:

interface Dog {
    Human owner();
}

In that case A1 -> B would be Husky -> Human:

class Husky implements Dog {
    private final Human owner;

    public Human owner() {
        return owner;    
    }
}

The interesting thing is even if Dog depends on Human, even if both Husky and Rottweiler have to depend on Human as well, none of them have to depend either on Male or Female, i.e. no classes from dog package have to depend on anything from human package. dog and human packages can be completely decoupled from each other. This is how:

11-Large-diagram

How does Husky get an instance of Male to be his owner then? There has to be a symmetry between singular entity relations and plural entity relations. Just as A -> B and A1 -> B, so too AN1 -> BN. Or, given our pet example, just as Dog -> Human, so too DogRegistryInDatabase -> PopulationRegistry:

class DogRegistryInDatabase implements DogRegistry {
    private final Database db;
    private final BreedMapping dogs;
    private final PopulationRegistry population;
    
    public Dog byId(String id) {
        Row result = db.fetch(id);
        return dogs.resolve(
            id,
            result.breed,
            population.byId(result.ownerId)
        );
    } 
}

Here is the full picture, the red arrows showing how Husky can get Male even though nothing from dog package depends on human package or vice versa.

12-Full-diagram-with-red-arrows

Conclusion

If/else/switch blocks are dependency magnets, Uncle Bob is right about that. They need to be replaced with polymorphism, and dependencies need to be reversed. Factories can do that, but factories are awful at modelling domain. Still, to reverse the dependency, an additional interface, or type, is needed, even if it is not a factory interface. Different models call for different types, but it has to be an additional type, either a custom one or from the SDK, because just adding another implementation of the first type sadly will not reverse the dependency. When we have these types in the model, however, additional entities, relations and packages can be added, while keeping all of them decoupled and dependencies reversed.

  1. There are unacceptable ways such as Service Locator. 

Written on April 7, 2021