Antecedentes: estoy desarrollando un marco de mensajería. Este marco permitirá:
- envío de mensajes a través de un bus de servicio
- suscribirse a colas en el bus de mensajes
- suscribirse a temas en un bus de mensajes
Actualmente estamos utilizando RabbitMQ, pero sé que nos trasladaremos a Microsoft Service Bus (en las instalaciones) en un futuro muy cercano.
Planeo crear un conjunto de interfaces e implementaciones para que cuando pasemos a ServiceBus, simplemente necesite proporcionar una nueva implementación sin modificar el código del cliente (es decir, editores o suscriptores).
El problema aquí es que RabbitMQ y ServiceBus no son directamente traducibles. Por ejemplo, RabbitMQ se basa en intercambios y nombres de temas, mientras que ServiceBus se trata de espacios de nombres y colas. Además, no hay interfaces comunes entre el cliente ServiceBus y el cliente RabbitMQ (por ejemplo, ambos pueden tener una conexión IC, pero la interfaz es diferente, no de un espacio de nombres común).
Entonces, en mi opinión, puedo crear una interfaz de la siguiente manera:
public interface IMessageReceiver{
void AddSubscription(ISubscription subscriptionDetails)
}
Debido a las propiedades no traducibles de las dos tecnologías, las implementaciones de ServiceBus y RabbitMQ de la interfaz anterior tienen requisitos diferentes. Entonces mi implementación de RabbitMq de IMessageReceiver puede verse así:
public void AddSubscription(ISubscription subscriptionDetails){
if(!subscriptionDetails is RabbitMqSubscriptionDetails){
// I have a problem!
}
}
Para mí, la línea de arriba rompe la regla de sustituibilidad de Liskov.
Pensé en cambiar esto, para que una Suscripción acepte una IMessageConnection, pero nuevamente la Suscripción RabbitMq requeriría propiedades específicas de una RabbitMQMessageConnection.
Entonces, mis preguntas son:
- ¿Estoy en lo cierto de que esto rompe LSP?
- ¿Estamos de acuerdo en que en algunos casos es inevitable o me estoy perdiendo algo?
¡Con suerte, esto está claro y sobre el tema!
fuente
interface TestInterface<T extends ISubscription>
comunicaría claramente qué tipos son aceptados, y que hay diferencias entre las implementaciones.interface IMessageReceiver<T extends ISubscription>{void AddSubscription(T subscriptionDetails); }
. Una implementación podría verse asípublic class RabbitMqMessageReceiver implements IMessageReceiver<RabbitMqSubscriptionDetails> { public void AddSubscription(RabbitMqSubscriptionDetails subscriptionDetails){} }
(en Java).Respuestas:
Sí, rompe el LSP, porque al reducir el alcance de la subclase al limitar el número de valores aceptados, se refuerzan las condiciones previas. El padre especifica que acepta
ISubscription
pero el niño no.Si es inevitable es algo para debatir. ¿Podrías cambiar completamente tu diseño para evitar este escenario, tal vez cambiar la relación introduciendo cosas en tus productores? De esta forma, reemplaza los servicios declarados como interfaces que aceptan estructuras de datos y las implementaciones deciden qué quieren hacer con ellos.
Otra opción es dejar que el usuario de la API sepa explícitamente que puede ocurrir una situación de un subtipo inaceptable, al anotar la interfaz con una excepción que podría producirse, como
UnsupportedSubscriptionException
. Hacer que defina la condición previa estricta durante el modelado de la interfaz inicial y se le permita debilitarla al no lanzar la excepción en caso de que el tipo sea correcto sin afectar el resto de la aplicación que explica el error.fuente
Sí, su código rompe LSP aquí. En tales casos, usaría la capa anticorrupción de los patrones de diseño DDD. Puede ver un ejemplo allí: http://www.markhneedham.com/blog/2009/07/07/domain-driven-design-anti-corruption-layer/
La idea es reducir al máximo la dependencia del subsistema aislándolo explícitamente, para minimizar la refactorización al cambiarlo.
Espero eso ayude !
fuente