valuacion sustitucion solid segregation segregación principios principio inversión interfaces ejemplo economia devexperto dependencias dependencia oop definition solid-principles design-principles lsp

oop - solid - principio de sustitucion valuacion



¿Cuál es un ejemplo del principio de sustitución de Liskov? (26)

He escuchado que el principio de sustitución de Liskov (LSP) es un principio fundamental del diseño orientado a objetos. ¿Qué es y cuáles son algunos ejemplos de su uso?


La capacidad de sustitución es un principio en la programación orientada a objetos que indica que, en un programa de computadora, si S es un subtipo de T, los objetos de tipo T pueden reemplazarse con objetos de tipo S

Hagamos un ejemplo simple en Java:

Mal ejemplo

public class Bird{ public void fly(){} } public class Duck extends Bird{}

El pato puede volar debido a su ave, pero ¿qué pasa con esto?

public class Ostrich extends Bird{}

El avestruz es un ave, pero no puede volar, la clase de avestruz es un subtipo de la clase Aves, pero no puede usar el método de volar, eso significa que estamos rompiendo el principio LSP.

Buen ejemplo

public class Bird{ } public class FlyingBirds extends Bird{ public void fly(){} } public class Duck extends FlyingBirds{} public class Ostrich extends Bird{}


Las funciones que usan punteros o referencias a clases base deben poder usar objetos de clases derivadas sin saberlo.

Cuando leí por primera vez acerca de LSP, asumí que esto estaba pensado en un sentido muy estricto, esencialmente equiparándolo a la implementación de la interfaz y la conversión de tipos seguros. Lo que significaría que el LSP está asegurado o no por el lenguaje en sí. Por ejemplo, en este sentido estricto, ThreeDBoard es ciertamente sustituible por Board, en lo que respecta al compilador.

Después de leer más sobre el concepto, encontré que el LSP generalmente se interpreta más ampliamente que eso.

En resumen, lo que significa que el código del cliente "sepa" que el objeto detrás del puntero es de un tipo derivado en lugar del tipo de puntero no está restringido a la seguridad de tipos. La adherencia a LSP también se puede probar a través del sondeo del comportamiento real de los objetos. Es decir, examinar el impacto de los argumentos de estado y método de un objeto en los resultados de las llamadas de método, o los tipos de excepciones lanzadas desde el objeto.

Volviendo al ejemplo nuevamente, en teoría, los métodos de la Junta pueden funcionar bien en ThreeDBoard. Sin embargo, en la práctica, será muy difícil evitar las diferencias de comportamiento que el cliente puede no manejar adecuadamente, sin obstaculizar la funcionalidad que pretende agregar ThreeDBoard.

Con este conocimiento en la mano, evaluar la adherencia al LSP puede ser una gran herramienta para determinar cuándo la composición es el mecanismo más apropiado para extender la funcionalidad existente, en lugar de la herencia.


Principio de Sustitución de Liskov (LSP)

En todo momento diseñamos un módulo de programa y creamos algunas jerarquías de clase. Luego extendemos algunas clases creando algunas clases derivadas.

Debemos asegurarnos de que las nuevas clases derivadas solo se extiendan sin reemplazar la funcionalidad de las clases antiguas. De lo contrario, las nuevas clases pueden producir efectos no deseados cuando se usan en módulos de programa existentes.

El Principio de Sustitución de Liskov establece que si un módulo de programa está utilizando una clase Base, la referencia a la clase Base puede reemplazarse por una clase Derivada sin afectar la funcionalidad del módulo del programa.

Ejemplo:

A continuación se muestra el ejemplo clásico para el cual se viola el principio de sustitución de Liskov. En el ejemplo, se utilizan 2 clases: Rectángulo y Cuadrado. Supongamos que el objeto Rectángulo se utiliza en algún lugar de la aplicación. Ampliamos la aplicación y añadimos la clase Cuadrado. La clase cuadrada es devuelta por un patrón de fábrica, basado en algunas condiciones y no sabemos exactamente qué tipo de objeto será devuelto. Pero sabemos que es un rectángulo. Obtenemos el objeto rectángulo, establecemos el ancho en 5 y la altura en 10 y obtenemos el área. Para un rectángulo con ancho 5 y altura 10, el área debe ser 50. En su lugar, el resultado será 100

// Violation of Likov''s Substitution Principle class Rectangle { protected int m_width; protected int m_height; public void setWidth(int width) { m_width = width; } public void setHeight(int height) { m_height = height; } public int getWidth() { return m_width; } public int getHeight() { return m_height; } public int getArea() { return m_width * m_height; } } class Square extends Rectangle { public void setWidth(int width) { m_width = width; m_height = width; } public void setHeight(int height) { m_width = height; m_height = height; } } class LspTest { private static Rectangle getNewRectangle() { // it can be an object returned by some factory ... return new Square(); } public static void main(String args[]) { Rectangle r = LspTest.getNewRectangle(); r.setWidth(5); r.setHeight(10); // user knows that r it''s a rectangle. // It assumes that he''s able to set the width and height as for the base // class System.out.println(r.getArea()); // now he''s surprised to see that the area is 100 instead of 50. } }

Conclusión:

Este principio es solo una extensión del principio de cierre abierto y significa que debemos asegurarnos de que las nuevas clases derivadas extiendan las clases base sin cambiar su comportamiento.

Ver también: Principio de cierre abierto.

Algunos conceptos similares para una mejor estructura: convención sobre configuración


Curiosamente, nadie ha publicado el paper original que describe lsp. No es una lectura fácil como la de Robert Martin, pero vale la pena.


El LSP es necesario cuando algún código cree que está llamando a los métodos de un tipo T , y puede invocar sin saberlo los métodos de un tipo S , donde S extends T (es decir, S hereda, deriva de, o es un subtipo de, el supertipo T ) .

Por ejemplo, esto ocurre cuando se llama (es decir, se invoca) una función con un parámetro de entrada de tipo T con un valor de argumento de tipo S O, cuando a un identificador de tipo T , se le asigna un valor de tipo S

val id : T = new S() // id thinks it''s a T, but is a S

LSP requiere que las expectativas (es decir, invariantes) para los métodos de tipo T (por ejemplo, Rectangle ), no se violen cuando se llaman los métodos de tipo S (por ejemplo, Square ).

val rect : Rectangle = new Square(5) // thinks it''s a Rectangle, but is a Square val rect2 : Rectangle = rect.setWidth(10) // height is 10, LSP violation

Incluso un tipo con campos inmutables todavía tiene invariantes, por ejemplo, los emisores Rectangle inmutables esperan que las dimensiones se modifiquen independientemente, pero los emisores Square inmutables violan esta expectativa.

class Rectangle( val width : Int, val height : Int ) { def setWidth( w : Int ) = new Rectangle(w, height) def setHeight( h : Int ) = new Rectangle(width, h) } class Square( val side : Int ) extends Rectangle(side, side) { override def setWidth( s : Int ) = new Square(s) override def setHeight( s : Int ) = new Square(s) }

LSP requiere que cada método del subtipo S debe tener parámetros de entrada contravariantes y una salida covariante.

Contravariante significa que la varianza es contraria a la dirección de la herencia, es decir, el tipo Si , de cada parámetro de entrada de cada método del subtipo S , debe ser el mismo o un supertipo del tipo Ti del parámetro de entrada correspondiente del método correspondiente del supertipo T

La covarianza significa que la varianza es en la misma dirección de la herencia, es decir, el tipo So , de la salida de cada método del subtipo S , debe ser el mismo o un subtipo del tipo To de la salida correspondiente del método correspondiente de la supertipo T

Esto se debe a que si la persona que llama cree que tiene un tipo T , cree que está llamando a un método de T , entonces proporciona argumentos de tipo Ti y asigna la salida al tipo To . Cuando en realidad está llamando al método correspondiente de S , entonces cada argumento de entrada Ti se asigna a un parámetro de entrada Si , y la salida So se asigna al tipo To . Por lo tanto, si Si no fuera contravariable en Ti , entonces un subtipo Xi , que no sería un subtipo de Si podría asignarse a Ti .

Además, para los idiomas (por ejemplo, Scala o Ceilán) que tienen anotaciones de varianza del sitio de definición en los parámetros de polimorfismo de tipo (es decir, genéricos), la co- o contra-dirección de la anotación de varianza para cada parámetro de tipo del tipo T debe ser opposite o igual dirección respectivamente a cada parámetro de entrada o salida (de cada método de T ) que tenga el tipo del parámetro de tipo.

Además, para cada parámetro de entrada o salida que tiene un tipo de función, se invierte la dirección de varianza requerida. Esta regla se aplica recursivamente.

El subtipo es apropiado donde los invariantes pueden ser enumerados.

Hay mucha investigación en curso sobre cómo modelar invariantes, de modo que el compilador los imponga.

Typestate (vea la página 3) declara y aplica invariantes estatales ortogonales al tipo. Alternativamente, las invariantes se pueden imponer convirtiendo aserciones a tipos . Por ejemplo, para afirmar que un archivo está abierto antes de cerrarlo, entonces File.open () podría devolver un tipo de OpenFile, que contiene un método close () que no está disponible en File. Una API de tic-tac-toe puede ser otro ejemplo de emplear la escritura para imponer invariantes en tiempo de compilación. El sistema de tipos puede incluso ser Turing-complete, por ejemplo, Scala . Los lenguajes dependientes y los probadores de teoremas formalizan los modelos de tipificación de orden superior.

Debido a la necesidad de que la semántica se abstraiga sobre la extensión , espero que emplear la tipificación para modelar invariantes, es decir, semántica de denotación de orden superior unificada, sea superior a la Typestate. ''Extensión'' significa la composición ilimitada y permutada del desarrollo modular no coordinado. Porque me parece que es la antítesis de la unificación y, por lo tanto, los grados de libertad, tener dos modelos mutuamente dependientes (por ejemplo, tipos y Typestate) para expresar la semántica compartida, que no se pueden unificar entre sí para una composición extensible. . Por ejemplo, la extensión similar a un problema de expresión se unificó en los subtipos, la sobrecarga de funciones y los dominios de escritura paramétrica.

Mi posición teórica es que para que el conocimiento exista (consulte la sección “La centralización es ciega e inadecuada”), nunca habrá un modelo general que pueda imponer el 100% de la cobertura de todas las posibles invariantes en un lenguaje de computadora completo de Turing. Para que exista el conocimiento, existen muchas posibilidades inesperadas, es decir, el desorden y la entropía siempre deben aumentar. Esta es la fuerza entrópica. Para probar todos los cálculos posibles de una extensión potencial, es calcular a priori todas las extensiones posibles.

Esta es la razón por la que existe el Teorema de Halting, es decir, es indecidible si todos los programas posibles en un lenguaje de programación completo de Turing terminan. Se puede probar que algunos programas específicos terminan (uno en el que se han definido y computado todas las posibilidades). Pero es imposible probar que toda la extensión posible de ese programa termina, a menos que las posibilidades de extensión de ese programa no estén completas Turing (por ejemplo, a través de la tipificación dependiente). Dado que el requisito fundamental para la integridad de Turing es la recursión ilimitada , es intuitivo comprender cómo los teoremas de incompletitud de Gödel y la paradoja de Russell se aplican a la extensión.

Una interpretación de estos teoremas los incorpora en una comprensión conceptual generalizada de la fuerza entrópica:

  • Teoremas de incompletitud de Gödel : cualquier teoría formal, en la que se puedan probar todas las verdades aritméticas, es inconsistente.
  • La paradoja de Russell : cada regla de membresía para un conjunto que puede contener un conjunto, enumera el tipo específico de cada miembro o se contiene a sí misma. Por lo tanto, los conjuntos no se pueden extender o son recursión ilimitada. Por ejemplo, el conjunto de todo lo que no es una tetera, se incluye a sí mismo, que se incluye a sí mismo, que se incluye a sí mismo, etc. Por lo tanto, una regla es inconsistente si (puede contener un conjunto y) no enumera los tipos específicos (es decir, permite todos los tipos no especificados) y no permite la extensión sin límites. Este es el conjunto de conjuntos que no son miembros de sí mismos. Esta incapacidad de ser a la vez coherente y completamente enumerada en toda la extensión posible, es el teorema de incompletud de Gödel.
  • Principio de la subestación de Liskov : generalmente es un problema indecidible si cualquier conjunto es el subconjunto de otro, es decir, la herencia es generalmente indecidible.
  • Referencia de Linsky : es indecidible lo que es el cálculo de algo, cuando se describe o percibe, es decir, la percepción (realidad) no tiene un punto de referencia absoluto.
  • Teorema de Coase : no hay un punto de referencia externo, por lo tanto, cualquier barrera a las posibilidades externas ilimitadas fallará.
  • Segunda ley de la termodinámica : todo el universo (un sistema cerrado, es decir, todo) tiende al máximo desorden, es decir, a las máximas posibilidades independientes.

El LSP es una regla sobre el contrato de las clases: si una clase base satisface un contrato, entonces las clases derivadas del LSP también deben cumplir ese contrato.

En pseudo-python

class Base: def Foo(self, arg): # *... do stuff* class Derived(Base): def Foo(self, arg): # *... do stuff*

satisface el LSP si cada vez que llama a Foo en un objeto Derivado, da exactamente los mismos resultados que llamar a Foo en un objeto Base, siempre que arg sea el mismo.


El principio de sustitución de Liskov (LSP, lsp ) es un concepto en la programación orientada a objetos que establece:

Las funciones que usan punteros o referencias a clases base deben poder usar objetos de clases derivadas sin saberlo.

En su corazón, el LSP se trata de interfaces y contratos, así como de cómo decidir cuándo extender una clase en lugar de usar otra estrategia, como la composición, para lograr su objetivo.

La forma más efectiva que he visto para ilustrar este punto fue en Head First OOA & D. Presentan un escenario en el que eres un desarrollador en un proyecto para construir un marco para juegos de estrategia.

Presentan una clase que representa una tabla que se ve así:

Todos los métodos toman las coordenadas X e Y como parámetros para ubicar la posición del mosaico en la matriz bidimensional de Tiles . Esto permitirá que un desarrollador de juegos administre unidades en el tablero durante el transcurso del juego.

El libro continúa para cambiar los requisitos para decir que el marco del juego también debe ser compatible con los tableros de juegos 3D para adaptarse a los juegos que tienen vuelo. Así que se introduce una clase ThreeDBoard que amplía el Board .

A primera vista esto parece una buena decisión. Board proporciona las propiedades de ThreeDBoard y Width y ThreeDBoard proporciona el eje Z.

Donde se descompone es cuando observas a todos los otros miembros heredados de la Board . Los métodos para AddUnit , AddUnit , GetTile , GetUnits toman todos los parámetros X e Y en la clase Board , pero ThreeDBoard necesita un parámetro Z.

Así que debes implementar esos métodos nuevamente con un parámetro Z. El parámetro Z no tiene ningún contexto para la clase Board y los métodos heredados de la clase Board pierden su significado. Una unidad de código que intente utilizar la clase ThreeDBoard como su clase base Board no tendrá mucha suerte.

Tal vez deberíamos encontrar otro enfoque. En lugar de extender el Board , ThreeDBoard debe estar compuesto de objetos del Board . Un objeto de Board por unidad del eje Z.

Esto nos permite usar buenos principios orientados a objetos como la encapsulación y la reutilización y no viola el LSP.


Hay una lista de verificación para determinar si está violando o no a Liskov.

  • Si viola uno de los siguientes elementos -> viola Liskov.
  • Si no violas ninguna -> no puedo concluir nada.

Lista de verificación:

  • No se deben lanzar excepciones nuevas en la clase derivada : si su clase base arrojó ArgumentNullException, entonces sus subclases solo pudieron lanzar excepciones de tipo ArgumentNullException o cualquier excepción derivada de ArgumentNullException. Lanzar la excepción IndexOutOfRangeException es una violación de Liskov.
  • Las condiciones previas no pueden fortalecerse : suponga que su clase base trabaja con un miembro int. Ahora su subtipo requiere que int sea positivo. Esto fortalece las condiciones previas, y ahora se rompe cualquier código que funcionó perfectamente bien antes con intenciones negativas.
  • Las condiciones posteriores no se pueden debilitar : suponga que su clase base requirió que todas las conexiones a la base de datos se cerraran antes de que se devuelva el método. En su subclase, usted anuló ese método y se dejó abierta la conexión para su posterior reutilización. Has debilitado las post-condiciones de ese método.
  • Las invariantes deben ser preservadas : la restricción más difícil y dolorosa de cumplir. Las invariantes están algún tiempo ocultas en la clase base y la única forma de revelarlas es leer el código de la clase base. Básicamente, tienes que estar seguro de que cuando anules un método, cualquier cosa que no se pueda cambiar debe permanecer sin cambios después de que se ejecute el método anulado. Lo mejor que se me ocurre es imponer estas restricciones invariables en la clase base, pero eso no sería fácil.
  • Restricción de historial : al anular un método, no se le permite modificar una propiedad no modificable en la clase base. Eche un vistazo a estos códigos y puede ver que el Nombre se define como no modificable (conjunto privado) pero SubType introduce un nuevo método que permite modificarlo (a través de la reflexión):

    public class SuperType { public string Name { get; private set; } public SuperType(string name, int age) { Name = name; Age = age; } } public class SubType : SuperType { public void ChangeName(string newName) { var propertyType = base.GetType().GetProperty("Name").SetValue(this, newName); } }

Hay otros 2 elementos: la contraparte de los argumentos de los métodos y la covarianza de los tipos de devolución . Pero no es posible en C # (soy un desarrollador de C #), así que no me importan.

Referencia:


LSP se refiere a los invariantes.

El ejemplo clásico viene dado por la siguiente declaración de pseudo código (implementaciones omitidas):

class Rectangle { int getHeight() void setHeight(int value) int getWidth() void setWidth(int value) } class Square : Rectangle { }

Ahora tenemos un problema aunque la interfaz coincide. La razón es que hemos violado invariantes derivados de la definición matemática de cuadrados y rectángulos. De la forma en que funcionan los captadores y definidores, un Rectangle debe satisfacer los siguientes invariantes:

void invariant(Rectangle r) { r.setHeight(200) r.setWidth(100) assert(r.getHeight() == 200 and r.getWidth() == 100) }

Sin embargo, este invariante debe ser violado por una implementación correcta de Square , por lo tanto no es un sustituto válido de Rectangle .


Robert Martin tiene un excelente artículo sobre el principio de sustitución de Liskov . Discute formas sutiles y no tan sutiles en las cuales el principio puede ser violado.

Algunas partes relevantes del documento (tenga en cuenta que el segundo ejemplo está muy condensado):

Un ejemplo simple de una violación de LSP

Una de las violaciones más evidentes de este principio es el uso de la información de tipo de tiempo de ejecución de C ++ (RTTI) para seleccionar una función basada en el tipo de un objeto. es decir:

void DrawShape(const Shape& s) { if (typeid(s) == typeid(Square)) DrawSquare(static_cast<Square&>(s)); else if (typeid(s) == typeid(Circle)) DrawCircle(static_cast<Circle&>(s)); }

Claramente la función DrawShape está mal formada. Debe conocer todas las derivaciones posibles de la clase Shape , y debe cambiarse cada vez que se creen nuevas derivadas de Shape . De hecho, muchos ven la estructura de esta función como un anatema para el diseño orientado a objetos.

Cuadrado y rectángulo, una violación más sutil.

Sin embargo, hay otras formas, mucho más sutiles, de violar el LSP. Considere una aplicación que usa la clase Rectangle como se describe a continuación:

class Rectangle { public: void SetWidth(double w) {itsWidth=w;} void SetHeight(double h) {itsHeight=w;} double GetHeight() const {return itsHeight;} double GetWidth() const {return itsWidth;} private: double itsWidth; double itsHeight; };

[...] Imagina que un día los usuarios exigen la capacidad de manipular cuadrados además de rectángulos. [...]

Claramente, un cuadrado es un rectángulo para todos los propósitos y propósitos normales. Dado que la relación ISA se mantiene, es lógico modelar la clase Square como derivada de Rectangle . [...]

Square heredará las funciones SetWidth y SetHeight . Estas funciones son totalmente inadecuadas para un Square , ya que el ancho y la altura de un cuadrado son idénticos. Esto debería ser una pista importante de que hay un problema con el diseño. Sin embargo, hay una manera de evitar el problema. Podríamos anular SetWidth y SetHeight [...]

Pero considere la siguiente función:

void f(Rectangle& r) { r.SetWidth(32); // calls Rectangle::SetWidth }

Si pasamos una referencia a un objeto Square en esta función, el objeto Square se corromperá porque no se cambiará la altura. Esta es una clara violación de LSP. La función no funciona para derivados de sus argumentos.

[...]


Supongo que todos cubren lo que el LSP es técnicamente: básicamente desea poder abstraerse de los detalles de subtipo y usar supertipos de forma segura.

Así que Liskov tiene 3 reglas subyacentes:

  1. Regla de firma: Debe haber una implementación válida de cada operación del supertipo en el subtipo sintácticamente. Algo que un compilador podrá comprobar por ti. Hay una pequeña regla sobre lanzar menos excepciones y ser al menos tan accesible como los métodos de supertipo.

  2. Regla de métodos: la implementación de esas operaciones es semánticamente sólida.

    • Condiciones previas más débiles: las funciones de subtipo deben tomar al menos lo que el supertipo tomó como entrada, si no más.
    • Condiciones posteriores más fuertes: deben producir un subconjunto de la salida que producen los métodos supertipo.
  3. Regla de propiedades: Esto va más allá de las llamadas a funciones individuales.

    • Invariantes: las cosas que siempre son ciertas deben seguir siendo ciertas. P.ej. El tamaño de un Set nunca es negativo.
    • Propiedades evolutivas: por lo general, algo relacionado con la inmutabilidad o el tipo de estados en que puede estar el objeto. O tal vez el objeto solo crece y nunca se contrae, por lo que los métodos de subtipo no deberían hacerlo.

Todas estas propiedades deben conservarse y la funcionalidad de subtipo adicional no debe violar las propiedades de supertipo.

Si se cuidan estas tres cosas, se ha abstraído de las cosas subyacentes y está escribiendo código acoplado libremente.

Fuente: Desarrollo de programas en Java - Barbara Liskov


Un ejemplo importante del uso de LSP es en las pruebas de software .

Si tengo una clase A que es una subclase de B compatible con LSP, entonces puedo reutilizar el conjunto de pruebas de B para probar A.

Para probar completamente la subclase A, probablemente deba agregar algunos casos de prueba más, pero como mínimo puedo reutilizar todos los casos de prueba de la superclase B.

Una forma de darse cuenta es a través de lo que McGregor denomina "Jerarquía paralela para pruebas": mi clase ATest heredará de BTest . Luego se necesita algún tipo de inyección para garantizar que el caso de prueba funcione con objetos de tipo A en lugar de de tipo B (un patrón de método de plantilla simple funcionará).

Tenga en cuenta que la reutilización de la suite de superpruebas para todas las implementaciones de subclases es, de hecho, una forma de probar que estas implementaciones de subclases son compatibles con LSP. Por lo tanto, también se puede argumentar que se debe ejecutar el conjunto de pruebas de superclase en el contexto de cualquier subclase.

Vea también la respuesta a la pregunta " ¿Puedo implementar una serie de pruebas reutilizables para probar la implementación de una interfaz? "


Un gran ejemplo que ilustra el LSP (dado por Uncle Bob en un podcast que escuché recientemente) fue cómo a veces algo que suena bien en lenguaje natural no funciona bien en el código.

En matemáticas, un Square es un Rectangle . De hecho es una especialización de un rectángulo. El "es un" hace que quieras modelar esto con herencia. Sin embargo, si en el código que creó Square deriva de Rectangle , entonces un Square debería poder usarse en cualquier lugar donde espere un Rectangle . Esto hace para algún comportamiento extraño.

Imagina que SetHeight métodos SetWidth y SetHeight en tu clase base Rectangle ; Esto parece perfectamente lógico. Sin embargo, si su referencia Rectangle apunta a un Square , SetWidth y SetHeight no tienen sentido porque configurar uno cambiaría el otro para que coincida. En este caso, Square no supera la prueba de sustitución de Liskov con un Rectangle y la abstracción de tener Square heredado de un Rectangle es mala.

Ustedes deberían revisar los otros afiches motivacionales de Principios Sólidos .


¿Sería útil implementar ThreeDBoard en términos de una matriz de Junta?

Quizás desee tratar las rebanadas de ThreeDBoard en varios planos como una Junta. En ese caso, es posible que desee abstraer una interfaz (o clase abstracta) para que la Junta permita múltiples implementaciones.

En términos de interfaz externa, es posible que desee factorizar una interfaz de placa para TwoDBoard y ThreeDBoard (aunque ninguno de los métodos anteriores encaja).


En pocas palabras, dejemos rectángulos y cuadrados cuadrados, ejemplo práctico al extender una clase primaria, debe PRESERVAR la API principal exacta o EXTENDERLO.

Digamos que tienes un ItemsRepository base .

class ItemsRepository { /** * @return int Returns number of deleted rows */ public function delete() { // perform a delete query $numberOfDeletedRows = 10; return $numberOfDeletedRows; } }

Y una subclase que lo amplía:

class BadlyExtendedItemsRepository extends ItemsRepository { /** * @return void Was suppose to return an INT like parent, but did not, breaks LSP */ public function delete() { // perform a delete query $numberOfDeletedRows = 10; // we broke the behaviour of the parent class return; } }

Entonces podría tener un Cliente trabajando con la API de Base ItemsRepository y confiando en él.

/** * Class ItemsService is a client for public ItemsRepository "API" (the public delete method). * * Technically, I am able to pass into a constructor a sub-class of the ItemsRepository * but if the sub-class won''t abide the base class API, the client will get broken. */ class ItemsService { /** * @var ItemsRepository */ private $itemsRepository; /** * @param ItemsRepository $itemsRepository */ public function __construct(ItemsRepository $itemsRepository) { $this->itemsRepository = $itemsRepository; } /** * !!! Notice how this is suppose to return an int. My clients expect it based on the * ItemsRepository API in the constructor !!! * * @return int */ public function delete() { return $this->itemsRepository->delete(); } }

El LSP se rompe cuando se sustituye la clase principal por una subclase se rompe el contrato de la API .

class ItemsController { /** * Valid delete action when using the base class. */ public function validDeleteAction() { $itemsService = new ItemsService(new ItemsRepository()); $numberOfDeletedItems = $itemsService->delete(); // $numberOfDeletedItems is an INT :) } /** * Invalid delete action when using a subclass. */ public function brokenDeleteAction() { $itemsService = new ItemsService(new BadlyExtendedItemsRepository()); $numberOfDeletedItems = $itemsService->delete(); // $numberOfDeletedItems is a NULL :( } }


Digamos que usamos un rectángulo en nuestro código

r = new Rectangle(); // ... r.setDimensions(1,2); r.fill(colors.red()); canvas.draw(r);

En nuestra clase de geometría aprendimos que un cuadrado es un tipo especial de rectángulo porque su ancho es el mismo que su altura. Hagamos una Squareclase también basada en esta información:

class Square extends Rectangle { setDimensions(width, height){ assert(width == height); super.setDimensions(width, height); } }

Si reemplazamos el Rectanglecon Squareen nuestro primer código, entonces se romperá:

r = new Square(); // ... r.setDimensions(1,2); // assertion width == height failed r.fill(colors.red()); canvas.draw(r);

Esto es porque el Squaretiene una nueva condición de que no tenemos en la Rectangleclase: width == height. De acuerdo con el LSP, las Rectangleinstancias deben ser sustituibles con Rectangleinstancias de subclases. Esto se debe a que estas instancias pasan la verificación de tipo de las Rectangleinstancias y, por lo tanto, causarán errores inesperados en su código.

Este fue un ejemplo de la parte de "las condiciones previas no se pueden reforzar en un subtipo" en el artículo de wiki . Entonces, para resumir, violar el LSP probablemente cause errores en su código en algún momento.


En una oración muy simple, podemos decir:

La clase infantil no debe violar sus características de clase base. Debe ser capaz de hacerlo. Podemos decir que es lo mismo que el subtipo.


Esta formulación del LSP es demasiado fuerte:

Si para cada objeto o1 de tipo S hay un objeto o2 de tipo T tal que para todos los programas P de fi nidos en términos de T, el comportamiento de P no cambia cuando o1 se sustituye por o2, entonces S es un subtipo de T.

Lo que básicamente significa que S es otra implementación completamente encapsulada de exactamente lo mismo que T. Y podría ser audaz y decidir que el rendimiento es parte del comportamiento de P ...

Así que, básicamente, cualquier uso de enlace tardío viola el LSP. El objetivo principal de OO es obtener un comportamiento diferente cuando sustituimos un objeto de un tipo por otro de otro tipo.

La formulación citada por wikipedia es mejor ya que la propiedad depende del contexto y no necesariamente incluye todo el comportamiento del programa.


Veo rectángulos y cuadrados en cada respuesta, y cómo violar el LSP.

Me gustaría mostrar cómo se puede adaptar el LSP a un ejemplo del mundo real:

<?php interface Database { public function selectQuery(string $sql): array; } class SQLiteDatabase implements Database { public function selectQuery(string $sql): array { // sqlite specific code return $result; } } class MySQLDatabase implements Database { public function selectQuery(string $sql): array { // mysql specific code return $result; } }

Este diseño se ajusta al LSP porque el comportamiento permanece sin cambios independientemente de la implementación que decidamos utilizar.

Y sí, puede violar el LSP en esta configuración haciendo un simple cambio así:

<?php interface Database { public function selectQuery(string $sql): array; } class SQLiteDatabase implements Database { public function selectQuery(string $sql): array { // sqlite specific code return $result; } } class MySQLDatabase implements Database { public function selectQuery(string $sql): array { // mysql specific code return [''result'' => $result]; // This violates LSP ! } }

Ahora los subtipos no se pueden usar de la misma manera ya que ya no producen el mismo resultado.


Algún apéndice:
me pregunto por qué nadie escribió sobre las Condiciones Invariantes, las condiciones previas y las condiciones de publicación de la clase base que deben ser obedecidas por las clases derivadas. Para que una clase D derivada sea completamente sustituible por la clase B base, la clase D debe obedecer ciertas condiciones:

  • Las variantes derivadas de la clase base deben ser preservadas por la clase derivada
  • Las condiciones previas de la clase base no deben ser fortalecidas por la clase derivada
  • Las condiciones posteriores de la clase base no deben ser debilitadas por la clase derivada.

Por lo tanto, los derivados deben conocer las tres condiciones anteriores impuestas por la clase base. Por lo tanto, las reglas de subtipo son pre-decididas. Lo que significa que la relación ''IS A'' se debe obedecer solo cuando ciertas reglas son obedecidas por el subtipo. Estas reglas, en forma de invariantes, precodiciones y condiciones posteriores, deben decidirse mediante un " contrato de diseño " formal .

Más discusiones sobre esto disponibles en mi blog: principio de sustitución de Liskov


Aquí hay un extracto de este post que aclara las cosas muy bien:

[..] para comprender algunos principios, es importante darse cuenta de cuándo se ha violado. Esto es lo que haré ahora.

¿Qué significa la violación de este principio? Implica que un objeto no cumple el contrato impuesto por una abstracción expresada con una interfaz. En otras palabras, significa que usted identificó sus abstracciones incorrectas.

Considere el siguiente ejemplo:

interface Account { /** * Withdraw $money amount from this account. * * @param Money $money * @return mixed */ public function withdraw(Money $money); } class DefaultAccount implements Account { private $balance; public function withdraw(Money $money) { if (!$this->enoughMoney($money)) { return; } $this->balance->subtract($money); } }

¿Es esto una violación de LSP? Sí. Esto se debe a que el contrato de la cuenta nos dice que una cuenta se retiraría, pero no siempre es así. Entonces, ¿qué debo hacer para arreglarlo? Acabo de modificar el contrato:

interface Account { /** * Withdraw $money amount from this account if its balance is enough. * Otherwise do nothing. * * @param Money $money * @return mixed */ public function withdraw(Money $money); }

Voilà, ahora el contrato está satisfecho.

Esta violación sutil a menudo impone a un cliente la capacidad de distinguir la diferencia entre los objetos concretos empleados. Por ejemplo, dado el primer contrato de la Cuenta, podría ser como el siguiente:

class Client { public function go(Account $account, Money $money) { if ($account instanceof DefaultAccount && !$account->hasEnoughMoney($money)) { return; } $account->withdraw($money); } }

Y, esto viola automáticamente el principio abierto-cerrado [es decir, para el requisito de retiro de dinero. Porque nunca se sabe qué sucede si un objeto que viola el contrato no tiene suficiente dinero. Probablemente no devuelva nada, probablemente se lanzará una excepción. Por lo tanto, debe verificarlo, lo hasEnoughMoney()que no forma parte de una interfaz. Así que esta verificación forzada dependiente de la clase concreta es una violación de OCP].

Este punto también aborda una idea errónea que encuentro con bastante frecuencia sobre la violación de LSP. Dice que “si la conducta de un padre cambió en un niño, entonces, viola el LSP”. Sin embargo, no lo hace, siempre y cuando el niño no viole el contrato de su padre.


El PRINCIPIO DE SUSTITUCIÓN DE LISKOV (del libro de Mark Seemann) declara que deberíamos poder reemplazar una implementación de una interfaz con otra sin romper ni el cliente ni la implementación. Este es el principio que permite abordar los requisitos que ocurren en el futuro, incluso si podemos '' No los prevea hoy.

Si desenchufamos la computadora de la pared (Implementación), ni el tomacorriente de pared (Interface) ni la computadora (Cliente) se rompen (de hecho, si es una computadora portátil, incluso puede funcionar con sus baterías por un período de tiempo) . Sin embargo, con el software, un cliente a menudo espera que un servicio esté disponible. Si el servicio fue eliminado, obtenemos una NullReferenceException. Para lidiar con este tipo de situación, podemos crear una implementación de una interfaz que no haga “nada”. Este es un patrón de diseño conocido como Objeto nulo, [4] y corresponde aproximadamente a desconectar la computadora de la pared. Debido a que estamos usando acoplamiento suelto, podemos reemplazar una implementación real con algo que no hace nada sin causar problemas.


El Principio de Sustitución de Likov establece que si un módulo de programa está utilizando una clase Base, la referencia a la clase Base puede reemplazarse por una clase Derivada sin afectar la funcionalidad del módulo del programa.

Intención: los tipos derivados deben ser completamente sustituibles para sus tipos base.

Ejemplo: tipos de retorno de co-variante en java.


La explicación más clara para el LSP que encontré hasta ahora ha sido "El principio de sustitución de Liskov dice que el objeto de una clase derivada debería poder reemplazar un objeto de la clase base sin traer ningún error en el sistema o modificar el comportamiento de la clase base "desde here . El artículo da un ejemplo de código por violar el LSP y arreglarlo.


Le animo a leer el artículo: Violación del principio de sustitución de Liskov (LSP) .

Puede encontrar una explicación de lo que es el Principio de Sustitución de Liskov, pistas generales que lo ayudarán a adivinar si ya lo ha violado y un ejemplo de enfoque que lo ayudará a hacer que su jerarquía de clases sea más segura.


Un cuadrado es un rectángulo donde el ancho es igual a la altura. Si el cuadrado establece dos tamaños diferentes para el ancho y la altura, viola el cuadrado invariante. Esto se trabaja alrededor de la introducción de efectos secundarios. Pero si el rectángulo tenía un setSize (altura, anchura) con precondición 0 <altura y 0 <anchura. El método de subtipo derivado requiere altura == ancho; una condición previa más fuerte (y que viola lsp). Esto muestra que aunque el cuadrado es un rectángulo, no es un subtipo válido porque la condición previa se fortalece. El trabajo alrededor (en general algo malo) causa un efecto secundario y esto debilita la condición posterior (que viola lsp). setWidth en la base tiene condición de publicación 0 <ancho. Lo derivado lo debilita con altura == ancho.

Por lo tanto, un cuadrado redimensionable no es un rectángulo redimensionable.