LSP vs OCP / Liskov Substitución VS Abrir Cerrar

48

Estoy tratando de entender los principios SÓLIDOS de OOP y he llegado a la conclusión de que LSP y OCP tienen algunas similitudes (si no decir más).

El principio abierto / cerrado establece que "las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para la extensión, pero cerradas para la modificación".

LSP en palabras simples establece que cualquier instancia de Foopuede ser reemplazada por cualquier instancia de la Barcual se deriva Fooy el programa funcionará de la misma manera.

No soy un programador profesional de OOP, pero me parece que el LSP solo es posible si Bar, derivado de Foo, no cambia nada en él, sino que solo lo extiende. Eso significa que, en particular, el programa LSP es verdadero solo cuando OCP es verdadero y OCP es verdadero solo si LSP es verdadero. Eso significa que son iguales.

Corrígeme si estoy equivocado. Realmente quiero entender estas ideas. Muchas gracias por una respuesta.

Kolyunya
fuente
44
Esta es una interpretación muy estrecha de ambos conceptos. Abierto / cerrado puede mantenerse pero aún violar LSP. Los ejemplos de Rectángulo / Cuadrado o Elipse / Círculo son buenas ilustraciones. Ambos se adhieren a OCP, pero ambos violan LSP.
Joel Etherton
1
El mundo (o al menos Internet) está confundido sobre esto. kirkk.com/modularity/2009/12/solid-principles-of-class-design . Este tipo dice que la violación de LSP también es violación de OCP. Y luego, en el libro "Diseño de ingeniería de software: teoría y práctica" en la página 156, el autor da un ejemplo de algo que se adhiere a OCP pero viola el LSP. He renunciado a esto.
Manoj R
@JoelEtherton Esos pares solo violan LSP si son mutables. En el caso inmutable, derivar Squarede Rectangleno viola LSP. (Pero probablemente sigue siendo un mal diseño en el caso inmutable, ya que puede tener cuadrados Rectangleque Squareno coinciden con las matemáticas)
CodesInChaos
Analogía simple (desde el punto de vista de una biblioteca escritora-usuario). LSP es como vender un producto (biblioteca) que dice implementar el 100% de lo que dice (en la interfaz o el manual del usuario), pero en realidad no (o no coincide con lo que se dice). OCP es como vender un producto (biblioteca) con la promesa de que se puede actualizar (ampliar) cuando sale una nueva funcionalidad (como el firmware), pero en realidad no se puede actualizar sin un servicio de fábrica.
rwong

Respuestas:

119

Gosh, hay algunos conceptos erróneos extraños sobre qué OCP y LSP y algunos se deben a la falta de coincidencia de algunas terminologías y ejemplos confusos. Ambos principios son solo la "misma cosa" si los implementa de la misma manera. Los patrones generalmente siguen los principios de una forma u otra con pocas excepciones.

Las diferencias se explicarán más abajo, pero primero echemos un vistazo a los principios mismos:

Principio Abierto-Cerrado (OCP)

Según el tío Bob :

Debería poder extender el comportamiento de una clase, sin modificarlo.

Tenga en cuenta que la palabra extender en este caso no significa necesariamente que deba subclasificar la clase real que necesita el nuevo comportamiento. ¿Ves cómo mencioné en la primera falta de coincidencia de terminología? La palabra clave extendsolo significa subclases en Java, pero los principios son más antiguos que Java.

El original vino de Bertrand Meyer en 1988:

Las entidades de software (clases, módulos, funciones, etc.) deben estar abiertas para extensión, pero cerradas para modificación.

Aquí es mucho más claro que el principio se aplica a las entidades de software . Un mal ejemplo sería anular la entidad de software ya que está modificando el código por completo en lugar de proporcionar algún punto de extensión. El comportamiento de la entidad de software en sí debería ser extensible y un buen ejemplo de esto es la implementación del patrón de estrategia (porque es el más fácil de mostrar del conjunto de patrones de GoF en mi humilde opinión):

// Context is closed for modifications. Meaning you are
// not supposed to change the code here.
public class Context {

    // Context is however open for extension through
    // this private field
    private IBehavior behavior;

    // The context calls the behavior in this public 
    // method. If you want to change this you need
    // to implement it in the IBehavior object
    public void doStuff() {
        if (this.behavior != null)
            this.behavior.doStuff();
    }

    // You can dynamically set a new behavior at will
    public void setBehavior(IBehavior behavior) {
        this.behavior = behavior;
    }
}

// The extension point looks like this and can be
// subclassed/implemented
public interface IBehavior {
    public void doStuff();
}

En el ejemplo anterior, el Contextestá bloqueado para modificaciones adicionales. La mayoría de los programadores probablemente desearían subclasificar la clase para extenderla, pero aquí no lo hacemos porque supone que su comportamiento se puede cambiar a través de cualquier cosa que implemente la IBehaviorinterfaz.

Es decir, la clase de contexto está cerrada para modificación pero abierta para extensión . En realidad, sigue otro principio básico porque estamos poniendo el comportamiento con la composición del objeto en lugar de la herencia:

"Favorecer ' composición de objeto ' sobre ' herencia de clase '". (Banda de cuatro 1995: 20)

Dejaré que el lector lea sobre ese principio ya que está fuera del alcance de esta pregunta. Para continuar con el ejemplo, digamos que tenemos las siguientes implementaciones de la interfaz IBehavior:

public class HelloWorldBehavior implements IBehavior {
    public void doStuff() {
        System.println("Hello world!");
    }
}

public class GoodByeBehavior implements IBehavior {
    public void doStuff() {
        System.out.println("Good bye cruel world!");
    }
}

Usando este patrón podemos modificar el comportamiento del contexto en tiempo de ejecución, a través del setBehaviormétodo como punto de extensión.

// in your main method
Context c = new Context();

c.setBehavior(new HelloWorldBehavior());
c.doStuff();
// prints out "Hello world!"

c.setBehavior(new GoodByeBehavior());
c.doStuff();
// prints out "Good bye cruel world!"

Por lo tanto, siempre que desee ampliar la clase de contexto "cerrado", hágalo subclasificando su dependencia colaboradora "abierta". Claramente, esto no es lo mismo que subclasificar el contexto en sí, pero es OCP. LSP tampoco menciona esto.

Extendiéndose con Mixins en lugar de Herencia

Hay otras formas de hacer OCP que no sean subclases. Una forma es mantener sus clases abiertas para la extensión mediante el uso de mixins . Esto es útil, por ejemplo, en lenguajes basados ​​en prototipos más que en clases. La idea es enmendar un objeto dinámico con más métodos o atributos según sea necesario, en otras palabras, objetos que se mezclan o "mezclan" con otros objetos.

Aquí hay un ejemplo de JavaScript de un mixin que representa una plantilla HTML simple para anclas:

// The mixin, provides a template for anchor HTML elements, i.e. <a>
var LinkMixin = {
    render: function() {
        return '<a href="' + this.link +'">'
            + this.content 
            + '</a>;
    }
}

// Constructor for a youtube link
var YoutubeLink = function(content, youtubeId) {
    this.content = content;
    this.setLink(this.youtubeId);
};
// Methods are added to the prototype
YoutubeLink.prototype = {
    setLink: function(youtubeid) {
        this.link = 'http://www.youtube.com/watch?v=' + youtubeid;
    }
};
// Extend YoutubeLink prototype with the LinkMixin using
// underscore/lodash extend
_.extend(YoutubeLink.protoype, LinkMixin);

// When used:
var ytLink = new YoutubeLink("Cool Movie!", "idOaZpX8lnA");

console.log(ytLink.render());
// will output: 
// <a href="http://www.youtube.com/watch?=vidOaZpX8lnA">Cool Movie!</a>

La idea es extender los objetos dinámicamente y la ventaja de esto es que los objetos pueden compartir métodos incluso si están en dominios completamente diferentes. En el caso anterior, puede crear fácilmente otros tipos de anclajes html extendiendo su implementación específica con LinkMixin.

En términos de OCP, los "mixins" son extensiones. En el ejemplo anterior, YoutubeLinknuestra entidad de software está cerrada para modificaciones, pero abierta para extensiones mediante el uso de mixins. La jerarquía de objetos se aplana, lo que hace que sea imposible verificar los tipos. Sin embargo, esto no es realmente algo malo, y explicaré más adelante que buscar tipos es generalmente una mala idea y rompe la idea con polimorfismo.

Tenga en cuenta que es posible hacer herencia múltiple con este método ya que la mayoría de las extendimplementaciones pueden mezclar múltiples objetos:

_.extend(MyClass, Mixin1, Mixin2 /* [, ...] */);

Lo único que debe tener en cuenta es no colisionar los nombres, es decir, los mixins definen el mismo nombre de algunos atributos o métodos, ya que se anularán. En mi humilde experiencia, esto no es un problema y si sucede, es una indicación de un diseño defectuoso.

Principio de sustitución de Liskov (LSP)

El tío Bob lo define simplemente por:

Las clases derivadas deben ser sustituibles por sus clases base.

Este principio es antiguo, de hecho, la definición del tío Bob no diferencia los principios, ya que eso hace que LSP aún esté estrechamente relacionado con OCP por el hecho de que, en el ejemplo de Estrategia anterior, se usa el mismo supertipo ( IBehavior). Así que veamos su definición original por Barbara Liskov y veamos si podemos encontrar algo más sobre este principio que se parezca a un teorema matemático:

Lo que se quiere aquí es algo así como la siguiente propiedad de sustitución: si para cada objeto o1de tipo Shay un objeto o2de tipo Ttal que para todos los programas Pdefinidos en términos de T, el comportamiento de Pno cambia cuando o1se sustituye por, o2entonces Ses un subtipo de T.

Hagamos caso omiso de esto por un tiempo, note que no menciona clases en absoluto. En JavaScript, puedes seguir LSP aunque no esté explícitamente basado en clases. Si su programa tiene una lista de al menos un par de objetos JavaScript que:

  • necesita ser calculado de la misma manera,
  • tener el mismo comportamiento, y
  • son de alguna manera completamente diferentes

... entonces se considera que los objetos tienen el mismo "tipo" y realmente no importa para el programa. Esto es esencialmente polimorfismo . En sentido genérico; No debería necesitar saber el subtipo real si está utilizando su interfaz. OCP no dice nada explícito sobre esto. También señala un error de diseño que la mayoría de los programadores novatos hacen:

Cada vez que sienta la necesidad de verificar el subtipo de un objeto, lo más probable es que lo haga INCORRECTAMENTE.

De acuerdo, por lo que no podría ser mal todo el tiempo, pero si usted tiene la necesidad de hacer algo de comprobación de tipos con instanceofo enumeraciones, que podría estar haciendo el programa un poco más enrevesado por sí mismo de lo que debe ser. Pero este no es siempre el caso; Hacks rápidos y sucios para hacer que las cosas funcionen es una buena concesión para hacer en mi mente si la solución es lo suficientemente pequeña, y si practicas una refactorización despiadada , puede mejorar una vez que los cambios lo exijan.

Hay formas de evitar este "error de diseño", dependiendo del problema real:

  • La superclase no está llamando a los requisitos previos, obligando a la persona que llama a hacerlo en su lugar.
  • A la superclase le falta un método genérico que necesita la persona que llama.

Ambos son "errores" comunes de diseño de código. Puede realizar un par de refactorizaciones diferentes, como el método pull-up o refactorizar un patrón como el patrón Visitor .

En realidad, me gusta mucho el patrón Visitor, ya que puede encargarse de los grandes espaguetis con sentencias if y es más sencillo de implementar de lo que pensarías en el código existente. Digamos que tenemos el siguiente contexto:

public class Context {

    public void doStuff(string query) {

        // outcome no. 1
        if (query.Equals("Hello")) {
            System.out.println("Hello world!");
        } 

        // outcome no. 2
        else if (query.Equals("Bye")) {
            System.out.println("Good bye cruel world!");
        }

        // a change request may require another outcome...

    }

}

// usage:
Context c = new Context();

c.doStuff("Hello");
// prints "Hello world"

c.doStuff("Bye");
// prints "Bye"

Los resultados de la declaración if se pueden traducir a sus propios visitantes, ya que cada uno depende de alguna decisión y algún código para ejecutar. Podemos extraer estos de esta manera:

public interface IVisitor {
    public bool canDo(string query);
    public void doStuff();
}

// outcome 1
public class HelloVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Hello");
    }
    public void doStuff() {
         System.out.println("Hello World");
    }
}

// outcome 2
public class ByeVisitor implements IVisitor {
    public bool canDo(string query) {
        return query.Equals("Bye");
    }
    public void doStuff() {
        System.out.println("Good bye cruel world");
    }
}

En este punto, si el programador no sabía sobre el patrón Visitante, implementaría la clase Contexto para verificar si es de cierto tipo. Debido a que las clases Visitor tienen un canDométodo booleano , el implementador puede usar esa llamada al método para determinar si es el objeto correcto para hacer el trabajo. La clase de contexto puede usar todos los visitantes (y agregar nuevos) así:

public class Context {
    private ArrayList<IVisitor> visitors = new ArrayList<IVisitor>();

    public Context() {
        visitors.add(new HelloVisitor());
        visitors.add(new ByeVisitor());
    }

    // instead of if-statements, go through all visitors
    // and use the canDo method to determine if the 
    // visitor object is the right one to "visit"
    public void doStuff(string query) {
        for(IVisitor visitor : visitors) {
            if (visitor.canDo(query)) {
                visitor.doStuff();
                break;
                // or return... it depends if you have logic 
                // after this foreach loop
            }
        }
    }

    // dynamically adds new visitors
    public void addVisitor(IVisitor visitor) {
        if (visitor != null)
            visitors.add(visitor);
    }
}

Ambos patrones siguen OCP y LSP, sin embargo, ambos señalan cosas diferentes sobre ellos. Entonces, ¿cómo se ve el código si viola uno de los principios?

Violar un principio pero seguir el otro

Hay maneras de romper uno de los principios, pero aún se debe seguir el otro. Los ejemplos a continuación parecen inventados, por una buena razón, pero en realidad los he visto aparecer en el código de producción (e incluso peor):

Sigue OCP pero no LSP

Digamos que tenemos el código dado:

public interface IPerson {}

public class Boss implements IPerson {
    public void doBossStuff() { ... }
}

public class Peon implements IPerson {
    public void doPeonStuff() { ... }
}

public class Context {
    public Collection<IPerson> getPersons() { ... }
}

Este código sigue el principio abierto-cerrado. Si llamamos al GetPersonsmétodo del contexto , obtendremos un grupo de personas, todas con sus propias implementaciones. Eso significa que IPerson está cerrado por modificación, pero abierto por extensión. Sin embargo, las cosas se vuelven oscuras cuando tenemos que usarlo:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // now we have to check the type... :-P
    if (person instanceof Boss) {
        ((Boss) person).doBossStuff();
    }
    else if (person instanceof Peon) {
        ((Peon) person).doPeonStuff();
    }
}

¡Tienes que hacer la verificación de tipos y la conversión de tipos! ¿Recuerdas cómo mencioné anteriormente cómo la verificación de tipo es algo malo ? ¡Oh no! Pero no temas, como también se mencionó anteriormente, o bien realizas algunas refactorizaciones pull-up o implementas un patrón de visitante. En este caso, simplemente podemos hacer una refactorización pull-up después de agregar un método general:

public class Boss implements IPerson {
    // we're adding this general method
    public void doStuff() {
        // that does the call instead
        this.doBossStuff();
    }
    public void doBossStuff() { ... }
}


public interface IPerson {
    // pulled up method from Boss
    public void doStuff();
}

// do the same for Peon

El beneficio ahora es que ya no necesita saber el tipo exacto, siguiendo LSP:

// in some routine that needs to do stuff with 
// a collection of IPerson:
Collection<IPerson> persons = context.getPersons();
for (IPerson person : persons) {
    // yay, no type checking!
    person.doStuff();
}

Sigue LSP pero no OCP

Veamos un código que sigue a LSP pero no a OCP, es un poco artificial, pero tenga paciencia sobre este, es un error muy sutil:

public class LiskovBase {
    public void doStuff() {
        System.out.println("My name is Liskov");
    }
}

public class LiskovSub extends LiskovBase {
    public void doStuff() {
        System.out.println("I'm a sub Liskov!");
    }
}

public class Context {
    private LiskovBase base;

    // the good stuff
    public void doLiskovyStuff() {
        base.doStuff();
    }

    public void setBase(LiskovBase base) { this.base = base }
}

El código hace LSP porque el contexto puede usar LiskovBase sin conocer el tipo real. Usted pensaría que este código también sigue a OCP, pero mire de cerca, ¿está realmente cerrada la clase ? ¿Qué pasa si el doStuffmétodo hizo algo más que imprimir una línea?

La respuesta si sigue a OCP es simplemente: NO , no es porque en este diseño de objeto se nos requiere anular el código por completo con otra cosa. Esto abre la lata de gusanos de cortar y pegar, ya que debe copiar el código de la clase base para que todo funcione. El doStuffmétodo seguro está abierto para la extensión, pero no estaba completamente cerrado para la modificación.

Podemos aplicar el patrón de método de plantilla en esto. El patrón del método de plantilla es tan común en los marcos que podría haberlo estado utilizando sin saberlo (por ejemplo, componentes de Java Swing, formularios y componentes de C #, etc.). Aquí hay una forma de cerrar el doStuffmétodo de modificación y asegurarse de que permanezca cerrado marcándolo con la finalpalabra clave de java . Esa palabra clave evita que cualquiera pueda subclasificar la clase aún más (en C # puede usar sealedpara hacer lo mismo).

public class LiskovBase {
    // this is now a template method
    // the code that was duplicated
    public final void doStuff() {
        System.out.println(getStuffString());
    }

    // extension point, the code that "varies"
    // in LiskovBase and it's subclasses
    // called by the template method above
    // we expect it to be virtual and overridden
    public string getStuffString() {
        return "My name is Liskov";
    }
}

public class LiskovSub extends LiskovBase {
    // the extension overridden
    // the actual code that varied
    public string getStuffString() {
        return "I'm sub Liskov!";
    }
}

Este ejemplo sigue a OCP y parece una tontería, lo cual es, pero imagina que se amplió con más código para manejar. Sigo viendo el código implementado en la producción, donde las subclases anulan completamente todo y el código anulado se corta y pega principalmente entre implementaciones. Funciona, pero como con toda la duplicación de código, también es una configuración para las pesadillas de mantenimiento.

Conclusión

Espero que todo esto aclare algunas preguntas sobre OCP y LSP y las diferencias / similitudes entre ellos. Es fácil descartarlos como iguales, pero los ejemplos anteriores deberían mostrar que no lo son.

Tenga en cuenta que, reuniendo del código de muestra anterior:

  • OCP se trata de bloquear el código de trabajo pero aún así mantenerlo abierto de alguna manera con algún tipo de puntos de extensión.

    Esto es para evitar la duplicación de código encapsulando el código que cambia como en el ejemplo del patrón de Método de plantilla. También permite fallar rápidamente, ya que los cambios importantes son dolorosos (es decir, cambiar un lugar, romperlo en cualquier otro lugar). En aras del mantenimiento, el concepto de encapsular el cambio es algo bueno, porque los cambios siempre ocurren.

  • LSP se trata de permitir que el usuario maneje diferentes objetos que implementan un supertipo sin verificar cuál es el tipo real. De esto se trata inherentemente el polimorfismo .

    Este principio proporciona una alternativa para realizar la verificación de tipos y la conversión de tipos, que puede salirse de control a medida que aumenta el número de tipos y puede lograrse mediante la refactorización pull-up o la aplicación de patrones como Visitor.

Spoike
fuente
77
Esta es una buena explicación, porque no simplifica demasiado el OCP al implicar que siempre significa implementación por herencia. Es esa simplificación excesiva la que une OCP y SRP en la mente de algunas personas, cuando realmente pueden ser dos conceptos completamente separados.
Eric King
55
Esta es una de las mejores respuestas de intercambio de pila que he visto. Desearía poder votarlo 10 veces. Bien hecho, y gracias por la excelente explicación.
Bob Horn
Allí, agregué una propaganda en Javascript que no es un lenguaje de programación basado en clases pero que aún puede seguir LSP y editó el texto para que se lea con más fluidez. ¡Uf!
Spoike
Si bien su cita del tío Bob de LSP es correcta (igual que su sitio web), ¿no debería ser al revés? ¿No debería indicar que "las clases base deben ser sustituibles por sus clases derivadas"? En LSP, la prueba de "compatibilidad" se realiza con la clase derivada y no con la clase base. Aún así, no soy un hablante nativo de inglés y creo que puede haber algunos detalles sobre la frase que me falta.
Alfa
@Alpha: Esa es una buena pregunta. La clase base siempre se puede sustituir con sus clases derivadas o, de lo contrario, la herencia no funcionaría. El compilador (al menos en Java y C #) se quejará si deja de lado un miembro (método o atributo / campo) de la clase extendida que necesita implementarse. El objetivo de LSP es evitar que agregue métodos que solo están disponibles localmente en las clases derivadas, ya que eso requiere que el usuario de esas clases derivadas las conozca. A medida que el código crece, dichos métodos serían difíciles de mantener.
Spoike
15

Esto es algo que causa mucha confusión. Prefiero considerar estos principios de manera algo filosófica, porque hay muchos ejemplos diferentes para ellos, y a veces ejemplos concretos realmente no capturan toda su esencia.

Lo que OCP intenta arreglar

Digamos que necesitamos agregar funcionalidad a un programa dado. La forma más fácil de hacerlo, especialmente para las personas que fueron capacitadas para pensar de manera procesal, es agregar una cláusula if cuando sea necesario, o algo por el estilo.

Los problemas con eso son

  1. Cambia el flujo del código de trabajo existente.
  2. Obliga a una nueva ramificación condicional en cada caso. Por ejemplo, supongamos que tiene una lista de libros, y algunos de ellos están en oferta, y desea iterar sobre todos ellos e imprimir su precio, de modo que si están en oferta, el precio impreso incluirá la cadena " (EN VENTA)".

Puede hacer esto agregando un campo adicional a todos los libros llamado "is_on_sale", y luego puede verificar ese campo al imprimir el precio de cualquier libro, o alternativamente , puede crear una instancia de libros en venta desde la base de datos usando un tipo diferente, que imprime "(EN VENTA)" en la cadena de precios (no es un diseño perfecto, pero entrega el punto a casa).

El problema con la primera solución de procedimiento es un campo adicional para cada libro y una complejidad adicional redundante en muchos casos. La segunda solución solo fuerza la lógica donde realmente se requiere.

Ahora considere el hecho de que podría haber muchos casos en los que se requieren diferentes datos y lógica, y verá por qué tener en cuenta OCP al diseñar sus clases, o reaccionar a los cambios en los requisitos, es una buena idea.

A estas alturas ya debe tener la idea principal: intente ponerse en una situación en la que se pueda implementar un nuevo código como extensiones polimórficas, no como modificaciones de procedimiento.

Pero nunca tenga miedo de analizar el contexto y ver si los inconvenientes superan los beneficios, porque incluso un principio como OCP puede hacer un desastre de clase 20 con un programa de 20 líneas, si no se trata con cuidado .

Lo que LSP intenta arreglar

Todos amamos la reutilización de códigos. Una enfermedad que sigue es que muchos programas no lo entienden completamente, hasta el punto de que están factorizando ciegamente líneas de código comunes solo para crear complejidades ilegibles y un acoplamiento estrecho redundante entre módulos que, aparte de unas pocas líneas de código, no tienen nada en común en lo que respecta al trabajo conceptual por hacer.

El mayor ejemplo de esto es la reutilización de la interfaz . Probablemente lo hayas presenciado tú mismo; una clase implementa una interfaz, no porque sea una implementación lógica de la misma (o una extensión en el caso de clases base concretas), sino porque los métodos que declara en ese punto tienen las firmas correctas en lo que respecta.

Pero entonces te encuentras con un problema. Si las clases implementan interfaces solo considerando las firmas de los métodos que declaran, entonces puede pasar instancias de clases de una funcionalidad conceptual a lugares que exigen una funcionalidad completamente diferente, que solo depende de firmas similares.

Eso no es tan horrible, pero causa mucha confusión, y tenemos la tecnología para evitar cometer errores como estos. Lo que debemos hacer es tratar las interfaces como API + Protocol . La API es evidente en las declaraciones, y el protocolo es evidente en los usos existentes de la interfaz. Si tenemos 2 protocolos conceptuales que comparten la misma API, deberían representarse como 2 interfaces diferentes. De lo contrario, quedamos atrapados en el dogmatismo SECO e, irónicamente, solo creamos códigos más difíciles de mantener.

Ahora deberías poder entender la definición perfectamente. LSP dice: No herede de una clase base e implemente la funcionalidad en esas subclases con las que, en otros lugares, que dependen de la clase base, no se llevarán bien.

Ñame Marcovic
fuente
1
Me inscribí solo para poder votar esto y las respuestas de Spoike: gran trabajo.
David Culp
7

Desde mi entendimiento:

OCP dice: "Si va a agregar una nueva funcionalidad, cree una nueva clase que amplíe una existente, en lugar de cambiarla".

LSP dice: "Si crea una nueva clase que amplía una clase existente, asegúrese de que sea completamente intercambiable con su base".

Así que creo que se complementan pero no son iguales.

henginy
fuente
4

Si bien es cierto que OCP y LSP tienen que ver con la modificación, el tipo de modificación del que habla OCP no es del que habla LSP.

La modificación con respecto a OCP es la acción física de un desarrollador que escribe código en una clase existente.

LSP se ocupa de la modificación del comportamiento que trae una clase derivada en comparación con su clase base, y la alteración del tiempo de ejecución de la ejecución del programa que puede ser causada por el uso de la subclase en lugar de la superclase.

Entonces, aunque podrían parecer similares desde la distancia, OCP! = LSP. De hecho, creo que pueden ser los únicos 2 principios SÓLIDOS que no pueden entenderse entre sí.

guillaume31
fuente
2

LSP en palabras simples establece que cualquier instancia de Foo se puede reemplazar con cualquier instancia de Bar que se deriva de Foo sin ninguna pérdida de funcionalidad del programa.

Esto está mal. LSP establece que la clase Bar no debería introducir un comportamiento, eso no se espera cuando el código usa Foo, cuando Bar se deriva de Foo. No tiene nada que ver con la pérdida de funcionalidad. Puede eliminar la funcionalidad, pero solo cuando el código que usa Foo no depende de esta funcionalidad.

Pero al final, esto suele ser difícil de lograr, porque la mayoría de las veces, el código que usa Foo depende de todo su comportamiento. Eliminarlo viola el LSP. Pero simplificarlo así es solo una parte de LSP.

Eufórico
fuente
Un caso muy común es cuando el objeto sustituido elimina los efectos secundarios : por ejemplo. un registrador ficticio que no genera nada, o un objeto simulado utilizado en las pruebas.
Inútil
0

Sobre objetos que pueden violar

Para comprender la diferencia, debe comprender temas de ambos principios. No es una parte abstracta de código o situación que puede violar o no algún principio. Siempre es algún componente específico (función, clase o módulo) el que puede violar OCP o LSP.

Quién puede violar LSP

Uno puede verificar si LSP está roto solo cuando hay una interfaz con algún contrato y una implementación de esa interfaz. Si la implementación no se ajusta a la interfaz o, en términos generales, al contrato, entonces LSP está roto.

El ejemplo más simple:

class Container {
    // Should add the object to the container.
    void addObject(object) {
        internalArray.append(object);
    }

    int size() {
        return internalArray.size();
    }
}

class CustomContainer extends Container {
    @Override void addObject(object) {
        System.console.print("Skipping object! Ha-ha!");
    }
}

void fillWithRandomNumbers(Container container) {
    while (container.size() < 42) {
        container.addObject(Randomizer.getNumber())
    }
}

El contrato establece claramente que addObjectdebe agregar su argumento al contenedor. Y CustomContainerclaramente rompe ese contrato. Por lo tanto, la CustomContainer.addObjectfunción viola LSP. Así, la CustomContainerclase viola LSP. La consecuencia más importante es que CustomContainerno se puede pasar a fillWithRandomNumbers(). Containerno puede ser sustituido con CustomContainer.

Tenga en cuenta un punto muy importante. No es todo este código el que rompe el LSP, es específicamente CustomContainer.addObjecty generalmente el CustomContainerque rompe el LSP. Cuando declara que se viola el LSP, siempre debe especificar dos cosas:

  • La entidad que viola el LSP.
  • El contrato que se rompe por la entidad.

Eso es. Solo un contrato y su implementación. Un downcast en el código no dice nada sobre la violación de LSP.

Quién puede violar OCP

Uno puede verificar si se viola OCP solo cuando hay un conjunto de datos limitado y un componente que maneja los valores de ese conjunto de datos. Si los límites del conjunto de datos pueden cambiar con el tiempo y eso requiere cambiar el código fuente del componente, entonces el componente viola OCP.

Suena complejo Probemos un ejemplo simple:

enum Platform {
    iOS,
    Android
}

class PlatformDescriber {
    String describe(Platform platform) {
        switch (platform) {
            case iOS: return "iPhone OS, v10.0.1";
            case Android: return "Android, v7.1";
        }
    }
}

El conjunto de datos es el conjunto de plataformas compatibles. PlatformDescriberes el componente que maneja los valores de ese conjunto de datos. Agregar una nueva plataforma requiere actualizar el código fuente de PlatformDescriber. Por lo tanto, la PlatformDescriberclase viola OCP.

Otro ejemplo:

class Shop {
    void sellItemToCustomer(item, customer) {
        // some buisiness logic here
        ...
        logger.logItemSold()
    }
}

class Logger {
    void logItemSold() {
        logger.logToStdErr("an item was sold")
        logger.logToRemote("an item was sold")
        logger.logToDatabase("an item was sold")
    }
}

El "conjunto de datos" es el conjunto de canales donde se debe agregar una entrada de registro. Loggeres el componente responsable de agregar entradas a todos los canales. Agregar soporte para otra forma de inicio de sesión requiere actualizar el código fuente de Logger. Por lo tanto, la Loggerclase viola OCP.

Tenga en cuenta que en ambos ejemplos el conjunto de datos no es algo semánticamente fijo. Puede cambiar con el tiempo. Puede surgir una nueva plataforma. Puede surgir un nuevo canal de registro. Si su componente debe actualizarse cuando eso sucede, viola OCP.

Empujando los limites

Ahora la parte difícil. Compare los ejemplos anteriores con los siguientes:

enum GregorianWeekDay {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}

String translateToRussian(GregorianWeekDay weekDay) {
    switch (weekDay) {
        case Monday: return "Понедельник";
        case Tuesday: return "Вторник";
        case Wednesday: return "Среда";
        case Thursday: return "Четверг";
        case Friday: return "Пятница";
        case Saturday: return "Суббота";
        case Sunday: return "Воскресенье";
    }
}

Puede pensar que translateToRussianviola OCP. Pero en realidad no lo es. GregorianWeekDaytiene un límite específico de exactamente 7 días de la semana con nombres exactos. Y lo importante es que estos límites semánticamente no pueden cambiar con el tiempo. Siempre habrá 7 días en la semana gregoriana. Siempre habrá lunes, martes, etc. Este conjunto de datos se arregla semánticamente. No es posible que translateToRussianel código fuente requiera modificaciones. Por lo tanto, OCP no se viola.

Ahora debe quedar claro que una switchdeclaración agotadora no siempre es una indicación de OCP roto.

La diferencia

Ahora siente la diferencia:

  • El tema de LSP es "una implementación de interfaz / contrato". Si la implementación no se ajusta al contrato, entonces rompe el LSP. No es importante si esa implementación puede cambiar con el tiempo o no, si es extensible o no.
  • El tema de OCP es "una forma de responder a un cambio de requisitos". Si el soporte para un nuevo tipo de datos requiere cambiar el código fuente del componente que maneja esos datos, entonces ese componente rompe el OCP. No es importante si el componente rompe su contrato o no.

Estas condiciones son completamente ortogonales.

Ejemplos

En @ respuesta de Spoike la violación de un principio, pero después de la otra parte es totalmente equivocado.

En el primer ejemplo, la forparte -loop está violando claramente el OCP porque no es extensible sin modificación. Pero no hay indicios de violación de LSP. Y ni siquiera está claro si el Contextcontrato permite que getPersons devuelva algo excepto Bosso Peon. Incluso suponiendo un contrato que permita la IPersondevolución de cualquier subclase, no existe una clase que anule esta condición posterior y la viole. Además, si getPersons devolverá una instancia de alguna tercera clase, for-loop hará su trabajo sin ningún fallo. Pero ese hecho no tiene nada que ver con LSP.

Próximo. En el segundo ejemplo, no se viola ni LSP ni OCP. Una vez más, la Contextparte simplemente no tiene nada que ver con LSP: sin contrato definido, sin subclases, sin anulaciones de ruptura. No es Contextquien debe obedecer a LSP, no LiskovSubdebe romper el contrato de su base. En cuanto a OCP, ¿ está realmente cerrada la clase? - sí lo es. No se necesita ninguna modificación para extenderlo. Obviamente, el nombre del punto de extensión indica Haz lo que quieras, sin límites . El ejemplo no es muy útil en la vida real, pero claramente no viola OCP.

Tratemos de hacer algunos ejemplos correctos con verdadera violación de OCP o LSP.

Siga OCP pero no LSP

interface Platform {
    String name();
    String version();
}

class iOS implements Platform {
    @Override String name() { return "iOS"; }
    @Override String version() { return "10.0.1"; }
}

interface PlatformSerializer {
    String toJson(Platform platform);
}

class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return platform.name() + ", v" + platform.version();
    }
}

Aquí, HumanReadablePlatformSerializerno requiere ninguna modificación cuando se agrega una nueva plataforma. Por lo tanto, sigue OCP.

Pero el contrato requiere que toJsondebe devolver un JSON formateado correctamente. La clase no hace eso. Debido a eso, no se puede pasar a un componente que se utiliza PlatformSerializerpara formatear el cuerpo de una solicitud de red. Por lo tanto, HumanReadablePlatformSerializerviola LSP.

Siga LSP pero no OCP

Algunas modificaciones al ejemplo anterior:

class Android implements Platform {
    @Override String name() { return "Android"; }
    @Override String version() { return "7.1"; }
}
class HumanReadablePlatformSerializer implements PlatformSerializer {
    String toJson(Platform platform) {
        return "{ "
                + "\"name\": \"" + platform.name() + "\","
                + "\"version\": \"" + platform.version() + "\","
                + "\"most-popular\": " + isMostPopular(platform) + ","
                + "}"
    }

    boolean isMostPopular(Platform platform) {
        return (platform instanceof Android)
    }
}

El serializador devuelve una cadena JSON formateada correctamente. Entonces, no hay violación de LSP aquí.

Pero existe el requisito de que si la plataforma se usa en gran medida, debería haber una indicación correspondiente en JSON. En este ejemplo, la HumanReadablePlatformSerializer.isMostPopularfunción viola OCP porque algún día iOS se convirtió en la plataforma más popular. Formalmente significa que el conjunto de plataformas más utilizadas se define como "Android" por ahora, y isMostPopularmaneja de manera inadecuada ese conjunto de datos. El conjunto de datos no está semánticamente fijo y puede cambiar libremente con el tiempo. HumanReadablePlatformSerializerSe requiere actualizar el código fuente en caso de un cambio.

También puede notar una violación de la responsabilidad individual en este ejemplo. Lo hice intencionalmente para poder demostrar ambos principios sobre la misma entidad sujeto. Para arreglar SRP, puede extraer la isMostPopularfunción a alguna externa Helpery agregarle un parámetro PlatformSerializer.toJson. Pero esa es otra historia.

mekarthedev
fuente
0

LSP y OCP no son lo mismo.

LSP habla sobre la corrección del programa tal como está . Si una instancia de un subtipo rompería la corrección del programa cuando se sustituye en el código por tipos de antepasados, entonces usted ha demostrado una violación de LSP. Es posible que deba simular una prueba para mostrar esto, pero no tendría que cambiar la base de código subyacente. Está validando el programa en sí para ver si cumple con LSP.

OCP habla sobre la corrección de los cambios en el código del programa, el delta de una versión de origen a otra. El comportamiento no debe modificarse. Solo debe extenderse. El ejemplo clásico es la suma de campo. Todos los campos existentes continúan funcionando como antes. El nuevo campo solo agrega funcionalidad. Sin embargo, eliminar un campo suele ser una violación de OCP. Aquí está validando el delta de la versión del programa para ver si cumple con OCP.

Esa es la diferencia clave entre LSP y OCP. El primero valida solo la base del código tal como está , el segundo valida solo el delta base del código de una versión a la siguiente . Como tales, no pueden ser lo mismo, se definen como validar cosas diferentes.

Te daré una prueba más formal: decir "LSP implica OCP" implicaría un delta (porque OCP requiere uno distinto al caso trivial), pero LSP no requiere uno. Entonces eso es claramente falso. Por el contrario, podemos refutar "OCP implica LSP" simplemente diciendo que OCP es una declaración sobre deltas, por lo tanto, no dice nada acerca de una declaración sobre un programa en el lugar. Esto se deduce del hecho de que puede crear CUALQUIER delta comenzando con CUALQUIER programa establecido. Son totalmente independientes.

Brad Thomas
fuente
-1

Lo miraría desde el punto de vista del cliente. si el Cliente está usando características de una interfaz, e internamente esa característica ha sido implementada por la Clase A. Supongamos que hay una clase B que extiende la clase A, luego mañana si elimino la clase A de esa interfaz y agrego la clase B, entonces la clase B debería También proporcionan las mismas características al cliente. El ejemplo estándar es una clase Duck que nada, y si ToyDuck extiende Duck, entonces también debe nadar y no se queja de que no puede nadar, de lo contrario ToyDuck no debería haber extendido la clase Duck.

AKS
fuente
Sería muy constructivo si la gente también comentara mientras rechaza cualquier respuesta. Después de todo, todos estamos aquí para compartir conocimientos, y simplemente juzgar sin una razón adecuada no servirá para nada.
AKS
Esto no parece ofrecer nada sustancial sobre los puntos hechos y explicados en las 6 respuestas anteriores
mosquito
1
Parece que solo estás explicando uno de los principios, el L, creo. Para qué es, está bien, pero la pregunta pedía una comparación / contraste de dos principios diferentes. Probablemente por eso alguien lo rechazó.
StarWeaver