Architecture Advice (For CSharp)

Do Not Use Base Classes

Given

public class D : B
{
    public void M(int x)
    {
        x *= 2; 
        return PM(x);
    }

    public override void V()
    {
        //...
    }

}

If we are to test method M we must also test method PM, and any other dependencies method PM uses. We must provide whatever other dependencies are required by B to create an instance of D.

In the case of overriding virtual methods we should instead implement interfaces.

Refactoring Base Classes Into Injected Dependencies

Re-write the above as follows:

public interface IV
{
    void V();
}

public sealed class D : IV

{
    public interface IB
    {
        int PM(int x); 
    }

    private IB _b;

    public D(IB b)
    {
        _b=b;
    }

    public int M(int x)
    {
        x *= 2; 
        return _b.PM(x);
    }

    void V()
    {
        //...
    }

}


This is a good first step. We can now test method M without having to know about the details of PM, and we can test class D without bringing in class B and its dependencies. We have to explicitly provide an implementation of the base class B, which can be seen as a drawback.

Proper Encapsulation

We we really want is to expose class D without the fuss of needing to always provide an implementation of interface IB, while still allowing our tests the ability to provide an IB implementation for testing.

public interface IV
{
    void V();
}

public interface ID : IV
{
    int M(int x);
}

public static class BuildD
{
    static ID New() { return new D(new B()); }
    static ID New(IB b) { return new D(b); }
}

internal sealed class D : ID
{
    public interface IB
    {
        int PM(int x); 
    }

    private IB _b;

    public D(IB b)
    {
        _b=b;
    }

    public int M(int x)
    {
        x *= 2; 
        return _b.PM(x);
    }

    void V()
    {

        //...
    }
}

Now instead of writing new D() we can write BuildD.New(), or if we are testing D we can write BuildD.New(mockB). The details of D's implementation are now completely hidden away behind the ID interface.

Why is IB defined in D?

Class D should define its dependencies locally and should minimize those definitions to only to parts of the dependencies it needs. If IB was defined elsewhere it would be tempting to extend it to fulfill other classes requirements, placing a further testing burden on class D that class D doesn't require. For instance, if class D needed to persist and update information between runs it could depend on a file system representation. If the representation was shared by several classes it would grow over time to encompass a full wrapping of the file system making mocking it for testing considerably more complex. By instead declaring a local interface that is limited to two functions, storing a value and retrieving it, we decouple class D from this complexity and more accurately reflect its external dependencies. Indeed, we are no longer limited to the concept of a file system being used for the storage and are free to implement the persistence in whatever form we, as the consumer of D, feel we want to.

Do Not Use Static Variable That Are Not (Effectively) Immutable

Static variables (a.k.a. global variables, singletons, etc.) can be used, but only if they are immutable. That is they can be assigned once (during initialization) and then never change after that. All uses of static variables should be via an indirection or interface so testing code can hook their usage. When in doubt, avoid them.

Refactoring Statics Into Injected Dependencies

public class X
{
    public void NeedsTheTime()
    {
        var now = DateTime.UtcNow;
        ...
    }
}

Reading the system clock directly couples your code to the concrete implementation of the system time. To test this code you'd have to alter the systems clock. Many places do exactly that, preventing them from running their tests concurrently, since there is only one system time.

To improving this code we decouple the concrete coupling and make the access to the time use a locally defined interface:

public sealed class X 
{
    public interface IDateTimeProvider { DateTime UtcNow(); }

    private readonly IDateTimeProvider _dateTimeProvider;

    public X(IDateTimeProvider dateTimeProvider)
    {
        _dateTimeProvider = dateTimeProvider;
    }

    public void NeedsTheTime() 
    {
        var now = _dateTimeProvider.UtcNow();
        ...
    }
}


Comments