14

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!

  • Is adding a type parameter to the interface an option for you? In terms of Java-syntax, something like interface TestInterface<T extends ISubscription> would clearly communicate which types are accepted, and that there are differences between implementations. – Hulk Nov 24 '16 at 12:06
  • @Hulk, I don't believe so, as each implementation would require a different implementation of ISubscription. – GinjaNinja Nov 24 '16 at 12:16
  • 1
    Sorry, what I wanted to propose was interface IMessageReceiver<T extends ISubscription>{void AddSubscription(T subscriptionDetails); }. An implementation could then look like public class RabbitMqMessageReceiver implements IMessageReceiver<RabbitMqSubscriptionDetails> { public void AddSubscription(RabbitMqSubscriptionDetails subscriptionDetails){} } (in java). – Hulk Nov 24 '16 at 13:25
11

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.

  • Thanks. I can't think how to 'flip' it without just moving the issue. E.g. The subscription would need to know the IModel for RabbitMQ and something else for ServiceBus in order to receive the message. I think the annotated exception is the only way forward. – GinjaNinja Nov 24 '16 at 15:53
2

Yes, your code breaks LSP here. In such cases, I would use Anti-Corruption Layer from DDD Design Patterns. You can see one example there : http://www.markhneedham.com/blog/2009/07/07/domain-driven-design-anti-corruption-layer/

The idea is to reduce as much as possible the dependency to sub-system by explicitly isolating it, to minimize the refactoring when changing it

Hope it helps !

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.