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.