DisasterDev

Prefer interface to factory

Published on Jul 10, 2022

dotnetc#interface design

If you've read the design patterns book, then it's highly likely that you know what a Factory is.

While this pattern certainly has its usage in many places, it introduces a layer of abstraction. Therefore, I usually prefer to using a straightaway interface to writing a factory class, and now I'm going to tell you why.


Another layer of abstraction


A factory introduces another layer of abstraction to its consumers. For example, let us consider we have an interface IDrawer and some implementations.

interface IDrawer {
    void Draw(Canvas canvas);
}

class CircleDrawer : IDrawer {
    public void Draw(Canvas canvas) {
        // implementation details...
    }
}
class RectangleDrawer: IDrawer {
    public void Draw(Canvas canvas) {
        // implementation details...
    }
}

And this is how we usually have a factory class in a simplest way:

class DrawerFactory {
    private readonly IDrawer _circleDrawer;
    private readonly IDrawer _rectangleDrawer;

    public DrawerFactory(
        CircleDrawer circleDrawer,
        RectangleDrawer rectangleDrawer
    )
    {
        _circleDrawer = circleDrawer;
        _rectangleDrawer = rectangleDrawer;
    }

    public IDrawer GetDrawer(string shape) {
        return shape switch
        {
            "circle" => _circleDrawer,
            "rectangle" => _rectangleDrawer,
            _ => throw new InvalidOperationException($"Could not find Drawer for shape: {shape}")
        };
    }
}

The factory will work but it does introduce an extra layer of abstraction to the upper level logic. For example, to draw a circle, we need to ask IoC container to get a factory instance first, and then create an IDrawer instance, and then draw on the Canvas. It probably goes like this:

var factory = IoC.GetService<DrawerFactory>();
var drawer = factory.GetDrawer("circle");
drawer.Draw(canvas);

First of all, as an API consumer, I only want to draw something "ASAP"; why do I need to know there is a "factory" to be used to begin with? A factory class is really some low-level technical details that the consumer does not need to care about.

Second, it makes unit testing harder, because besides the actual IDrawer interface, there's a factory to mock in the first place. The code snippet below illustrates what I meant.

// This is the App class which uses IDrawer to draw
class App {
    private readonly DrawerFactory _factory;

    public App(DrawerFactory factory) {
        _factory = factory;
    }

    public Run() {
        var shape = IOUtils.GetUserInput();
        var drawer = _factory.GetDrawer(shape);
        drawer.Draw(canvas);
        // ... more code omitted
    }
}

// Now the unit test to test App.Run
[Fact]
public void ItCanDrawCircle()
{
    // Arrange
    var drawerFactory = new Mock<DrawerFactory>();
    var drawer = new Mock<IDrawer>();
    // we have to mock the factory first to return the actual mock of the IDrawer
    drawerFactory.Setup(m => m.GetDrawer("circle"))
        .Returns(drawer.Object);
    // now we mock the IDrawer
    drawer.Setup(m => m.Draw("circle"))
        .Verifiable();
    var sut = App(drawerFactory.Object);

    // Act
    sut.Run("circle");

    // Assert
    drawer.Verify();
}

As you can see, because we have introduced another layer of abstraction, so does our uint testing need to deal with that abstraction, which is not ideal.


Remove the abstraction


Now that we know the downside of the factory class, here's what we can improve. It's in fact very straightforward to better it. We update the interface to this:

interface IDrawer {
    void Draw(Canvas canvas, string shape);
}

So we lifted the shape parameter to the Draw method so that the consumer can just use this interface directly without caring about any sort of "factory". The implementation does not change much:

class Drawer : IDrawer {
    private readonly IDrawer _circleDrawer;
    private readonly IDrawer _rectangleDrawer;

    // As the number of IDrawers goes up, you may want to be
    // smarter about the injection. For example, consider automatically
    // scanning assemblies to inject
    public Drawer(
        CircleDrawer circleDrawer,
        RectangleDrawer rectangleDrawer
    )
    {
        _circleDrawer = circleDrawer;
        _rectangleDrawer = rectangleDrawer;
    }

    public void Draw(Canvas canvas, string shape) {
        return shape switch
        {
            "circle" => _circleDrawer.Draw(canvas),
            "rectangle" => _rectangleDrawer.Draw(canvas),
            _ => throw new InvalidOperationException($"Could not find Drawer for shape: {shape}")
        };
    }
}

Now the unit test also becomes simpler:

// This is the App class which uses IDrawer to draw
class App {
    private readonly IDrawer _drawer;

    public App(IDrawer drawer) {
        _drawer = drawer;
    }

    public Run() {
        var shape = IOUtils.GetUserInput();
        _drawer.Draw(canvas, shape);
        // ... more code omitted
    }
}

// Now the unit test to test App.Run
[Fact]
public void ItCanDrawCircle()
{
    // Arrange
    var drawer = new Mock<IDrawer>();
    // JUST mock the IDrawer
    drawer.Setup(m => m.Draw("circle"))
        .Verifiable();
    var sut = App(drawer.Object);

    // Act
    sut.Run("circle");

    // Assert
    drawer.Verify();
}

In summary


I suppose by now you've got the gist. The main point of using an direct interface rather than a factory class is to reduce the complexity for high level logic. Not only does it make the code easier to understand (at least from the consumer's perspective), but also it simplifies the unit testing.

You may see this as a simple and even naive change, but the idea behind it is never old - that is to write simple code that makes sense.

© 2022 disasterdev.net. All rights reserved