Prinzip der realen Welt - Liskov-Substitution

14

Hintergrund: Ich entwickle ein Messaging-Framework. Dieser Rahmen ermöglicht:

  • Senden von Nachrichten über einen Servicebus
  • Abonnieren von Warteschlangen auf dem Nachrichtenbus
  • Abonnieren von Themen auf einem Nachrichtenbus

Wir verwenden derzeit RabbitMQ, aber ich weiß, dass wir in naher Zukunft auf Microsoft Service Bus (on Premise) umsteigen werden.

Ich plane, eine Reihe von Schnittstellen und Implementierungen zu erstellen, sodass ich beim Wechsel zu ServiceBus lediglich eine neue Implementierung bereitstellen muss, ohne den Clientcode (dh Herausgeber oder Abonnenten) zu ändern.

Das Problem hierbei ist, dass RabbitMQ und ServiceBus nicht direkt übersetzbar sind. RabbitMQ basiert beispielsweise auf Börsen- und Themennamen, wohingegen sich ServiceBus ausschließlich um Namespaces und Warteschlangen dreht. Außerdem gibt es keine gemeinsamen Schnittstellen zwischen dem ServiceBus-Client und dem RabbitMQ-Client (z. B. haben beide möglicherweise eine IC-Verbindung, aber die Schnittstelle unterscheidet sich - nicht von einem gemeinsamen Namespace).

Ich kann also meines Erachtens eine Schnittstelle wie folgt erstellen:

public interface IMessageReceiver{
  void AddSubscription(ISubscription subscriptionDetails)
}

Aufgrund der nicht übersetzbaren Eigenschaften der beiden Technologien haben die ServiceBus- und RabbitMQ-Implementierungen der obigen Schnittstelle unterschiedliche Anforderungen. Meine RabbitMq-Implementierung von IMessageReceiver könnte also so aussehen:

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

Für mich verstößt die obige Zeile gegen die Ersetzbarkeitsregel von Liskov.

Ich habe darüber nachgedacht, dies umzudrehen, damit ein Abonnement eine IMessageConnection akzeptiert, aber das RabbitMq-Abonnement würde wiederum bestimmte Eigenschaften einer RabbitMQMessageConnection erfordern.

Meine Fragen lauten also:

  • Bin ich richtig, dass dies LSP bricht?
  • Sind wir uns einig, dass es in einigen Fällen unvermeidlich ist, oder fehle ich etwas?

Hoffentlich ist das klar und thematisch!

GinjaNinja
quelle
Ist das Hinzufügen eines Typparameters zur Schnittstelle eine Option für Sie? In Bezug auf die Java-Syntax interface TestInterface<T extends ISubscription>würde so etwas eindeutig mitteilen, welche Typen akzeptiert werden und dass es Unterschiede zwischen den Implementierungen gibt.
Hulk
@Hulk, ich glaube nicht, da jede Implementierung eine andere Implementierung von ISubscription erfordern würde.
GinjaNinja
1
Entschuldigung, was ich vorschlagen wollte, war interface IMessageReceiver<T extends ISubscription>{void AddSubscription(T subscriptionDetails); }. Eine Implementierung könnte dann so aussehen public class RabbitMqMessageReceiver implements IMessageReceiver<RabbitMqSubscriptionDetails> { public void AddSubscription(RabbitMqSubscriptionDetails subscriptionDetails){} }(in Java).
Hulk

Antworten:

11

Ja, dies unterbricht den LSP, da Sie den Umfang der Unterklasse einschränken, indem Sie die Anzahl der akzeptierten Werte begrenzen und die Voraussetzungen verbessern. Das übergeordnete Element gibt an, dass es akzeptiert ISubscription, das untergeordnete Element jedoch nicht.

Ob es unvermeidlich ist, ist ein Diskussionsgegenstand. Könnten Sie vielleicht Ihr Design komplett ändern, um dieses Szenario zu vermeiden, oder die Beziehung umkehren, indem Sie Ihre Produzenten damit beauftragen? Auf diese Weise ersetzen Sie die als Schnittstellen deklarierten Dienste, die Datenstrukturen akzeptieren, und die Implementierungen entscheiden, was sie damit tun möchten.

Eine andere Möglichkeit besteht darin, den Benutzer der API explizit darauf hinzuweisen, dass eine Situation eines nicht akzeptablen Untertyps auftreten kann, indem die Schnittstelle mit einer möglicherweise ausgelösten Ausnahme wie z UnsupportedSubscriptionException. Wenn Sie dies tun, definieren Sie die strenge Vorbedingung während der Modellierung der anfänglichen Schnittstelle und dürfen diese schwächen, indem Sie die Ausnahme nicht auslösen, sollte der Typ korrekt sein, ohne den Rest der Anwendung zu beeinträchtigen, der den Fehler verursacht.

Andy
quelle
Vielen Dank. Ich kann mir nicht vorstellen, wie ich es umdrehen soll, ohne nur das Thema zu verschieben. ZB Das Abonnement muss das IModel für RabbitMQ und etwas anderes für ServiceBus kennen, um die Nachricht zu erhalten. Ich denke, die kommentierte Ausnahme ist der einzige Weg nach vorne.
GinjaNinja
2

Ja, Ihr Code bricht hier LSP. In solchen Fällen würde ich Anti-Corruption Layer von DDD Design Patterns verwenden. Sie können dort ein Beispiel sehen: http://www.markhneedham.com/blog/2009/07/07/domain-driven-design-anti-corruption-layer/

Die Idee ist, die Abhängigkeit vom Subsystem durch explizite Isolierung so weit wie möglich zu verringern, um das Refactoring bei Änderungen so gering wie möglich zu halten

Ich hoffe es hilft !

Julien
quelle