¿Cuál es un ejemplo del Principio de sustitución de Liskov?

908

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?

No a mí mismo
fuente
Más ejemplos de adhesión y violación de LSP aquí
StuartLC
1
Esta pregunta tiene infinitas buenas respuestas y, por lo tanto, es demasiado amplia .
Raedwald

Respuestas:

892

Un gran ejemplo que ilustra el LSP (dado 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 en el código.

En matemáticas, a Squarees a 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 hizo Squarederivan de Rectangle, a continuación, una Squaredebe estar en cualquier parte utilizable se espera una Rectangle. Esto genera un comportamiento extraño.

Imagina que tienes SetWidthy SetHeightmétodos en tu Rectangleclase base; Esto parece perfectamente lógico. Sin embargo, si su Rectanglereferencia apunta a a Square, entonces, SetWidthy SetHeightno tiene sentido porque establecer uno cambiaría al otro para que coincida. En este caso, Squarefalla la prueba de sustitución de Liskov Rectangley la abstracción de haber Squareheredado Rectanglees mala.

enter image description here

Todos deberían ver los otros valiosos carteles motivacionales de los Principios SÓLIDOS .

m-sharp
fuente
19
@ m-sharp ¿Qué sucede si se trata de un rectángulo inmutable tal que, en lugar de SetWidth y SetHeight, tengamos los métodos GetWidth y GetHeight?
Pacerier
140
Moraleja de la historia: modele sus clases basándose en comportamientos no en propiedades; modele sus datos basados ​​en propiedades y no en comportamientos. Si se comporta como un pato, sin duda es un pájaro.
Sklivvz
193
Bueno, un cuadrado claramente ES un tipo de rectángulo en el mundo real. Si podemos modelar esto en nuestro código depende de la especificación. Lo que indica el LSP es que el comportamiento del subtipo debe coincidir con el comportamiento del tipo base como se define en la especificación del tipo base. Si la especificación del tipo de base del rectángulo dice que la altura y el ancho se pueden establecer de forma independiente, entonces LSP dice que el cuadrado no puede ser un subtipo de rectángulo. Si la especificación del rectángulo dice que un rectángulo es inmutable, entonces un cuadrado puede ser un subtipo de rectángulo. Se trata de subtipos que mantienen el comportamiento especificado para el tipo base.
SteveT
63
@Pacerier no hay problema si es inmutable. El problema real aquí es que no estamos modelando rectángulos, sino más bien "rectángulos reestructurables", es decir, rectángulos cuyo ancho o alto se pueden modificar después de la creación (y todavía consideramos que es el mismo objeto). Si nos fijamos en la clase de rectángulo de esta manera, está claro que un cuadrado no es un "rectángulo reestructurable", porque un cuadrado no puede ser reformado y sigue siendo un cuadrado (en general). Matemáticamente, no vemos el problema porque la mutabilidad ni siquiera tiene sentido en un contexto matemático.
Asmeurer
14
Tengo una pregunta sobre el principio. ¿Por qué sería el problema si Square.setWidth(int width)se implementara así this.width = width; this.height = width;:? En este caso, se garantiza que el ancho es igual a la altura.
MC Emperor
488

El principio de sustitución de Liskov (LSP, ) es un concepto en 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 esencia, LSP se trata de interfaces y contratos, así como de cómo decidir cuándo extender una clase versus usar otra estrategia, como la composición, para lograr su objetivo.

La manera eficaz la mayoría que he visto para ilustrar este punto estaba en cabeza 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í:

Diagrama de clase

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 cambiando los requisitos para decir que el marco del juego también debe ser compatible con tableros de juegos 3D para acomodar juegos que tienen vuelo. Entonces ThreeDBoardse introduce una clase que se extiende Board.

A primera vista, esto parece una buena decisión. Boardproporciona tanto el Heighty Widthpropiedades y ThreeDBoardproporciona el eje Z.

Cuando se descompone es cuando observa a todos los demás miembros heredados de Board. Los métodos para AddUnit, GetTile, GetUnitsy así sucesivamente, todos toman ambos parámetros x e y en la Boardclase pero la ThreeDBoardnecesita un parámetro Z también.

Entonces debe implementar esos métodos nuevamente con un parámetro Z. El parámetro Z no tiene contexto para la Boardclase y los métodos heredados de la Boardclase pierden su significado. Una unidad de código que intente usar la ThreeDBoardclase como su clase base Boardno tendrá mucha suerte.

Tal vez deberíamos encontrar otro enfoque. En lugar de extenderse Board, ThreeDBoarddebe estar compuesto de Boardobjetos. Un Boardobjeto 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 el LSP.

No a mí mismo
fuente
10
Vea también Problema círculo-elipse en Wikipedia para un ejemplo similar pero más simple.
Brian
Requote de @NotMySelf: "Creo que el ejemplo es simplemente para demostrar que heredar de la placa no tiene sentido en el contexto de ThreeDBoard y que todas las firmas de métodos no tienen sentido con un eje Z".
Contango
1
Entonces, si agregamos otro método a una clase Child pero toda la funcionalidad de Parent todavía tiene sentido en la clase Child, ¿estaría rompiendo LSP? Dado que, por un lado, modificamos un poco la interfaz para usar el Niño, por otro lado, si lanzamos el Niño para que sea un Padre, el código que espera que un Padre funcione bien.
Nickolay Kondratyev
55
Este es un ejemplo anti-Liskov. Liskov nos hace derivar un rectángulo de la plaza. Más-parámetros-clase de menos-parámetros-clase. Y has demostrado muy bien que es malo. Realmente es una buena broma haber marcado como respuesta y haber votado 200 veces una respuesta anti-liskov para la pregunta de liskov. ¿Es el principio de Liskov una falacia realmente?
Gangnus
3
He visto que la herencia funciona de manera incorrecta. Aquí hay un ejemplo. La clase base debe ser 3DBoard y la clase derivada Board. La Junta todavía tiene un eje Z de Máx. (Z) = Mín. (Z) = 1
Paulustioso
169

La sustituibilidad es un principio en 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 por 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 de avestruz es un subtipo de la clase pájaro, 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{} 
Maysara Alhindi
fuente
3
Buen ejemplo, pero qué haría si el cliente lo tiene Bird bird. Tienes que lanzar el objeto a FlyingBirds para hacer uso de la mosca, lo cual no es bueno, ¿verdad?
Moody
17
No. Si el cliente tiene Bird bird, eso significa que no puede usar fly(). Eso es. Pasar a Duckno cambia este hecho. Si el cliente lo ha hecho FlyingBirds bird, incluso si se supera Duck, siempre debería funcionar de la misma manera.
Steve Chamaillard
99
¿No sería esto también un buen ejemplo para la segregación de interfaz?
Saharsh
Excelente ejemplo Gracias Hombre
Abdelhadi Abdo
66
¿Qué tal el uso de la interfaz 'Flyable' (no se me ocurre un nombre mejor). De esta manera no nos comprometemos en esta rígida jerarquía. A menos que sepamos que realmente lo necesitamos.
Tercero
132

LSP se refiere a invariantes.

El ejemplo clásico está dado por 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 derivados de la definición matemática de cuadrados y rectángulos. La forma en que funcionan los captadores y los establecedores, Rectangledebe satisfacer lo 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 implementación correcta de Square, por lo tanto, no es un sustituto válido de Rectangle.

Konrad Rudolph
fuente
35
Y de ahí la dificultad de usar "OO" para modelar cualquier cosa que queramos modelar.
DrPizza el
99
@DrPizza: Absolutamente. Sin embargo, dos cosas. En primer lugar, tales relaciones aún se pueden modelar en OOP, aunque de manera incompleta o utilizando desvíos más complejos (elija el que mejor se adapte a su problema). En segundo lugar, no hay mejor alternativa. Otras asignaciones / modelados tienen los mismos problemas o problemas similares. ;-)
Konrad Rudolph el
77
@NickW En algunos casos (pero no en lo anterior) simplemente puede invertir la cadena de herencia, lógicamente hablando, un punto 2D es un punto 3D, donde la tercera dimensión no se tiene en cuenta (o 0 - todos los puntos se encuentran en el mismo plano en Espacio 3D). Pero esto, por supuesto, no es realmente práctico. En general, este es uno de los casos en que la herencia realmente no ayuda, y no existe una relación natural entre las entidades. Modelarlos por separado (al menos no sé de una mejor manera).
Konrad Rudolph
77
OOP está destinado a modelar comportamientos y no datos. Sus clases violan la encapsulación incluso antes de violar LSP.
Sklivvz
2
@AustinWBryan Sip; cuanto más tiempo he estado trabajando en este campo, más tiendo a usar la herencia solo para interfaces y clases base abstractas, y la composición para el resto. A veces es un poco más trabajo (mecanografía), pero evita muchos problemas y es un consejo ampliamente repetido por otros programadores experimentados.
Konrad Rudolph
77

Robert Martin tiene un excelente artículo sobre el Principio de sustitución de Liskov . Discute formas sutiles y no tan sutiles de cómo se puede violar el principio.

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 DrawShapefunción está mal formada. Debe conocer todas las derivadas posibles de la Shapeclase, y debe cambiarse siempre que Shapese creen nuevas derivadas de . 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 Rectangleclase 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 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 Square clase como derivada Rectangle. [...]

Squareheredará las funciones SetWidthy SetHeight. Estas funciones son completamente inapropiadas para a 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 SetWidthy SetHeight[...]

Pero considere la siguiente función:

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

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

[...]

Phillip Wells
fuente
14
Muy tarde, pero pensé que esta era una cita interesante en ese documento: Now the rule for the preconditions and postconditions for derivatives, as stated by Meyer is: ...when redefining a routine [in a derivative], you may only replace its precondition by a weaker one, and its postcondition by a stronger one. si una precondición de clase infantil es más fuerte que una precondición de clase de padre, no podría sustituir a un niño por un padre sin violar la precondición. De ahí el LSP.
user2023861
@ user2023861 Tienes toda la razón. Escribiré una respuesta basada en esto.
inf3rno
40

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

Por ejemplo, esto ocurre cuando una función con un parámetro de entrada de tipo Tse llama (es decir, se invoca) 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 que las expectativas (es decir, invariantes) para los métodos de tipo T(p Rectangle. Ej. ) No se infrinjan cuando se invocan los métodos de tipo S(p Square. Ej. ).

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 establecedores de rectángulo inmutables esperan que las dimensiones se modifiquen de forma independiente, pero los establecedores cuadrados 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 Stenga 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 Side cada parámetro de entrada de cada método del subtipo Sdebe ser el mismo o un supertipo del tipo Tidel 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 decir, el tipo So, de la salida de cada método del subtipo S, debe ser el mismo o un subtipo del tipo Tode 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, cree que está llamando a un método T, entonces proporciona argumentos de tipo Tiy asigna la salida al tipo To. Cuando realmente llama al método correspondiente de S, entonces cada Tiargumento de entrada se asigna a un Siparámetro de entrada, y la Sosalida se asigna al tipo To. Por lo tanto, si Sino fuera contravariante wrt a Ti, entonces Xise Sipodría asignar un subtipo , que no sería un subtipo de Ti.

Además, para los idiomas (p. Ej., Scala o Ceilán) que tienen anotaciones de variación del sitio de definición en los parámetros de polimorfismo de tipo (es decir, genéricos), la codirección o contradirección de la anotación de variación para cada parámetro de tipo del tipo Tdebe ser opuesta o la 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 varianza requerida. Esta regla se aplica de forma recursiva.


Subtipar es apropiado donde los invariantes pueden ser enumerados.

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

Typestate (consulte la página 3) declara y aplica invariantes de estado ortogonales para escribir. Alternativamente, los invariantes pueden hacerse cumplir mediante la conversión de aserciones 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 File. Una API tic-tac-toe puede ser otro ejemplo de empleo de tipeo para imponer invariantes en tiempo de compilación. El sistema de tipos puede incluso ser Turing completo, por ejemplo, Scala . Los lenguajes y demostradores de teoremas de tipo dependiente formalizan los modelos de mecanografía de orden superior.

Debido a la necesidad de que la semántica abstraiga sobre la extensión , espero que el empleo de la tipificación para modelar invariantes, es decir, una semántica denotativa de orden superior unificada, sea superior al Typestate. 'Extensión' significa la composición ilimitada y permutada del desarrollo modular no coordinado. Porque me parece ser 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 puede unificarse entre sí para una composición extensible . Por ejemplo, la extensión similar a un problema de expresión se unificó en los dominios de subtipo, sobrecarga de funciones y tipado paramétrico.

Mi posición teórica es que para que exista el conocimiento (ver sección “La centralización es ciega y no apta”), nunca habrá un modelo general que pueda exigir una cobertura del 100% de todos los posibles invariantes en un lenguaje informático completo de Turing. Para que exista el 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.

Es por eso que existe el Teorema de detención, es decir, es indecidible si todos los programas posibles en un lenguaje de programación completo de Turing terminan. Se puede demostrar que algún programa específico termina (uno en el que se han definido y calculado todas las posibilidades). Pero es imposible demostrar que toda la extensión posible de ese programa finaliza, a menos que las posibilidades de extensión de ese programa no estén completas (por ejemplo, a través del tipeo 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í mismo. Por lo tanto, los conjuntos no se pueden extender o son una 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 ilimitada. Este es el conjunto de conjuntos que no son miembros de sí mismos. Esta incapacidad para ser a la vez consistente y completamente enumerada en toda la extensión posible, es el teorema de incompletitud de Gödel.
  • Principio de Substición de Liskov : generalmente es un problema indecidible si un conjunto es el subconjunto de otro, es decir, la herencia es generalmente indecidible.
  • Referencia de Linsky : es indecidible cuál es el cálculo de algo, cuando se describe o se 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 desorden máximo, es decir, las máximas posibilidades independientes.
Shelby Moore III
fuente
17
@ Shelyby: Has mezclado demasiadas cosas. Las cosas no son tan confusas como las expresas. Gran parte de sus afirmaciones teóricas se basan en motivos débiles, como 'Para que exista el conocimiento, existen muchas posibilidades inesperadas .........' Y 'generalmente es un problema indecidible si un conjunto es el subconjunto de otro, es decir la herencia es generalmente indecidible '. Puede iniciar un blog separado para cada uno de estos puntos. De todos modos, sus afirmaciones y suposiciones son muy cuestionables. ¡No se deben usar cosas de las que no se tiene conciencia!
aknon
1
@aknon Tengo un blog que explica estos asuntos con más profundidad. Mi modelo TOE de espacio-tiempo infinito son frecuencias ilimitadas. No me confunde que una función inductiva recursiva tenga un valor inicial conocido con un límite de fin infinito, o una función coinductiva tenga un valor final desconocido y un límite de inicio conocido. La relatividad es el problema una vez que se introduce la recursividad. Esta es la razón por la cual Turing completo es equivalente a una recursión ilimitada .
Shelby Moore III
44
@ShelbyMooreIII Vas en demasiadas direcciones. Esta no es una respuesta.
Soldalma
1
@Soldalma es una respuesta. ¿No lo ves en la sección de respuestas? El tuyo es un comentario porque está en la sección de comentarios.
Shelby Moore III
1
¡Me gusta tu mezcla con el mundo scala!
Ehsan M. Kermani
24

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

Me gustaría mostrar cómo se puede conformar 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 usar.

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

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

Steve Chamaillard
fuente
66
El ejemplo no viola el LSP solo mientras limitemos la semántica de Database::selectQueryadmitir solo el subconjunto de SQL compatible con todos los motores de base de datos. Eso no es práctico ... Dicho esto, el ejemplo es aún más fácil de entender que la mayoría de los demás utilizados aquí.
Palec
55
Encontré esta respuesta la más fácil de entender del resto.
Malcolm Salvador
23

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

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

Lista de Verificación:

  • No se deben lanzar nuevas excepciones en la clase derivada : si su clase base lanzó ArgumentNullException, entonces a sus subclases solo se les permitió 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 fortalecer : suponga que su clase base funciona con un miembro int. Ahora su subtipo requiere que int sea positivo. Esto se refuerza las condiciones previas, y ahora cualquier código que funcionó perfectamente bien antes con entradas negativas está roto.
  • Las condiciones posteriores no se pueden debilitar : suponga 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 restricción más difícil y dolorosa de cumplir. Las invariantes están ocultas en la clase base y la única forma de revelarlas es leer el código de la clase base. Básicamente, debe asegurarse cuando anula un método, cualquier cosa que no se pueda cambiar debe permanecer sin cambios después de que se ejecute su método anulado. Lo mejor que puedo pensar es hacer cumplir estas restricciones invariables en la clase base, pero eso no sería fácil.
  • Restricción de historial : al anular un método, no está permitido modificar una propiedad no modificable en la clase base. Eche un vistazo a este código y podrá ver que el Nombre se define como no modificable (conjunto privado), pero SubType presenta 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 elementos: contravarianza de los argumentos del método y covarianza de los tipos de retorno . Pero no es posible en C # (soy un desarrollador de C #), así que no me importan.

Referencia:

Cù Đức Hiếu
fuente
También soy desarrollador de C # y diré que su última declaración no es cierta a partir de Visual Studio 2010, con el marco .Net 4.0. La covarianza de los tipos de retorno permite un tipo de retorno más derivado que el definido por la interfaz. Ejemplo: Ejemplo: IEnumerable <T> (T es covariante) IEnumerator <T> (T es covariante) IQueryable <T> (T es covariante) IGrouping <TKey, TElement> (TKey y TElement son covariantes) IComparer <T> (T es contravariante) IEqualityComparer <T> (T es contravariante) IComparable <T> (T es contravariante) msdn.microsoft.com/en-us/library/dd233059(v=vs.100).aspx
LCarter
1
Excelente y enfocada respuesta (aunque las preguntas originales eran sobre ejemplos más que reglas).
Mike
22

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

En pseudo-pitón

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.

Charlie Martin
fuente
99
Pero ... si siempre obtienes el mismo comportamiento, ¿cuál es el punto de tener la clase derivada?
Leonid
2
Te perdiste un punto: es el mismo comportamiento observado . Puede, por ejemplo, reemplazar algo con el rendimiento O (n) con algo funcionalmente equivalente, pero con el rendimiento O (lg n). O puede reemplazar algo que accede a datos implementados con MySQL y reemplazarlo con una base de datos en memoria.
Charlie Martin el
@Charlie Martin, codificando para una interfaz en lugar de una implementación, lo entiendo. Esto no es exclusivo de OOP; lenguajes funcionales como Clojure promueven eso también. Incluso en términos de Java o C #, creo que usar una interfaz en lugar de usar una clase abstracta más jerarquías de clases sería natural para los ejemplos que proporcione. Python no está fuertemente tipado y realmente no tiene interfaces, al menos no explícitamente. Mi dificultad es que he estado haciendo POO durante varios años sin adherirme a SOLID. Ahora que lo encontré, parece limitante y casi contradictorio.
Hamish Grubijan
Bueno, debes regresar y revisar el artículo original de Barbara. Reports-archive.adm.cs.cmu.edu/anon/1999/CMU-CS-99-156.ps No se menciona realmente en términos de interfaces, y es una relación lógica que se mantiene (o no) en cualquier lenguaje de programación que tiene alguna forma de herencia.
Charlie Martin
1
@HamishGrubijan No sé quién te dijo que Python no está mal escrito, pero te estaban mintiendo (y si no me crees, enciende un intérprete de Python e inténtalo 2 + "2"). ¿Quizás confunde "fuertemente tipado" con "tipado estáticamente"?
Asmeurer
21

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

Digamos que tiene una base de ItemsRepository.

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 extiende:

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 Base ItemsRepository y confiando 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 la sustitución de la clase padre con una 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/

Lukas Lukac
fuente
20

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, supuse que se refería a esto en un sentido muy estricto, esencialmente equiparándolo con la implementación de la interfaz y la conversión de tipo seguro. Lo que significaría que el lenguaje en sí garantiza o no el LSP. 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, descubrí que el LSP generalmente se interpreta de manera más amplia 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 tipo. La adherencia al LSP también es comprobable mediante el sondeo del 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 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 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 a 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.

Chris Ammerman
fuente
19

Supongo que todo el mundo cubrió lo que LSP es técnicamente: básicamente desea poder abstraerse de los detalles del subtipo y usar supertipos de manera segura.

Entonces 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á verificar por usted. 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: 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 verdaderas deben seguir siéndolo. 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 que puede estar el objeto. O tal vez el objeto solo crece y nunca se encoge, por lo que los métodos de subtipo no deberían hacerlo.

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

Si se resuelven estas tres cosas, se ha abstraído de las cosas subyacentes y está escribiendo código débilmente acoplado.

Fuente: Desarrollo de programas en Java - Barbara Liskov

snagpaul
fuente
18

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

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

Para probar completamente la subclase A, probablemente necesito 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 esto construyendo lo que McGregor llama una "jerarquía paralela para las pruebas": mi ATestclase heredará de BTest. 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 funcionará).

Tenga en cuenta que reutilizar el conjunto de superpruebas para todas las implementaciones de subclase es, de hecho, una forma de comprobar que estas implementaciones de subclase 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.

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? "

avandeursen
fuente
14

Vamos a ilustrar 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() { ... }
}

No hay problema aquí, ¿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!*/
}

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

Estos son los tipos de problemas a los que conduce la violación del Principio de sustitución de Liskov, y generalmente se pueden reconocer mediante un método que no hace nada, o incluso 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 las clases de dispositivos de transporte con y sin motores. Aunque una bicicleta es un dispositivo de transporte, no tiene motor. En este ejemplo, nuestra definición de dispositivo de transporte es incorrecta. No debe tener un 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 extender el dispositivo de transporte para dispositivos no motorizados.

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

Y extienda el dispositivo de transporte 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() { ... }
}

Por lo tanto, nuestra clase de automóviles se vuelve más especializada, mientras se adhiere al Principio de sustitución de Liskov.

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

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

class Bicycle extends DevicesWithoutEngines
{
   @Override
   void startMoving() { ... }
}
Khaled Qasem
fuente
9

Esta formulación del LSP es demasiado fuerte:

Si para cada objeto o1 del tipo S hay un objeto o2 del tipo T, de modo 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 rendimiento 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 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.

Damien Pollet
fuente
2
Erm, esa formulación es de Barbara Liskov. Barbara Liskov, "Abstracción de datos y jerarquía", SIGPLAN Notices, 23,5 (mayo de 1988). No es "demasiado fuerte", es "exactamente correcto", y no tiene la implicación que usted cree que tiene. Es fuerte, pero tiene la cantidad justa de fuerza.
DrPizza el
Entonces, hay muy pocos subtipos en la vida real :)
Damien Pollet
3
"El comportamiento no ha cambiado" no significa que un subtipo le dará exactamente los mismos valores concretos de resultados. Significa que el comportamiento del subtipo coincide con lo que se espera en el tipo base. Ejemplo: la forma de tipo base podría tener un método draw () y estipular que este método debería representar la forma. Dos subtipos de Forma (por ejemplo, Cuadrado y Círculo) implementarían el método draw () y los resultados serían diferentes. Pero siempre y cuando el comportamiento (renderizando la forma) coincida con el comportamiento especificado de Shape, Square y Circle serían subtipos de Shape de acuerdo con el LSP.
SteveT
9

En una oración muy simple, podemos decir:

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

Alireza Rahmani Khalili
fuente
9

Principio de sustitución de Liskov (LSP)

Todo el tiempo diseñamos un módulo de programa y creamos algunas jerarquías de clases. Luego ampliamos algunas clases creando algunas clases derivadas.

Debemos asegurarnos de que las nuevas clases derivadas 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á usando una clase Base, entonces la referencia a la clase Base se puede reemplazar con una clase Derivada sin afectar la funcionalidad del módulo del programa.

Ejemplo:

A continuación se muestra el ejemplo clásico por el cual se viola el Principio de sustitución de Liskov. En el ejemplo, se usan 2 clases: Rectángulo y Cuadrado. Supongamos que el objeto Rectangle se usa en algún lugar de la aplicación. Extendemos la aplicación y agregamos la clase Square. 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 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 es solo una extensión del Principio Open Close y significa que debemos asegurarnos de que las nuevas clases derivadas amplíen las clases base sin cambiar su comportamiento.

Ver también: Abrir Cerrar Principio

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

GauRang Omar
fuente
8

El principio de sustitución de Liskov

  • El método anulado no debe permanecer vacío.
  • El método anulado no debería arrojar un error
  • El comportamiento de la clase base o de la interfaz no debe modificarse (reprocesarse) debido a los comportamientos derivados de la clase.
Rahamath
fuente
7

Algún apéndice:
Me pregunto por qué nadie escribió acerca de las invariantes, condiciones previas y condiciones de publicación de la clase base que las clases derivadas deben obedecer. Para que una clase D derivada sea completamente sustituible por la clase B básica, la clase D debe obedecer ciertas condiciones:

  • Las variantes dentro 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, el derivado debe tener en cuenta las tres condiciones anteriores impuestas por la clase base. Por lo tanto, las reglas de subtipo están predeterminadas. Lo que significa que la relación 'IS A' se obedecerá solo cuando el subtipo obedezca ciertas reglas. Estas reglas, en forma de invariantes, precondiciones 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

aknon
fuente
6

El LSP en términos simples establece que los objetos de la misma superclase deben poder intercambiarse entre sí sin romper nada.

Por ejemplo, si tenemos una Caty una Dogclase derivada de una Animalclase, cualquier función que use la clase Animal debería poder usar Cato Dogcomportarse normalmente.

johannesMatevosyan
fuente
4

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

Tal vez desee tratar las rebanadas 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 desee factorizar una interfaz de placa para TwoDBoard y ThreeDBoard (aunque ninguno de los métodos anteriores se ajusta).

Tom Hawtin - tackline
fuente
1
Creo que el ejemplo es simplemente para demostrar que heredar de la placa no tiene sentido en el contexto de ThreeDBoard y que todas las firmas de métodos no tienen sentido con un eje Z.
NotMyself
4

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 soluciona mediante la introducción de efectos secundarios. Pero si el rectángulo tenía un setSize (alto, ancho) con la condición previa 0 <alto y 0 <ancho. El método del subtipo derivado requiere height == width; una precondición más fuerte (y eso 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. La solución (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. El derivado lo debilita con altura == ancho.

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

Wouter
fuente
4

Este principio fue introducido por Barbara Liskov en 1987 y extiende el Principio Abierto-Cerrado al enfocarse en el comportamiento de una superclase y sus subtipos.

Su importancia se hace evidente cuando consideramos las consecuencias de violarlo. Considere una aplicación que usa la siguiente clase.

public class Rectangle 
{ 
  private double width;

  private double height; 

  public double Width 
  { 
    get 
    { 
      return width; 
    } 
    set 
    { 
      width = value; 
    }
  } 

  public double Height 
  { 
    get 
    { 
      return height; 
    } 
    set 
    { 
      height = value; 
    } 
  } 
}

Imagine que un día, el cliente exige la capacidad de manipular cuadrados además de rectángulos. Como un cuadrado es un rectángulo, la clase de cuadrado debe derivarse de la clase Rectangle.

public class Square : Rectangle
{
} 

Sin embargo, al hacerlo nos encontraremos con dos problemas:

Un cuadrado no necesita variables de altura y ancho heredadas del rectángulo y esto podría generar un desperdicio significativo en la memoria si tenemos que crear cientos de miles de objetos cuadrados. Las propiedades de establecimiento de ancho y alto heredadas del rectángulo no son apropiadas para un cuadrado ya que el ancho y alto de un cuadrado son idénticos. Para establecer el alto y el ancho en el mismo valor, podemos crear dos nuevas propiedades de la siguiente manera:

public class Square : Rectangle
{
  public double SetWidth 
  { 
    set 
    { 
      base.Width = value; 
      base.Height = value; 
    } 
  } 

  public double SetHeight 
  { 
    set 
    { 
      base.Height = value; 
      base.Width = value; 
    } 
  } 
}

Ahora, cuando alguien establece el ancho de un objeto cuadrado, su altura cambiará en consecuencia y viceversa.

Square s = new Square(); 
s.SetWidth(1); // Sets width and height to 1. 
s.SetHeight(2); // sets width and height to 2. 

Avancemos y consideremos esta otra función:

public void A(Rectangle r) 
{ 
  r.SetWidth(32); // calls Rectangle.SetWidth 
} 

Si pasamos una referencia a un objeto cuadrado en esta función, violaríamos el LSP porque la función no funciona para derivadas de sus argumentos. El ancho y la altura de las propiedades no son polimórficos porque no se declaran virtuales en rectángulo (el objeto cuadrado se dañará porque la altura no se cambiará).

Sin embargo, al declarar que las propiedades del configurador son virtuales, enfrentaremos otra violación, el OCP. De hecho, la creación de un cuadrado de clase derivado está causando cambios en el rectángulo de la clase base.

Ivan Porta
fuente
3

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 ser capaz de reemplazar un objeto de la clase base sin traer ningún error en el sistema o modificar el comportamiento de la clase base "desde aquí . El artículo da un ejemplo de código para violar LSP y arreglarlo.

Prasa
fuente
1
Proporcione los ejemplos de código en stackoverflow.
sebenalern
3

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 tiene la misma longitud 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. Según LSP, las Rectangleinstancias deben ser sustituibles por Rectangleinstancias de subclase. Esto se debe a que estas instancias pasan la verificación de tipo para Rectangleinstancias y, por lo tanto, causarán errores inesperados en su código.

Este fue un ejemplo para la parte de "precondiciones que no pueden fortalecerse en un subtipo" en el artículo wiki . En resumen, violar LSP probablemente causará errores en su código en algún momento.

inf3rno
fuente
3

LSP dice que "los objetos deben ser reemplazables por sus subtipos". Por otro lado, este principio apunta a

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

y el siguiente ejemplo ayuda a comprender mejor el 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();
}
Zahra.HY
fuente
2

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

Puede encontrar una explicación sobre 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.

Ryszard Dżegan
fuente
2

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

Si desconectamos la computadora de la pared (Implementación), ni la toma de corriente de la pared (Interfaz) ni la computadora (Cliente) se descomponen (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 se eliminó el servicio, obtenemos una NullReferenceException. Para hacer frente a 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 desenchufar la computadora de la pared. Debido a que estamos utilizando un acoplamiento flexible, podemos reemplazar una implementación real con algo que no hace nada sin causar problemas.

Raghu Reddy Muttana
fuente
2

El Principio de sustitución de Likov establece que si un módulo de programa está usando una clase Base, entonces la referencia a la clase Base se puede reemplazar con una clase Derivada sin afectar la funcionalidad del módulo del programa.

Intención: los tipos derivados deben ser completamente sustitutos de sus tipos base.

Ejemplo: tipos de retorno covariantes en java.

Ishan Aggarwal
fuente
1

Aquí hay un extracto de esta publicación 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 identificó mal sus 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 de LSP? Si. Esto se debe a que el contrato de la cuenta nos dice que se retiraría una cuenta, pero este no es siempre el caso. Entonces, ¿qué debo hacer para solucionarlo? 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 entre los objetos concretos empleados. Por ejemplo, dado el contrato de la primera cuenta, podría tener el siguiente aspecto:

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 lo que sucede si un objeto que viola el contrato no tiene suficiente dinero. Probablemente simplemente no devuelve nada, probablemente se lanzará una excepción. Entonces tienes que comprobar sihasEnoughMoney() , lo 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 de LSP. Dice "si el comportamiento de un padre cambió en un niño, entonces viola el LSP". Sin embargo, no lo hace, siempre y cuando un niño no viole el contrato de sus padres.

Vadim Samokhin
fuente