Cross-cutting concerns and the Single Responsibility Principle

When developing a piece of production software, there are often additional things that a function must do beyond its intended task: the function must not just return the result of the calculation, but also add a logging message. Or add an auditing message. Or the results must not just be presented to the user, but also persisted on disk or in a database. Or the validity of the user’s license must be assessed before the function can be run. The examples are nearly endless.

Read more: Cross-cutting concerns and the Single Responsibility Principle

I always used to end up with a single function that does multiple things: its main purpose, plus an additional number of side effects that were required to make it production ready.

Single Responsibility Principle

However, a function that does more than one thing violates the Single Responsibility Principle. This principle states that every single part of the software is responsible for a single job. Like the software can be decomposed in modules, classes and functions, responsibilities can be decomposed as well. So a module may be responsible for something like the database access, then there may be various classes inside the database module to handle various smaller responsibilities.

The Single Responsibility Principle has two sides: no job can be distributed over multiple parts (although the responsibility can be decomposed into smaller, but in itself complete, responsibilities), but also the reverse: every part must have one single responsibility.

It is the latter that seems to get the way when we need to take cross-cutting concerns into account. I have this database module, but then I have to perform logging so that database connection issues can be analyzed. How can I possibly move the logging to a separate class, where it does not even have all data available?

Decorating responsibilities

When responsibilities are decomposed small enough, most of the time I end up with role interfaces: an interface struct that defines a single responsibility. Most of the time, that turns out to be an interface with one member, sometimes two. For example:

public interface ICalculator {
    double Calculate();
}

Such an interface makes it easy to have multiple implementations, while relieving the clients from all implementation details. For example, you could have the following implementations:

public class ItemPriceCalculator : ICalculator {
    private Item mItem;
    private Quantity mQuantity;

    public ItemPriceCalculator(Item item, Quantity quantity) {
        mItem = item;
        mQuantity = quantity;
    }

    public double Calculate() {
        return mItem.Price * mQuantity;
    }

public class SumCalculator : ICalculator {
    private ICalculator[] mItems;

    public SumCalculator(params ICalculator[] items) {
        mItems = items;
    }

    public double Calculate() {
        double result = 0;
        foreach(var item in mItems)
            result += item.Calculate();
        return result;
    }
}  
public class AddingCalculator : ICalculator {
    private readonly double mAmount;
    private readonly ICalculator mInner;
    
    public AddingCalculator(double amount, ICalculator inner) {
        mAmount = amount;
        mInner = inner;
    }
    
    public double Calculate() {
        return mInner.Calculate() + amount;
    }
}

These implementations all perform some actual calculation. Nice and tidy.

But, as long as our class has a double Calculate() method, nothing stops us from doing completely different things. We could, for example, delegate the complete implementation (actually this creates some adapter):

public class FuncCalculator : ICalculator {
    private Func<double> mAdaptee;

    public FuncCalculator(Func<double> adaptee) {
        mAdaptee = adaptee;
    }

    public double Calculate() {
        return mAdaptee();
    }
}

We can also wrap the implementation and decorate it with something completely unrelated to calculation:

public class LoggingCalculator : ICalculator {
    private ILogger mLog;
    private ICalculator mInner;

    public LoggingCalculator(ILogger log, ICalculator inner) {
        mLog = log;
        mInner = inner;
    }

    public double Calculate() {
        mLog.Info("Calculating....");
        mInner.Calculate();
        mLog.Info("Done!");
    }
}

Then, at the top-level point where we initialize the application, we can mix and match ICalculator instances any way we like. For example:

var logger = new Logger();
var random = new FuncCalculator(() => Random.NextDouble());
var randomOffset = new AddingCalculator(3, random);
var calculator = new LoggingCalculator(logger, randomOffset);

This is just a simple example of the Decorator pattern applied to a role interface. In this case, the role interface has a single member with no parameters, but the same principle can be applied to more complex cases. An example that accepts parameters is shown below:

public interface IOrderProcessor {
    IOrderResult Process(IOrder order);
}

public class LoggingProcessor : IOrderProcessor {
    private readonly ILogger mLog;
    private readonly IOrderProcessor mInner;
    
    public LoggingProcessor(ILogger log, IOrderProcessor inner) {
        mLog = log;
        mInner = inner;
    }
    
    public IOrderResult Process(IOrder order) {
        mLog.Info("Processing {Order}", order);
        return mInner.Process(order);
    }
}

public class LicensingProcessor : IOrderProcessor {
    private readonly License mLicense;
    private readonly IOrderProcessor mInner;
    
    public LicensingProcessor(License license, IOrderProcessor inner) {
        mLicense = license;
        mInner = inner;
    }
    
    public IOrderResult Process(IOrder order) {
        mLicense.CheckValidity();
        return mInner.Process(order);
    }
}

public class OrderProcessor : IOrderProcessor {
    private readonly ICustomer mCustomer;
    
    public OrderProcessor(ICustomer customer) {
        mCustomer = customer;
    }
    
    public IOrderResult Process(IOrder order) {
        return mCustomer.Accept(order);
    }
}

If you look closely, you can see a pattern emerge from the examples. Most decorators accept an inner parameter in their constuctors, so they act as wrapper classes around an actual implementation. This way, you can attach completely unrelated behavior to business functions without breaking encapsulation.

Leave a Reply

Your email address will not be published. Required fields are marked *