How to avoid violating of SOLID principles while extending service behaviours


Single responsobilty

This principe says that every class should be changed only by one reason.

Let’s imagen that we have some working service for example :

public interface ISomeService
{
    public int Count();
}

public class SomeService : ISomeService
{
    public int Count() 
    { 
        // 
        // Some business logic here
        //
        // return count;
    }
}

Now, this class respects single responsobility principe, so there is only one reason to change it. It is logic of counting.

However, next of your task says you should add support of logging time of executing every call of CountAsync method. You could notice it is not reason to change that specific class and question how to add that behaviour is appear. Fortunatelly, there is suitable pattern for this situations which called decorator.

Let’s create new class SomeServiceWithLogging which also implements ISomeService interface but also it is has link to another ISomeService.

public class SomeServiceWithLogging : ISomeService
{

    private readonly ISomeService _someService;
    private readonly ILogger<SomeServiceWithLogging> _logger;

    public SomeServiceWithLogging(ISomeService someService, ILogger<SomeServiceWithLogging> logger)
    {
        _someService = someService ?? throw new ArgumentNullException(nameof(someService));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public int Count()
    {
        using (_logger.BeginScope("Call some service count method"))
        {
            return _someService.Count();
        }
    }
}

As you see, we added support of logging without changing any line of code in original SomeService. One more thing we should set for dependency injection in Startup.cs -> ConfigureServices method.

services.AddScoped<SomeService>();

services.AddScoped<ISomeService>(serviceProvier=>
{
    return new SomeServiceWithLogging(
        serviceProvier.GetService<SomeService>(),
        serviceProvier.GetService<ILogger<SomeServiceWithLogging>>());
});

Here we go. Now our SomeServiceWithLogging class also respects Single reponsobility principe with one reason of changing and it only logging logic.

More decorators and sequence of calling

Let’s imagen that next task is support of caching result of Count method. Again, it is not reason for both services to be changed. So we create new class:

public class SomeServiceWithCaching : ISomeService
{
    private readonly ISomeService _someService;
    private readonly ICache _cache;

    public SomeServiceWithCaching(ISomeService someService, ICache cache)
    {
        _someService = someService ?? throw new ArgumentNullException(nameof(someService));
        _cache = cache ?? throw new ArgumentNullException(nameof(cache));
    }

    public int Count()
    {
        if(_cache.TryGet("Count", int cachedCount))
        {
            return cachedCount;
        }

        var count = _someService.Count();
        _cache.Set("Count", count);

        return count
    }
}

Again, we added support of caching without changing our existed services. However, now we need to call caching after logging behaviour, so it easy to set up in ConfigureServices method.

 services.AddScoped<SomeService>();
            
services.AddScoped<ISomeService>(serviceProvier=>
{
    var cachingService = new SomeServiceWithCaching(
        serviceProvier.GetService<SomeService>(),
        serviceProvier.GetService<ICache>());

    return new SomeServiceWithLogging(
        cachingService,
        serviceProvier.GetService<ILogger<SomeServiceWithLogging>>());
});

Here we can set up sequence of decorators calls.

Conclusion

Decorator is simple and strong pattern, which helps keep your code clean and simple respecting first of SOLID principles.

Buy Me A Coffee

Related Posts

Avoid reflections of mappers and let Mapster generate mappers for you

Mapster generating tool for onion application

Predict Bitcoin price with ML.net

Live time series coin price predictor with machine learning

Throw exceptions from backend to frontend with blazor

One of advantages of using same code language on both frontend and backend

Blazor render optimization

Best practices

.Net 6 brings application state to blazor webassembly server prerender

It kills strange blinking on screen

Must have libraries for blazor

List of best nuget packages

Blazor virtualize component

Lazy loading of big lists. Rendering optimization.

Blazor grpc - comunication optimization

Smaller and faster requests to your backend from blazor wasm

Free database for your blazor app

Don't pay for the cloud

Blazor common error component

Single component for showing errors on any page and component