Consider the following scenario.
I have an interface IService:
public interface IService
{
void DoSomething();
}
with an implementation:
public class Implementation : IService
{
// This might throw an exception of type MyException
public void DoSomething()
{
// Implementation code
}
}
MyException
can be considered a part of the interface contract in this case, since all implementations may throw this exception.
Now, I have multiple clients which consume this service via dependency injection:
public class Client
{
private readonly IService _service;
public Client(IService service)
{
_service = service;
}
public void Consume()
{
try
{
_service.DoSomething();
}
catch (MyException ex)
{
// Handle MyException
}
}
}
In order to handle rate limiting, I implement a resilience decorator:
public class ResilienceDecorator : IService
{
private readonly IService _decoratee;
public ResilienceDecorator(IService decoratee)
{
_decoratee = decoratee;
}
public void DoSomething()
{
// Put rate limiting logic here which wraps call to decoratee.DoSomething()
// This logic may throw "RateLimiterRejectedException"
}
}
The problem:
The ResilienceDecorator
might throw a RateLimiterRejectedException
. However, it doesn't make sense to include this exception in the IService
contract because it is possible that clients receive different implementations. Some clients may receive the decorated version while others may not.
To me, this seems to violate the Liskov Substitution Principle because the client should only know about the abstraction IService
, but it needs to handle exceptions that are specific to certain implementations or decorators.
How can I improve this design?