Real World – Liskov Substitution Principle

liskov-substitutionsolid

Background: I am developing a messaging framework. This framework will allow:

  • sending of messages over a service bus
  • subscribing to queues on the message bus
  • subscribing to topics on a message bus

We are currently using RabbitMQ, but I know we will be moving to Microsoft Service Bus (on Premise) in the very near future.

I plan to create a set of interfaces and implementations so that when we move to ServiceBus, I simply need to provide a new implementation without amending any of the client code (i.e. publishers or subscribers).

The issue here is that RabbitMQ and ServiceBus are not directly translatable. For example, RabbitMQ relies on Exchanges and Topic Names, whereas ServiceBus is all about Namespaces and Queues. Also, there are no common interfaces between the ServiceBus client and the RabbitMQ client (e.g. both may have an IConnection, but the interface is different – not from a common namespace).

So to my point, I can create an interface as follows:

public interface IMessageReceiver{
  void AddSubscription(ISubscription subscriptionDetails)
}

Due to the non-translatable properties of the two technologies, the ServiceBus and RabbitMQ implementations of above interface have different requirements. So my RabbitMq implemetation of IMessageReceiver may look like this:

public void AddSubscription(ISubscription subscriptionDetails){
  if(!subscriptionDetails is RabbitMqSubscriptionDetails){
    // I have a problem!
  }
}

To me, the line above breaks Liskov's rule of substitutability.

I considered flipping this around, so that a Subscription accepts a IMessageConnection, but again the RabbitMq Subscription would require specific properties of a RabbitMQMessageConnection.

So, my questions are:

  • Am I correct that this breaks LSP?
  • Do we agree that in some cases it is unavoidable, or, am I missing something?

Hopefully, this is clear and on topic!

Best Answer

Yes, it does break the LSP, because you are narrowing the scope of the sub-class by limiting the number of accepted values, you are strenghtening the pre-conditions. The parent specifies it accepts ISubscription but the child does not.

Whether it's unavoidable is a thing for discussion. Could you maybe completely change your design to avoid this scenario, maybe flip the relationship around by pushing stuff into your producers? That way you replace the services declared as interfaces accepting data structures and the implementations decide what they want to do with them.

Other option is to let the user of the API explicitly know, that a situation of an unacceptable sub type might occur, by annotating the interface with an exception that might be thrown, such as UnsupportedSubscriptionException. Doing that you define the strict pre-condition right during modeling of the initial interface and are allowed to weaken it by not throwing the exception should the type be correct without affecting the rest of the application which accounts for the error.