Реальный мир - принцип замещения Лискова

14

Справочная информация: я разрабатываю систему обмена сообщениями. Эта структура позволит:

  • отправка сообщений через сервисную шину
  • подписка на очереди в шине сообщений
  • подписка на темы в шине сообщений

В настоящее время мы используем RabbitMQ, но я знаю, что мы перейдем на Microsoft Service Bus (в Premise) в самое ближайшее время.

Я планирую создать набор интерфейсов и реализаций, чтобы при переходе на ServiceBus мне просто нужно было предоставить новую реализацию без внесения каких-либо изменений в код клиента (т. Е. Издателей или подписчиков).

Проблема в том, что RabbitMQ и ServiceBus не могут быть переведены напрямую. Например, RabbitMQ опирается на биржи и имена тем, а ServiceBus - это пространства имен и очереди. Также нет общих интерфейсов между клиентом ServiceBus и клиентом RabbitMQ (например, оба могут иметь IConnection, но интерфейс отличается - не от общего пространства имен).

Итак, на мой взгляд, я могу создать интерфейс следующим образом:

public interface IMessageReceiver{
  void AddSubscription(ISubscription subscriptionDetails)
}

Из-за непереводимых свойств двух технологий реализации ServiceBus и RabbitMQ вышеприведенного интерфейса предъявляют различные требования. Так что моя реализация RabessMq для IMessageReceiver может выглядеть так:

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

Для меня вышеприведенная линия нарушает правило замещаемости Лискова.

Я подумал об этом, чтобы подписка приняла IMessageConnection, но опять-таки для подписки RabbitMq потребовались бы определенные свойства RabbitMQMessageConnection.

Итак, мои вопросы:

  • Я прав, что это нарушает LSP?
  • Согласны ли мы с тем, что в некоторых случаях это неизбежно или я что-то упускаю?

Надеюсь, это понятно и по теме!

GinjaNinja
источник
Является ли добавление параметра типа в интерфейс для вас? С точки зрения Java-синтаксиса, что-то вроде interface TestInterface<T extends ISubscription>бы ясно показывает, какие типы принимаются, и что есть различия между реализациями.
Халк
@ Халк, я не верю в это, поскольку каждая реализация потребует отдельной реализации ISubscription.
GinjaNinja
1
Извините, я хотел предложить interface IMessageReceiver<T extends ISubscription>{void AddSubscription(T subscriptionDetails); }. Реализация может выглядеть так public class RabbitMqMessageReceiver implements IMessageReceiver<RabbitMqSubscriptionDetails> { public void AddSubscription(RabbitMqSubscriptionDetails subscriptionDetails){} }(в Java).
Халк

Ответы:

11

Да, это нарушает LSP, потому что вы сужаете область действия подкласса, ограничивая количество принятых значений, вы усиливаете предварительные условия. Родитель указывает, что принимает, ISubscriptionа ребенок - нет.

То, неизбежно ли это, является предметом обсуждения. Не могли бы вы, возможно, полностью изменить свой дизайн, чтобы избежать этого сценария, возможно, перевернуть отношения, подтолкнув вещи к своим продюсерам? Таким образом вы заменяете сервисы, объявленные как интерфейсы, принимающие структуры данных, и реализации решают, что они хотят с ними делать.

Другой вариант - сообщить пользователю API о том, что может возникнуть ситуация недопустимого подтипа, аннотируя интерфейс исключением, которое может быть выдано, например UnsupportedSubscriptionException. При этом вы определяете строгое предварительное условие прямо во время моделирования начального интерфейса и можете ослабить его, не выбрасывая исключение, если тип будет правильным, не затрагивая остальную часть приложения, которое объясняет ошибку.

Энди
источник
Благодарю. Я не могу думать, как «перевернуть» это, не перемещая проблему. Например, подписка должна знать IModel для RabbitMQ и что-то еще для ServiceBus, чтобы получить сообщение. Я думаю, что аннотированное исключение - единственный путь вперед.
GinjaNinja
2

Да, ваш код нарушает LSP здесь. В таких случаях я бы использовал Антикоррупционный слой из DDD Design Patterns. Вы можете увидеть один пример там: http://www.markhneedham.com/blog/2009/07/07/domain-driven-design-anti-corruption-layer/

Идея состоит в том, чтобы максимально уменьшить зависимость от подсистемы путем ее явной изоляции, чтобы минимизировать рефакторинг при его изменении.

Надеюсь, это поможет !

Julien
источник