Tło: opracowuję strukturę przesyłania wiadomości. Ramy te umożliwią:
- wysyłanie wiadomości za pośrednictwem magistrali usług
- subskrybowanie kolejek na szynie komunikatów
- subskrybowanie tematów na szynie wiadomości
Obecnie używamy RabbitMQ, ale wiem, że w najbliższej przyszłości przejdziemy do Microsoft Service Bus (w lokalu).
Planuję utworzyć zestaw interfejsów i implementacji, aby po przejściu na ServiceBus po prostu musiałem dostarczyć nową implementację bez zmiany żadnego kodu klienta (tj. Wydawców lub subskrybentów).
Problem polega na tym, że RabbitMQ i ServiceBus nie podlegają bezpośredniemu tłumaczeniu. Na przykład RabbitMQ opiera się na wymianach i nazwach tematów, podczas gdy ServiceBus dotyczy przestrzeni nazw i kolejek. Ponadto nie ma wspólnych interfejsów między klientem ServiceBus a klientem RabbitMQ (np. Oba mogą mieć połączenie IConnection, ale interfejs jest inny - nie od wspólnej przestrzeni nazw).
Moim zdaniem mogę utworzyć interfejs w następujący sposób:
public interface IMessageReceiver{
void AddSubscription(ISubscription subscriptionDetails)
}
Ze względu na nieprzekształcalne właściwości obu technologii, implementacje ServiceBus i RabbitMQ powyższego interfejsu mają różne wymagania. Więc moja implementacja IMessageReceiver w RabbitMq może wyglądać następująco:
public void AddSubscription(ISubscription subscriptionDetails){
if(!subscriptionDetails is RabbitMqSubscriptionDetails){
// I have a problem!
}
}
Dla mnie powyższa linia łamie zasadę substytucyjności Liskowa.
Rozważyłem odwrócenie tego, aby Subskrypcja akceptowała IMessageConnection, ale znowu Subskrypcja RabbitMq wymagałaby określonych właściwości RabbitMQMessageConnection.
Tak więc moje pytania to:
- Czy mam rację, że to łamie LSP?
- Czy zgadzamy się, że w niektórych przypadkach jest to nieuniknione, czy coś mi brakuje?
Mam nadzieję, że jest to jasne i na temat!
źródło
interface TestInterface<T extends ISubscription>
jasno komunikuje, które typy są akceptowane i że istnieją różnice między implementacjami.interface IMessageReceiver<T extends ISubscription>{void AddSubscription(T subscriptionDetails); }
. Implementacja może wtedy wyglądać następującopublic class RabbitMqMessageReceiver implements IMessageReceiver<RabbitMqSubscriptionDetails> { public void AddSubscription(RabbitMqSubscriptionDetails subscriptionDetails){} }
(w java).Odpowiedzi:
Tak, psuje LSP, ponieważ zawężasz zakres podklasy ograniczając liczbę akceptowanych wartości, wzmacniasz warunki wstępne. Rodzic określa, że akceptuje,
ISubscription
ale dziecko nie.To, czy jest to nieuniknione, należy do dyskusji. Czy mógłbyś całkowicie zmienić swój projekt, aby uniknąć tego scenariusza, może odwrócić relacje, popychając rzeczy do swoich producentów? W ten sposób zastępujesz usługi zadeklarowane jako interfejsy akceptujące struktury danych, a implementacje decydują, co chcą z nimi zrobić.
Inną opcją jest wyraźne poinformowanie użytkownika interfejsu API, że może wystąpić sytuacja o niedopuszczalnym podtypie, poprzez opatrzenie opisu interfejsu wyjątkiem, który może zostać zgłoszony, np
UnsupportedSubscriptionException
. W ten sposób zdefiniujesz ścisłe warunki wstępne podczas modelowania początkowego interfejsu i możesz go osłabić, nie rzucając wyjątku, jeśli typ jest poprawny bez wpływu na resztę aplikacji, która odpowiada za błąd.źródło
Tak, twój kod łamie LSP tutaj. W takich przypadkach używałbym warstwy antykorupcyjnej z wzorców projektowych DDD. Można tam zobaczyć jeden przykład: http://www.markhneedham.com/blog/2009/07/07/domain-driven-design-anti-corruption-layer/
Chodzi o to, aby maksymalnie ograniczyć zależność od podsystemu poprzez jawne odizolowanie go, aby zminimalizować refaktoryzację podczas jego zmiany
Mam nadzieję, że to pomoże !
źródło