Antecedentes: estou desenvolvendo uma estrutura de mensagens. Essa estrutura permitirá:
- envio de mensagens através de um barramento de serviço
- assinando filas no barramento de mensagens
- assinando tópicos em um barramento de mensagens
Atualmente, estamos usando o RabbitMQ, mas sei que iremos mudar para o Microsoft Service Bus (no local) em um futuro muito próximo.
Eu pretendo criar um conjunto de interfaces e implementações para que, quando mudarmos para o ServiceBus, eu simplesmente precise fornecer uma nova implementação sem alterar nenhum código do cliente (ou seja, editores ou assinantes).
O problema aqui é que o RabbitMQ e o ServiceBus não são diretamente traduzíveis. Por exemplo, o RabbitMQ depende de trocas e nomes de tópicos, enquanto o ServiceBus é sobre espaços para nome e filas. Além disso, não há interfaces comuns entre o cliente ServiceBus e o cliente RabbitMQ (por exemplo, ambos podem ter um IConnection, mas a interface é diferente - não de um espaço para nome comum).
Então, ao meu ponto de vista, posso criar uma interface da seguinte maneira:
public interface IMessageReceiver{
void AddSubscription(ISubscription subscriptionDetails)
}
Devido às propriedades não traduzíveis das duas tecnologias, as implementações ServiceBus e RabbitMQ da interface acima têm requisitos diferentes. Portanto, minha implementação do IMessageReceiver RabbitMq pode ficar assim:
public void AddSubscription(ISubscription subscriptionDetails){
if(!subscriptionDetails is RabbitMqSubscriptionDetails){
// I have a problem!
}
}
Para mim, a linha acima quebra a regra de substituibilidade de Liskov.
Eu considerei inverter isso, para que uma Assinatura aceite um IMessageConnection, mas novamente a Assinatura RabbitMq exigiria propriedades específicas de um RabbitMQMessageConnection.
Então, minhas perguntas são:
- Estou correto que isso quebra o LSP?
- Concordamos que, em alguns casos, é inevitável ou estou perdendo alguma coisa?
Felizmente, isso é claro e sobre o assunto!
fonte
interface TestInterface<T extends ISubscription>
comunicaria claramente quais tipos são aceitos e que existem diferenças entre implementações.interface IMessageReceiver<T extends ISubscription>{void AddSubscription(T subscriptionDetails); }
. Uma implementação pode parecerpublic class RabbitMqMessageReceiver implements IMessageReceiver<RabbitMqSubscriptionDetails> { public void AddSubscription(RabbitMqSubscriptionDetails subscriptionDetails){} }
(em java).Respostas:
Sim, ele quebra o LSP, porque você está restringindo o escopo da subclasse limitando o número de valores aceitos, e fortalecendo as pré-condições. O pai especifica que aceita,
ISubscription
mas o filho não.Se é inevitável, isso é motivo de discussão. Você poderia mudar completamente seu design para evitar esse cenário, talvez mudar o relacionamento empurrando coisas para seus produtores? Dessa forma, você substitui os serviços declarados como interfaces que aceitam estruturas de dados e as implementações decidem o que elas querem fazer com elas.
Outra opção é deixar explicitamente ao usuário da API que uma situação de um subtipo inaceitável pode ocorrer, anotando a interface com uma exceção que pode ser lançada, como
UnsupportedSubscriptionException
. Fazendo isso, você define o direito estrito de pré-condição durante a modelagem da interface inicial e pode enfraquecê-la ao não lançar a exceção, caso o tipo esteja correto sem afetar o restante do aplicativo responsável pelo erro.fonte
Sim, seu código quebra o LSP aqui. Nesses casos, eu usaria a camada anticorrupção dos padrões de design do DDD. Você pode ver um exemplo aqui: http://www.markhneedham.com/blog/2009/07/07/domain-driven-design-anti-corruption-layer/
A idéia é reduzir ao máximo a dependência do subsistema, isolando-o explicitamente, para minimizar a refatoração ao alterá-lo
Espero que ajude !
fonte