Pregunta

He oído 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?

¿Fue útil?

Solución 2

El principio de sustitución de Liskov (LSP, ) es un concepto de 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 conocerlo.

En esencia, LSP se trata de interfaces y contratos, así como de cómo decidir cuándo extender una clase o cuándo extenderla.Utilice otra estrategia como la composición para lograr su objetivo.

La forma más efectiva que he visto para ilustrar este punto fue en Dirígete primero OOA&D.Presentan un escenario en el que usted es desarrollador de un proyecto para crear un marco para juegos de estrategia.

Presentan una clase que representa un tablero que se ve así:

Class Diagram

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á al desarrollador del juego administrar unidades en el tablero durante el transcurso del juego.

El libro continúa cambiando los requisitos para decir que el marco del juego también debe admitir tableros de juego 3D para acomodar juegos que tengan vuelo.entonces un ThreeDBoard Se introduce una clase que se extiende. Board.

A primera vista parece una buena decisión. Board proporciona tanto el Height y Width propiedades y ThreeDBoard proporciona el eje Z.

Donde se descompone es cuando miras a todos los demás miembros heredados de Board.Los métodos para AddUnit, GetTile, GetUnits y así sucesivamente, todos toman los parámetros X e Y en el Board clase pero el ThreeDBoard También necesita un parámetro Z.

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

Quizás deberíamos encontrar otro enfoque.en lugar de extender Board, ThreeDBoard debe estar compuesto por Board objetos.Uno Board objeto por unidad del eje Z.

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

Otros consejos

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

En matemáticas, un Square es un Rectangle.De hecho, es una especialización de un rectángulo.El "es un" te hace querer modelar esto con herencia.Sin embargo, si en el código hiciste Square derivar de Rectangle, Entonces un Square debe ser utilizable en cualquier lugar donde espere un Rectangle.Esto genera un comportamiento extraño.

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

enter image description here

Todos deberían ver el otro invaluable. Pósteres motivacionales de principios SOLID.

LSP se refiere a invariantes.

El ejemplo clásico lo proporciona la siguiente declaración de pseudocó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 que surgen de la definición matemática de cuadrados y rectángulos.La forma en que funcionan los captadores y definidores, una Rectangle debe satisfacer la siguiente invariante:

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

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

La sustituibilidad es un principio de la programación orientada a objetos que establece que, en un programa de computadora, si S es un subtipo de T, entonces 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 porque es un pájaro, pero ¿qué pasa con esto?

public class Ostrich extends Bird{}

El avestruz es un pájaro, pero no puede volar. La clase Avestruz es un subtipo de la clase Pájaro, pero no puede usar el método 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{} 

Robert Martin tiene una excelente documento sobre el principio de sustitución de Liskov.Analiza formas sutiles y no tan sutiles en las que se puede violar el principio.

Algunas partes relevantes del artículo (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 información de tipo de tiempo de ejecución C ++ (RTTI) para seleccionar una función basada en el tipo de 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 el DrawShape La función está mal formada.Debe saber acerca de todas las derivadas posibles de la Shape clase, y debe ser cambiada cada vez que se introduzcan nuevos derivados de Shape son creados.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, existen otras formas, mucho más sutiles, de violar el LSP.Considere una aplicación que utiliza el Rectangle clase 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;
};

...] Imagine que algún día los usuarios exigen la capacidad de manipular cuadrados además de los rectángulos.[...]

Claramente, un cuadrado es un rectángulo para todos los efectos normales.Dado que la relación ISA se cumple, es lógico modelar la Squareclase como derivada de Rectangle. [...]

Square heredará el SetWidth y SetHeight funciones.Estos funciones son totalmente inapropiadas para un Square, ya que la anchura y altura de un cuadrado son idénticos.Esto debería ser una pista importante que hay un problema con el diseño.Sin embargo, hay una manera de Eludir 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 Square objeto en esta función, el Square El objeto se dañará porque la altura no se cambiará.Esta es una clara violación del LSP.La función no funciona para derivados de sus argumentos.

[...]

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

Por ejemplo, esto ocurre cuando una función con un parámetro de entrada de tipo T, se llama (es decirinvocado) con un valor de argumento de tipo S.O, donde 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 las expectativas (es decir,invariantes) para métodos de tipo T (p.ej. Rectangle), no será violado cuando los métodos de tipo S (p.ej. Square) se llaman en su lugar.

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, p.el inmutable Quienes definen rectángulos esperan que las dimensiones se modifiquen de forma independiente, pero el inmutable Los establecedores cuadrados 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 variación es contraria a la dirección de la herencia, es decirel 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.

Covarianza significa que la varianza está en la misma dirección de la herencia, es decirel 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 del supertipo T.

Esto se debe a que si la persona que llama cree que tiene un tipo T, piensa que está llamando a un método de T, entonces proporciona argumento(s) de tipo Ti y asigna la salida al tipo To.Cuando en realidad está llamando al método correspondiente de S, entonces cada Ti El argumento de entrada se asigna a un Si parámetro de entrada y el So la salida se asigna al tipo To.Así si Si no fueron contravariantes w.r.t.a Ti, entonces un subtipo Xi—que no sería un subtipo de Si—podría ser asignado a Ti.

Además, para idiomas (p. ej.Scala o Ceilán) que tienen anotaciones de variación del sitio de definición en parámetros de polimorfismo de tipo (es decir,genéricos), la codirección o contradirección de la anotación de varianza para cada parámetro de tipo del tipo T debe ser opuesto o misma dirección respectivamente a cada parámetro de entrada o salida (de cada método de T) que tiene 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 la varianza requerida.Esta regla se aplica de forma recursiva.


La subtipificación es apropiada donde se pueden enumerar las invariantes.

Hay mucha investigación en curso sobre cómo modelar invariantes, para que el compilador los aplique.

Tipo de estado (ver página 3) declara y aplica invariantes de estado ortogonales al tipo.Alternativamente, las invariantes pueden imponerse mediante convertir afirmaciones a tipos.Por ejemplo, para afirmar que un archivo está abierto antes de cerrarlo, File.open() podría devolver un tipo OpenFile, que contiene un método close() que no está disponible en Archivo.A API de tres en raya puede ser otro ejemplo del empleo de tipificación para imponer invariantes en tiempo de compilación.El sistema de tipos puede incluso ser completo de Turing, p. escala.Los lenguajes de tipificación dependiente y los demostradores de teoremas formalizan los modelos de tipificación de orden superior.

Debido a la necesidad de que la semántica resumen sobre extensión, espero que emplear tipificación para modelar invariantes, es decirLa semántica denotacional unificada de orden superior es superior al Typestate."Extensión" significa la composición ilimitada y permutada de un desarrollo modular descoordinado.Porque me parece la antítesis de la unificación y, por tanto, de los grados de libertad, tener dos modelos mutuamente dependientes (p. ej.tipos y Typestate) para expresar la semántica compartida, que no se pueden unificar entre sí para una composición extensible.Por ejemplo, Problema de expresiónLa extensión tipo -like se unificó en los dominios de subtipificación, sobrecarga de funciones y tipificación paramétrica.

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

Por eso existe el teorema de la detención, es decirEs indecidible si todos los programas posibles en un lenguaje de programación completo de Turing terminan.Se puede demostrar que termina algún programa específico (uno en el que se han definido y calculado todas las posibilidades).Pero es imposible probar que toda extensión posible de ese programa termina, a menos que las posibilidades de extensión de ese programa no sean completas según Turing (p. ej.mediante escritura dependiente).Dado que el requisito fundamental para la integridad de Turing es recursividad ilimitada, es intuitivo comprender cómo se aplican a la extensión los teoremas de incompletitud de Gödel y la paradoja de Russell.

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 puedan demostrarse 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í mismo.Por tanto, los conjuntos no pueden ampliarse o son recursivos ilimitados.Por ejemplo, el conjunto de todo lo que no es tetera, se incluye a sí mismo, lo que se incluye a sí mismo, lo 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 extensiones ilimitadas.Es el conjunto de conjuntos que no son miembros de sí mismos.Esta incapacidad de ser coherente y enumerarse completamente en todas las extensiones posibles son los teoremas de incompletitud de Gödel.
  • Principio de sustitución de Liskov:generalmente es un problema indecidible si un conjunto es subconjunto de otro, es decirla herencia es generalmente indecidible.
  • Referencias de Linsky:es indecidible cuál es el cálculo de algo, cuándo se describe o se percibe, es decir,la percepción (realidad) no tiene un punto de referencia absoluto.
  • teorema de coase:no existe un punto de referencia externo, por lo que cualquier barrera a las posibilidades externas ilimitadas fracasará.
  • Segunda ley de la termodinámica:el universo entero (un sistema cerrado, es decirtodo) tiende al máximo desorden, es decir.Máximas posibilidades de independencia.

El LSP es una regla sobre el contrato de las clases:Si una clase base satisface un contrato, entonces, según el LSP, las clases derivadas también deben satisfacer ese contrato.

En pseudo-python

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

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

satisface 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.

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

Cuando leí por primera vez sobre LSP, supuse que esto se refería en un sentido muy estricto, esencialmente equiparándolo con la implementación de interfaz y la conversión de tipos seguros.Lo que significaría que el LSP está garantizado o no por el propio lenguaje.Por ejemplo, en este sentido estricto, ThreeDBoard es ciertamente reemplazable por Board, en lo que respecta al compilador.

Después de leer más sobre el concepto, descubrí que LSP generalmente se interpreta de manera más amplia.

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

Volviendo al ejemplo nuevamente, En teoria Se puede hacer que los métodos de la Junta funcionen bien en ThreeDBoard.Sin embargo, en la práctica será muy difícil evitar diferencias de comportamiento que el cliente no pueda manejar adecuadamente, sin obstaculizar la funcionalidad que ThreeDBoard pretende agregar.

Con este conocimiento en 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 ampliar la funcionalidad existente, en lugar de la herencia.

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

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

Lista de Verificación:

  • No se deben lanzar nuevas excepciones 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 IndexOutOfRangeException es una violación de Liskov.
  • Las condiciones previas no se pueden reforzar:Supongamos que su clase base funciona con un miembro int.Ahora su subtipo requiere que int sea positivo.Estas son condiciones previas reforzadas y ahora cualquier código que antes funcionaba perfectamente bien con entradas negativas se rompe.
  • Las poscondiciones no pueden debilitarse:Supongamos que su clase base requiere que todas las conexiones a la base de datos se cierren antes de que se devuelva el método.En su subclase anuló ese método y dejó la conexión abierta para su posterior reutilización.Has debilitado las condiciones posteriores de ese método.
  • Se deben preservar las invariantes:La limitación más difícil y dolorosa de cumplir.Las invariantes están ocultas durante algún tiempo en la clase base y la única forma de revelarlas es leer el código de la clase base.Básicamente, debe asegurarse de que, al anular un método, todo lo que no se pueda modificar deba permanecer sin cambios después de que se ejecute el método anulado.Lo mejor que se me ocurre es aplicar estas restricciones invariantes en la clase base, pero eso no sería fácil.
  • Restricción histórica:Al anular un método, no se le permite modificar una propiedad no modificable en la clase base.Eche un vistazo a este código y podrá ver que el Nombre está definido como no modificable (conjunto privado), pero SubType introduce un nuevo método que permite modificarlo (mediante 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 artículos: Contravarianza de los argumentos del método. y Covarianza de tipos de retorno.Pero no es posible en C# (soy desarrollador de C#), así que no me importan.

Referencia:

Un ejemplo importante de la usar de LSP está en 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 necesite 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 lograrlo es construir lo que McGregor llama una "jerarquía paralela para pruebas":Mi ATest la clase heredará de BTest.Entonces se necesita alguna forma de inyección para garantizar que el caso de prueba funcione con objetos de tipo A en lugar de tipo B (un patrón de método de plantilla simple será suficiente).

Tenga en cuenta que reutilizar el conjunto 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 uno debería ejecute el conjunto de pruebas de superclase en el contexto de cualquier subclase.

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

Supongo que todo el mundo ha explicado qué es técnicamente LSP:Básicamente, desea poder abstraerse de los detalles de los subtipos y utilizar los supertipos de forma segura.

Entonces Liskov tiene 3 reglas subyacentes:

  1. Regla de firma:Debería haber una implementación válida de cada operación del supertipo en el subtipo sintácticamente.Algo que un compilador podrá comprobar por usted.Hay una pequeña regla acerca de 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 deberían tomar al menos lo que el supertipo tomó como entrada, si no más.
    • Postcondiciones más fuertes:Deberían producir un subconjunto de la salida que produjeron los métodos de 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 conjunto nunca es negativo.
    • Propiedades evolutivas:Por lo general, tiene algo que ver con la inmutabilidad o el tipo de estados en los que puede encontrarse el objeto.O tal vez el objeto sólo crece y nunca se reduce, por lo que los métodos de subtipo no deberían hacerlo.

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

Si se solucionan estas tres cosas, se habrá abstraído de las cosas subyacentes y se estará escribiendo código débilmente acoplado.

Fuente:Desarrollo de programas en Java - Barbara Liskov

Largo Para resumir, dejemos rectángulos, rectángulos y cuadrados, cuadrados. Un ejemplo práctico al extender una clase principal, debe CONSERVAR la API principal exacta o EXTENDERLA.

Digamos que tienes un base Repositorio de artículos.

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ías tener un Cliente trabajar con la API Base ItemsRepository y confiar en ella.

/**
 * 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 sustituyendo padre clase con un la subclase 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 :(
    }
}

Puede obtener más información sobre cómo escribir software mantenible en mi curso: https://www.udemy.com/enterprise-php/

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 definidos 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 desempeño es parte del comportamiento de P...

Entonces, básicamente, cualquier uso de enlace tardío viola el LSP.¡El objetivo de OO es obtener un comportamiento diferente cuando sustituimos un objeto de un tipo por uno 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.

Algunas adiciones:
Me pregunto por qué nadie escribió sobre el Invariante, las condiciones previas y las condiciones posteriores de la clase base que deben ser obedecidas por las clases derivadas.Para que una clase derivada D sea completamente sustituible por la clase base B, la clase D debe cumplir ciertas condiciones:

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

Por lo tanto, el derivado debe tener en cuenta las tres condiciones anteriores impuestas por la clase base.Por tanto, las reglas de subtipificación están predeterminadas.Lo que significa que la relación 'ES A' se obedecerá solo cuando el subtipo obedezca ciertas reglas.Estas reglas, en forma de invariantes, precocondiciones y poscondiciones, deben ser decididas por un proceso formal.contrato de diseño'.

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

En una frase muy sencilla podemos decir:

La clase secundaria no debe violar las características de su clase base.Debe ser capaz con eso.Podemos decir que es lo mismo que subtipificar.

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

Me gustaría mostrar cómo se puede ajustar el LSP con 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 elijamos utilizar.

Y sí, puedes violar LSP en esta configuración haciendo un cambio simple como este:

<?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 porque ya no producen el mismo resultado.

Principio de sustitución de Liskov (LSP)

Todo el tiempo diseñamos un módulo de programa y creamos alguna clase Jerarquías.A continuación, extendemos algunas clases creando algunas derivadas Clases.

Debemos asegurarnos de que las nuevas clases derivadas se extiendan sin reemplazando la funcionalidad de las clases antiguas.De lo contrario, las nuevas clases pueden producir efectos no deseados cuando se utilizan en programas existentes Módulos.

El Principio de Sustitución de Liskov establece que si un módulo de programa es usando una clase Base, entonces la referencia a la clase Base puede ser reemplazado por una clase derivada sin afectar la funcionalidad de el módulo del programa.

Ejemplo:

A continuación se muestra el ejemplo clásico en el que 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 Square.La clase cuadrada se devuelve mediante un patrón de fábrica, basado en algunas condiciones y no sabemos exactamente qué tipo de objeto se devolverá.Pero sabemos que es un rectángulo.Obtenemos el objeto rectángulo, establecemos el ancho en 5 y el alto en 10 y obtenemos el área.Para un rectángulo con ancho 5 y alto 10, el área debe ser 50.En cambio, 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 no es más que una extensión del principio de apertura y cierre significa que debemos asegurarnos de que las nuevas clases derivadas se extiendan las clases base sin cambiar su comportamiento.

Ver también: Principio de apertura y cierre

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

¿Sería tan útil implementar ThreeDBoard en términos de una variedad de tableros?

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

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

Un cuadrado es un rectángulo donde el ancho es igual al alto.Si el cuadrado establece dos tamaños diferentes para el ancho y el alto, viola la invariante del cuadrado.Esto se soluciona introduciendo efectos secundarios.Pero si el rectángulo tuviera un setSize(alto, ancho) con la condición previa 0 <alto y 0 <ancho.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 está reforzada.La solución alternativa (en general algo malo) causa un efecto secundario y esto debilita la condición posterior (que viola lsp).setWidth en la base tiene la condición de publicación 0 <ancho.La derivada lo debilita con altura == ancho.

Por lo tanto, un cuadrado de tamaño variable no es un rectángulo de tamaño variable.

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 largo que su alto.hagamos un Square clase también basada en esta información:

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

Si reemplazamos el Rectangle con Square en 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 se debe a que el Square tiene una nueva condición previa que no teníamos en el Rectangle clase: width == height.Según LSP el Rectangle las instancias deben ser sustituibles con Rectangle instancias de subclase.Esto se debe a que estas instancias pasan la verificación de tipo para Rectangle instancias y por lo tanto causarán errores inesperados en su código.

Este fue un ejemplo para el "Las condiciones previas no se pueden reforzar en un subtipo" parte en el artículo wiki.En resumen, violar LSP probablemente causará errores en su código en algún momento.

Ilustremos en Java:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }

   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

class Car extends TransportationDevice
{
   @Override
   void startEngine() { ... }
}

Aquí no hay ningún problema, ¿verdad?Un automóvil es definitivamente un dispositivo de transporte, y aquí podemos ver que anula el método startEngine() de su superclase.

Agreguemos otro dispositivo de transporte:

class Bicycle extends TransportationDevice
{
   @Override
   void startEngine() /*problem!*/
}

¡Ahora no todo va según lo planeado!Sí, una bicicleta es un dispositivo de transporte, sin embargo, no tiene motor y, por lo tanto, el método startEngine() no se puede implementar.

Estos son los tipos de problemas que la violación de la sustitución de Liskov Los principios conducen a, y por lo general pueden ser reconocidos por un método que no hace nada, o incluso que no se puede implementar.

La solución a estos problemas es una jerarquía de herencia correcta, y en nuestro caso resolveríamos el problema diferenciando clases de dispositivos de transporte con y sin motor.Aunque una bicicleta es un medio de transporte, no tiene motor.En este ejemplo nuestra definición de dispositivo de transporte es incorrecta.No debería tener motor.

Podemos refactorizar nuestra clase TransportationDevice de la siguiente manera:

class TrasportationDevice
{
   String name;
   String getName() { ... }
   void setName(String n) { ... }

   double speed;
   double getSpeed() { ... }
   void setSpeed(double d) { ... }
}

Ahora podemos ampliar TransportationDevice para dispositivos no motorizados.

class DevicesWithoutEngines extends TransportationDevice
{  
   void startMoving() { ... }
}

Y amplíe TransportationDevice para dispositivos motorizados.Aquí es más apropiado agregar el objeto Motor.

class DevicesWithEngines extends TransportationDevice
{  
   Engine engine;
   Engine getEngine() { ... }
   void setEngine(Engine e) { ... }

   void startEngine() { ... }
}

De este modo, nuestra clase Car se vuelve más especializada, al tiempo que se adhiere al Principio de sustitución de Liskov.

class Car extends DevicesWithEngines
{
   @Override
   void startEngine() { ... }
}

Y nuestra clase de Bicicleta también cumple con el Principio de sustitución de Liskov.

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}

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

Allí podrás encontrar una explicación de qué es el Principio de Sustitución de Liskov, pistas generales que te ayudarán a adivinar si ya lo has violado y un ejemplo de enfoque que te ayudará a hacer que tu jerarquía de clases sea más segura.

La explicación más clara para 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 generar ningún error en el sistema ni modificar el comportamiento de la clase base". " de aquí.El artículo ofrece un ejemplo de código para violar LSP y solucionarlo.

EL PRINCIPIO DE SUSTITUCIÓN DE LISKOV (del libro de Mark Seemann) establece que deberíamos poder reemplazar una implementación de una interfaz por otra sin dañar ni el cliente ni la implementación. Es este principio el que permite abordar los requisitos que se producirán en el futuro, incluso si podemos. No los preveo hoy.

Si desconectamos el ordenador de la pared (Implementación), ni el tomacorriente (Interfaz) ni el ordenador (Cliente) se estropean (de hecho, si es un ordenador portátil, puede incluso funcionar con sus pilas durante un tiempo) .Sin embargo, con el software, un cliente a menudo espera que un servicio esté disponible.Si se eliminó el servicio, obtenemos una NullReferenceException.Para hacer frente a este tipo de situaciones, podemos crear una implementación de una interfaz que no haga "nada". Este es un patrón de diseño conocido como Null Object,[4] y corresponde aproximadamente a desenchufar la computadora de la pared.Como utilizamos un acoplamiento flexible, podemos reemplazar una implementación real con algo que no haga nada sin causar problemas.

El principio de sustitución de Likov establece que Si un módulo de programa utiliza una clase Base, entonces la referencia a la clase Base se puede reemplazar con una clase Derivada sin afectar la funcionalidad del módulo de programa.

Intención: los tipos derivados deben poder sustituir completamente a sus tipos base.

Ejemplo: tipos de retorno covariantes en java.

LSP dice que "Los objetos deberían ser reemplazables por sus subtipos".Por otra parte, este principio apunta a

Las clases secundarias nunca deben romper las definiciones de tipo de la clase principal.

y el siguiente ejemplo ayuda a tener una mejor comprensión de LSP.

Sin LSP:

public interface CustomerLayout{

    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            return; //it isn`t rendered in this case
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Fijación por LSP:

public interface CustomerLayout{
    public void render();
}


public FreeCustomer implements CustomerLayout {
     ...
    @Override
    public void render(){
        //code
    }
}


public PremiumCustomer implements CustomerLayout{
    ...
    @Override
    public void render(){
        if(!hasSeenAd)
            showAd();//it has a specific behavior based on its requirement
        //code
    }
}

public void renderView(CustomerLayout layout){
    layout.render();
}

Déjame intentarlo, considera una interfaz:

interface Planet{
}

Esto se implementa por clase:

class Earth implements Planet {
    public $radius;
    public function construct($radius) {
        $this->radius = $radius;
    }
}

Usarás la Tierra como:

$planet = new Earth(6371);
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Consideremos ahora una clase más que extiende la Tierra:

class LiveablePlanet extends Earth{
   public function color(){
   }
}

Ahora, según LSP, debería poder utilizar LiveablePlanet en lugar de Earth y no debería dañar su sistema.Como:

$planet = new LiveablePlanet(6371);  // Earlier we were using Earth here
$calc = new SurfaceAreaCalculator($planet);
$calc->output();

Ejemplos tomados de aquí

Aquí hay un extracto de esta publicación eso aclara las cosas muy bien:

[...] para comprender algunos principios es importante darse cuenta de cuándo se han 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 identificaste mal tus abstracciones.

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 del LSP?Sí.Esto se debe a que el contrato de la cuenta nos dice que se retirará una cuenta, pero no siempre es así.Entonces, ¿qué debo hacer para solucionarlo?Solo modifico 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 sutil violación a menudo impone al cliente la capacidad de distinguir entre los objetos concretos empleados.Por ejemplo, dado el contrato de la primera Cuenta, podría verse así:

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, el requisito de retiro de dinero.Porque nunca se sabe qué pasa si un objeto que viola el contrato no tiene suficiente dinero.Probablemente simplemente no devuelva nada, probablemente se generará una excepción.Así que hay que comprobar si hasEnoughMoney() -- que no forma parte de una interfaz.Entonces, 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 del LSP.Dice que "si el comportamiento de un padre cambió en un niño, entonces, viola LSP". Sin embargo, no lo hace, siempre y cuando un niño no viole el contrato de sus padres.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top