Digamos que tenemos una lista de entidades de tareas y un ProjectTask
subtipo. Las tareas se pueden cerrar en cualquier momento, excepto ProjectTasks
que no se pueden cerrar una vez que tienen el estado de Iniciado. La interfaz de usuario debe garantizar que la opción de cerrar un inicio ProjectTask
nunca esté disponible, pero existen algunas salvaguardas en el dominio:
public class Task
{
public Status Status { get; set; }
public virtual void Close()
{
Status = Status.Closed;
}
}
public class ProjectTask : Task
{
public override void Close()
{
if (Status == Status.Started)
throw new Exception("Cannot close a started Project Task");
base.Close();
}
}
Ahora, cuando se llama Close()
a una Tarea, existe la posibilidad de que la llamada falle si es ProjectTask
con el estado iniciado, cuando no lo sería si fuera una Tarea base. Pero estos son los requisitos comerciales. Debería fallar. ¿Se puede considerar esto como una violación del principio de sustitución de Liskov ?
design
object-oriented-design
solid
liskov-substitution
Paul T Davies
fuente
fuente
public Status Status { get; private set; }
:; de lo contrario, elClose()
método se puede solucionar.Task
no introduzcan incompatibilidades extrañas en el código polimórfico que solo conoceTask
es un gran problema. LSP no es un capricho, pero se introdujo precisamente para ayudar a la mantenibilidad en sistemas grandes.TaskCloser
proceso queclosesAllTasks(tasks)
. Este proceso obviamente no intenta atrapar excepciones; después de todo, no es parte del contrato explícito deTask.Close()
. Ahora presentasProjectTask
y de repenteTaskCloser
comienzas a lanzar excepciones (posiblemente no manejadas). ¡Este es un gran problema!Respuestas:
Sí, es una violación del LSP. El principio de sustitución de Liskov requiere que
Su ejemplo rompe el primer requisito al fortalecer una condición previa para llamar al
Close()
método.Puede solucionarlo llevando la precondición fortalecida al nivel superior de la jerarquía de herencia:
Al estipular que una llamada de
Close()
es válida solo en el estado en el que se realizanCanClose()
devoluciones,true
usted hace que la condición previa se apliqueTask
tanto a laProjectTask
reparación como a la violación del LSP:fuente
Close
haga la verificación y agregar una protecciónDoClose
sería una alternativa válida. Sin embargo, quería estar lo más cerca posible del ejemplo del OP; mejorarlo es una pregunta separada.Si. Esto viola el LSP.
Mi sugerencia es agregar un
CanClose
método / propiedad a la tarea base, para que cualquier tarea pueda determinar si la tarea en este estado se puede cerrar. También puede proporcionar una razón por la cual. Y eliminar el virtual deClose
.Basado en mi comentario:
fuente
El principio de sustitución de Liskov establece que una clase base debe ser reemplazable por cualquiera de sus subclases sin alterar ninguna de las propiedades deseables del programa. Dado que solo
ProjectTask
plantea una excepción cuando se cierra, un programa tendría que cambiarse para adaptarse a eso, debeProjectTask
utilizarse en sustitución deTask
. Entonces es una violación.Pero si modifica
Task
indicando en su firma que puede generar una excepción cuando se cierra, entonces no estaría violando el principio.fuente
Una violación de LSP requiere tres partes. El Tipo T, el Subtipo S y el programa P que usa T pero recibe una instancia de S.
Su pregunta ha proporcionado T (Tarea) y S (ProjectTask), pero no P. Por lo tanto, su pregunta está incompleta y la respuesta está calificada: si existe una P que no espera una excepción, entonces, para esa P, tiene un LSP violación. Si cada P espera una excepción, entonces no hay violación de LSP.
Sin embargo, hacer una SRP violación. El hecho de que el estado de una tarea se puede cambiar, y la política de que ciertas tareas en ciertos estados deberían no ser cambiados a otros estados, son dos responsabilidades muy diferentes.
Estas dos responsabilidades cambian por diferentes razones y, por lo tanto, deben estar en clases separadas. Las tareas deben manejar el hecho de ser una tarea y los datos asociados con una tarea. TaskStatePolicy debe manejar la forma en que las tareas pasan de un estado a otro en una aplicación determinada.
fuente
OpenTaskException
(pista, pista) y cada P espera una excepción , ¿qué dice eso sobre el código para la interfaz, no la implementación? De que estoy hablando No lo sé. Estoy emocionado porque estoy comentando una respuesta de Unca 'Bob.Esto puede o no ser una violación del LSP.
Seriamente. Escúchame.
Si sigue el LSP, los objetos de tipo
ProjectTask
deben comportarse comoTask
se espera que se comporten los objetos de tipo .El problema con su código es que no ha documentado cómo
Task
se espera que se comporten los objetos de tipo . Tiene un código escrito, pero no tiene contratos. Agregaré un contrato paraTask.Close
. Dependiendo del contrato que agregue, el código paraProjectTask.Close
o no sigue el LSP.Dado el siguiente contrato para Task.Close, el código para
ProjectTask.Close
no sigue el LSP:Dado el siguiente contrato para Task.Close, el código para
ProjectTask.Close
no seguir el camino LSP:Los métodos que pueden anularse deben documentarse de dos maneras:
El "Comportamiento" documenta en qué puede confiar un cliente que sabe que el objeto receptor es un
Task
, pero no sabe de qué clase es una instancia directa. También le dice a los diseñadores de subclases qué anulaciones son razonables y cuáles no.El "comportamiento predeterminado" documenta en qué puede confiar un cliente que sabe que el objeto receptor es una instancia directa de
Task
(es decir, lo que obtienes si lo usasnew Task()
. También le dice a los diseñadores de subclases qué comportamiento se heredará si no lo hacen anular el métodoAhora deben tener las siguientes relaciones:
fuente
Close
does throw. Entonces, la firma declara que se puede lanzar una excepción , no dice que no se haga. Java hace un mejor trabajo en este sentido. Aun así, si declara que un método puede declarar una excepción, debe documentar las circunstancias bajo las cuales puede (o lo hará). Así que sostengo que para estar seguros de si se viola LSP, necesitamos documentación más allá de la firma.if (false) throw new Exception("cannot start")
a la clase base. El compilador lo eliminará, y aún así el código contiene lo que se necesita. Por cierto. todavía tenemos una violación de LSP con estas soluciones, porque la condición previa todavía se fortalece ...No es una violación del Principio de sustitución de Liskov.
El principio de sustitución de Liskov dice:
La razón por la cual su implementación del subtipo no es una violación del Principio de sustitución de Liskov es bastante simple: no se puede probar nada sobre lo que
Task::Close()
realmente hace. Por supuesto,ProjectTask::Close()
una excepción cuandoStatus == Status.Started
, pero por lo que podríaStatus = Status.Closed
enTask::Close()
.fuente
Sí, es una violación.
Te sugiero que tengas tu jerarquía al revés. Si no todos
Task
se pueden cerrar, entoncesclose()
no perteneceTask
. Quizás desee una interfazCloseableTask
que todos los que noProjectTasks
puedan implementar.fuente
Task
no se implementa,CloseableTask
entonces están haciendo un lanzamiento inseguro en algún lugar para siquiera llamarClose()
.Además de ser un problema de LSP, parece que está usando excepciones para controlar el flujo del programa (tengo que suponer que capturas esta excepción trivial en algún lugar y haces un flujo personalizado en lugar de dejar que bloquee tu aplicación).
Parece que este es un buen lugar para implementar el patrón de estado para TaskState y dejar que los objetos de estado administren las transiciones válidas.
fuente
Aquí me falta algo importante relacionado con el LSP y el diseño por contrato: en las condiciones previas, es la persona que llama cuya responsabilidad es asegurarse de que se cumplan las condiciones previas. El código llamado, en teoría DbC, no debería verificar la condición previa. El contrato debe especificar cuándo se puede cerrar una tarea (por ejemplo, CanClose devuelve True) y luego el código de llamada debe garantizar que se cumpla la condición previa antes de llamar a Close ().
fuente
ProjectTask
. Esta es una condición posterior (dice lo que sucede después de que se llama al método) y cumplirlo es responsabilidad del código llamado.Sí, es una clara violación de LSP.
Algunas personas argumentan aquí que hacer explícito en la clase base que las subclases pueden lanzar excepciones haría esto aceptable, pero no creo que sea cierto. No importa lo que documente en la clase base o el nivel de abstracción al que mueva el código, las condiciones previas aún se fortalecerán en la subclase, porque agrega la parte "No se puede cerrar una tarea de proyecto iniciada". Esto no es algo que pueda resolver con una solución alternativa, necesita un modelo diferente, que no viole el LSP (o tenemos que aflojar la restricción "no se pueden fortalecer las condiciones previas").
Puede probar el patrón de decorador si desea evitar la violación de LSP en este caso. Podría funcionar, no lo sé.
fuente