¿El patrón estatal viola el principio de sustitución de Liskov?

17

Esta imagen está tomada de la aplicación de diseño y patrones basados ​​en dominio: con ejemplos en C # y .NET

ingrese la descripción de la imagen aquí

Este es el diagrama de clases para el Patrón de estado donde un SalesOrderpuede tener diferentes estados durante su vida útil. Solo se permiten ciertas transiciones entre los diferentes estados.

Ahora la OrderStateclase es una abstractclase y todos sus métodos se heredan a sus subclases. Si consideramos la subclase Cancelledque es un estado final que no permite ninguna transición a ningún otro estado, tendremos que utilizar overridetodos sus métodos en esta clase para lanzar excepciones.

Ahora, ¿eso no viola el principio de sustitución de Liskov ya que una subclase no debería alterar el comportamiento de los padres? ¿Cambiar la clase abstracta en una interfaz soluciona esto?
¿Cómo se puede arreglar esto?

Songo
fuente
¿Cambiar OrderState para que sea una clase concreta que arroje excepciones "NotSupported" en todos sus métodos por defecto?
James
No he leído este libro, pero me parece extraño que OrderState contenga Cancel () y luego hay un estado Cancelado, que es un subtipo diferente.
Eufórico el
@Euphoric ¿por qué es raro? las llamadas cancel()cambian el estado a cancelado.
Songo
¿Cómo? ¿Cómo puede llamar a Cancelar en OrderState, cuando se supone que cambia la instancia de estado en SalesOrder? Creo que todo el modelo está mal. Me gustaría ver la implementación completa.
Eufórico el

Respuestas:

3

Esta implementación particular, sí. Si hace que los estados sean clases concretas en lugar de implementadores abstractos, se saldrá de esto.

Sin embargo, el patrón de estado al que se refiere, que es efectivamente un diseño de máquina de estado, es en general algo con lo que no estoy de acuerdo por la forma en que lo he visto crecer. Creo que hay motivos suficientes para denunciar esto como una violación del principio de responsabilidad única porque estos patrones de gestión del estado terminan siendo depósitos centrales para el conocimiento del estado actual de muchas otras partes de un sistema. Esta parte de la gestión del estado que se está centralizando requerirá a menudo reglas comerciales relevantes para muchas partes diferentes del sistema para coordinarlas razonablemente.

Imagine si todas las partes del sistema que se preocupan por el estado estuvieran en diferentes servicios, diferentes procesos en diferentes máquinas, un administrador de estado central que detalla el estado de cada uno de esos lugares efectivamente cuellos de botella en todo este sistema distribuido y creo que ese cuello de botella es una señal de una violación de SRP, así como un mal diseño en general.

Por el contrario, sugeriría que uno haga los objetos más inteligentes como en el objeto Modelo en el patrón MVC donde el modelo sabe cómo manejarse solo, no necesita un orquestador externo para administrar su funcionamiento interno o su razón.

Incluso poniendo un patrón de estado como este dentro de un objeto para que solo se administre a sí mismo, parece que haría que ese objeto sea demasiado grande. Diría que los flujos de trabajo deberían realizarse a través de la composición de varios objetos auto-responsables, en lugar de con un solo estado orquestado que maneja el flujo de otros objetos, o el flujo de inteligencia dentro de sí mismo.

Pero en ese momento es más arte que ingeniería, por lo que definitivamente es subjetivo su enfoque a algunas de esas cosas, que dice que los principios son una buena guía y sí, la implementación que enumera es una violación de LSP, pero podría corregirse para que no lo sea. Solo tenga mucho cuidado con el SRP cuando use cualquier patrón de esta naturaleza y es probable que esté seguro.

Jimmy Hoffa
fuente
Básicamente, el gran problema es que la orientación a objetos es buena para agregar nuevas clases pero dificulta agregar nuevos métodos. Y en el caso del estado, como dijiste, no es probable que necesites extender el código con nuevos estados muy a menudo.
hugomg
3
@Jimmy Hoffa Lo siento, pero ¿qué quieres decir exactamente con "Si haces que los estados sean clases concretas en lugar de implementadores abstractos, entonces te alejarás de esto". ?
Songo
@ Jimmy: Traté de implementar el estado sin el patrón de estado haciendo que cada componente solo conozca su propio estado. Esto terminó haciendo grandes cambios para que el flujo sea significativamente más complejo de implementar y mantener. Dicho esto, creo que usar una máquina de estado (idealmente la biblioteca de otra persona para forzarlo a ser tratado como un cuadro negro) en lugar del patrón de diseño de estado (por lo tanto, los estados son solo elementos dentro de una enumeración en lugar de clases completas y las transiciones son simplemente tontas edge) brinda muchos de los beneficios del patrón de estado, mientras que es hostil a los intentos de los desarrolladores de abusar de él.
Brian
8

Un sublcass no debería alterar el comportamiento del padre?

Esa es una mala interpretación común de LSP. Una subclase puede alterar el comportamiento del padre, siempre que permanezca fiel al tipo padre.

Hay una buena explicación larga en Wikipedia , que sugiere las cosas que infringirán LSP:

... hay una serie de condiciones de comportamiento que el subtipo debe cumplir. Estos se detallan en una terminología similar a la del diseño por metodología de contrato, lo que lleva a algunas restricciones sobre cómo los contratos pueden interactuar con la herencia:

  • Las condiciones previas no pueden fortalecerse en un subtipo.
  • Las condiciones posteriores no pueden debilitarse en un subtipo.
  • Las invariantes del supertipo deben conservarse en un subtipo.
  • Restricción del historial (la "regla del historial"). Los objetos se consideran modificables solo a través de sus métodos (encapsulación). Dado que los subtipos pueden introducir métodos que no están presentes en el supertipo, la introducción de estos métodos puede permitir cambios de estado en el subtipo que no están permitidos en el supertipo. La restricción de la historia lo prohíbe. Fue el nuevo elemento introducido por Liskov y Wing. Una violación de esta restricción puede ejemplificarse definiendo un MutablePoint como un subtipo de un ImmutablePoint. Esto es una violación de la restricción del historial, porque en el historial del punto Inmutable, el estado es siempre el mismo después de la creación, por lo que no puede incluir el historial de un MutablePoint en general. Sin embargo, los campos agregados al subtipo pueden modificarse de manera segura porque no son observables a través de los métodos de supertipo.

Personalmente, me resulta más fácil simplemente recordar esto: si estoy mirando un parámetro en un método que tiene el tipo A, ¿alguien que pase un subtipo B me causaría alguna sorpresa? Si lo hicieran, entonces hay una violación de LSP.

¿Lanzar una excepción es una sorpresa? Realmente no. Es algo que puede suceder en cualquier momento, ya sea que llame al método de envío en OrderState o Granted o Shipped. Así que tengo que dar cuenta de ello y no es realmente una violación de LSP.

Dicho esto , creo que hay mejores formas de manejar esta situación. Si escribiera esto en C #, usaría interfaces y verificaría la implementación de una interfaz antes de llamar al método. Por ejemplo, si el OrderState actual no implementa IShippable, no llame a un método de envío.

Pero entonces tampoco usaría el patrón de Estado para esta situación particular. El patrón de estado es mucho más apropiado para el estado de una aplicación que para el estado de un objeto de dominio como este.

En pocas palabras, este es un ejemplo mal diseñado del Patrón de Estado y no es una forma particularmente buena de manejar el estado de una orden. Pero podría decirse que no infringe LSP. Y el patrón del Estado, en sí mismo, ciertamente no lo hace.

pdr
fuente
Me parece que estás confundiendo LSP con el Principio de menor sorpresa / asombro, creo que LSP tiene límites más claros donde esto los está violando, aunque estás aplicando el POLA más subjetivo que se basa en expectativas que son subjetivas ; es decir, cada persona espera algo diferente a la siguiente. El LSP se basa en garantías contractuales y bien definidas otorgadas por el proveedor, no en expectativas evaluadas subjetivamente por el consumidor.
Jimmy Hoffa
@JimmyHoffa: Tienes razón, y es por eso que expliqué el significado exacto antes de declarar una forma más simple de recordar LSP. Sin embargo, si hay una pregunta en mi mente sobre qué significa "sorpresa", entonces volveré y comprobaré las reglas exactas de LSP (¿realmente puede recordarlas a voluntad?). Las excepciones que reemplazan la funcionalidad (o viceversa) es difícil porque no viola ninguna cláusula específica anterior, lo que probablemente sea una pista de que debe manejarse de una manera completamente diferente.
pdr
1
Una excepción es una condición esperada definida en el contrato en mi mente, piense en un NetworkSocket, puede tener una excepción esperada en el envío si no está abierto, si su implementación no arroja esa excepción para un socket cerrado que está violando LSP , si el contrato establece lo contrario y su subtipo arroja la excepción, entonces está violando LSP. Así es más o menos cómo clasifico las excepciones en LSP. En cuanto a recordar las reglas; Podría estar equivocado en esto y no dude en decirme eso, pero mi comprensión de LSP vs POLA se define en la última oración de mi comentario anterior.
Jimmy Hoffa
1
@JimmyHoffa: Nunca había considerado que POLA y LSP estuvieran conectados de forma remota (pienso en POLA en términos de UI) pero, ahora que lo has señalado, lo están. Sin embargo, no estoy seguro de que estén en conflicto, o que uno sea más subjetivo que el otro. ¿No es LSP una descripción temprana de POLA en términos de ingeniería de software? De la misma manera que me siento frustrado cuando Ctrl-C no es Copia (POLA), también me siento frustrado cuando un ImmutablePoint es mutable porque en realidad es una subclase de MutablePoint (LSP).
pdr
1
Consideraría una CircleWithFixedCenterButMutableRadiusviolación probable de LSP si el contrato del consumidor ImmutablePointespecifica que X.equals(Y), si los consumidores pueden sustituir libremente X por Y, y viceversa, y por lo tanto deben abstenerse de usar la clase de tal manera que dicha sustitución pueda causar problemas. El tipo podría ser capaz de definir legítimamente equalstal que todas las instancias se compararían como distintas, pero tal comportamiento probablemente limitaría su utilidad.
supercat
2

(Esto está escrito desde el punto de vista de C #, por lo que no hay excepciones marcadas).

Según el artículo de Wikipedia sobre LSP , una de las condiciones de LSP es:

Los métodos del subtipo no deberían lanzar nuevas excepciones, excepto cuando esas excepciones sean subtipos de excepciones lanzadas por los métodos del supertipo.

¿Cómo deberías entender eso? ¿Qué son exactamente las "excepciones lanzadas por los métodos del supertipo" cuando los métodos del supertipo son abstractos? Creo que esas son las excepciones que se documentan como las excepciones que pueden generar los métodos del supertipo.

Lo que esto significa es que si OrderState.Ship()se documenta como algo así como "arroja InvalidOperationExceptionsi estas operaciones no son compatibles con el estado actual", entonces creo que este diseño no rompe LSP. Por otro lado, si los métodos de supertipo no se documentan de esta manera, se viola el LSP.

Pero esto no significa que este sea un buen diseño, no debe usar excepciones para el flujo de control normal, que parece muy cercano. Además, en una aplicación real, es probable que desee saber si se puede realizar una operación antes de intentar hacerlo, por ejemplo, para desactivar el botón "Enviar" en la interfaz de usuario.

svick
fuente
En realidad, este es exactamente el punto que me hizo pensar. Si las subclases lanzan excepciones no definidas en el padre, entonces debe ser una violación de LSP.
Songo
Creo que en un idioma donde las excepciones no están marcadas, lanzarlas no es una violación de LSP. Es solo un concepto de un lenguaje en sí mismo, que en cualquier lugar en cualquier momento se puede lanzar cualquier excepción. Por lo tanto, cualquier código de cliente debe estar listo para eso.
Zapadlo