SÓLIDO Parte 3 - Sustitución de Liskov y principios de segregación de interfaz

La responsabilidad única (SRP), abierta / cerrada (OCP), Sustitución Liskov, Segregación De Interfaz, y la dependencia de la inversión. Cinco principios ágiles que deberían guiarte cada vez que escribas un código..

Debido a que tanto el Principio de Sustitución de Liskov (LSP) como el Principio de Segregación de la Interfaz (ISP) son bastante fáciles de definir y ejemplificar, en esta lección hablaremos de ambos.

Principio de Sustitución de Liskov (LSP)

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

El concepto de este principio fue introducido por Barbara Liskov en una conferencia magistral de la conferencia de 1987 y más tarde se publicó en un documento junto con Jannette Wing en 1994. Su definición original es la siguiente:

Sea q (x) una propiedad demostrable sobre objetos x de tipo T. Entonces q (y) debería ser demostrable para objetos y de tipo S, donde S es un subtipo de T.

Más adelante, con la publicación de los principios de SOLID por Robert C. Martin en su libro Agile Software Development, Principles, Patterns, and Practices y luego reeditado en la versión C # del libro Agile Principles, Patterns, and Practices in C #, la definición llegó a ser conocido como el principio de sustitución Liskov.

Esto nos lleva a la definición dada por Robert C. Martin:

Los subtipos deben ser sustituibles por sus tipos de base.

Tan simple como eso, una subclase debería anular los métodos de la clase padre de una manera que no rompa la funcionalidad desde el punto de vista del cliente. Aquí hay un ejemplo simple para demostrar el concepto..

clase Vehículo function startEngine () // Funcionalidad predeterminada de arranque del motor function accelerate () // Funcionalidad predeterminada de aceleración

Dado una clase Vehículo - puede ser abstracto y dos implementaciones:

la clase Coche amplía el Vehículo function startEngine () $ this-> engagementIgnition (); parent :: startEngine ();  función privada dedignIgnition () // procedimiento de encendido clase ElectricBus extiende Vehículo función acelera () $ this-> IncreaseVoltage (); $ this-> connectIndividualEngines ();  private function IncreaseVoltage () // Lógica eléctrica private function connectIndividualEngines () // Connection logic

Una clase cliente debe poder usar cualquiera de ellos, si puede usar Vehículo.

class Driver function go (Vehicle $ v) $ v-> startEngine (); $ v-> acelerar (); 

Lo que nos lleva a una implementación simple del Patrón de Diseño de Método de Plantilla como lo usamos en el tutorial de OCP.


Basándonos en nuestra experiencia previa con el Principio Abierto / Cerrado, podemos concluir que el Principio de Sustitución de Liskov está en una fuerte relación con el OCP. De hecho, "una violación de LSP es una violación latente de OCP" (Robert C. Martin), y el Patrón de Diseño de Método de Plantilla es un ejemplo clásico de respeto e implementación de LSP, que a su vez es una de las soluciones para respetar OCP también..

El ejemplo clásico de violación de LSP

Para ilustrar esto completamente, usaremos un ejemplo clásico porque es muy significativo y fácil de entender..

clase Rectángulo private $ topLeft; ancho privado $ altura privada $; función pública setHeight ($ altura) $ this-> altura = $ altura;  función pública getHeight () return $ this-> height;  función pública setWidth ($ width) $ this-> width = $ width;  función pública getWidth () return $ this-> width; 

Comenzamos con una forma geométrica básica, una Rectángulo. Es solo un objeto de datos simple con los que establecen y obtienen para anchura y altura. Imagine que nuestra aplicación está funcionando y ya está implementada en varios clientes. Ahora necesitan una nueva característica. Necesitan poder manipular los cuadrados..

En la vida real, en geometría, un cuadrado es una forma particular de rectángulo. Así que podríamos intentar implementar una Cuadrado clase que extiende un Rectángulo clase. Con frecuencia se dice que una clase infantil es un clase principal, y esta expresión también se ajusta a LSP, al menos a primera vista.


Pero es un Cuadrado realmente un Rectángulo en programación?

la clase Square extiende el rectángulo función pública setHeight ($ value) $ this-> width = $ value; $ this-> height = $ value;  función pública setWidth ($ value) $ this-> width = $ value; $ this-> height = $ value; 

Un cuadrado es un rectángulo con igual ancho y alto, y podríamos hacer una implementación extraña como en el ejemplo anterior. Podríamos sobrescribir ambos setters para establecer la altura y el ancho. Pero, ¿cómo afectaría eso al código del cliente??

clase Cliente función areaVerifier (Rectángulo $ r) $ r-> setWidth (5); $ r-> setHeight (4); if ($ r-> area ()! = 20) lanza una nueva excepción ('Bad area!');  devuelve true; 

Es concebible tener una clase cliente que verifique el área del rectángulo y arroje una excepción si está equivocada.

área de función () return $ this-> width * $ this-> height; 

Por supuesto añadimos el método anterior a nuestro Rectángulo clase para proporcionar el area.

la clase LspTest extiende PHPUnit_Framework_TestCase function testRectangleArea () $ r = new Rectangle (); $ c = nuevo cliente (); $ this-> assertTrue ($ c-> areaVerifier ($ r)); 

Y creamos una prueba simple enviando un objeto de rectángulo vacío al verificador de área y la prueba pasa. Si nuestro Cuadrado clase está correctamente definida, enviándola al cliente areaVerifier () No debe romper su funcionalidad. Después de todo, un Cuadrado es un Rectángulo En todo sentido matemático. Pero es nuestra clase?

function testSquareArea () $ r = new Square (); $ c = nuevo cliente (); $ this-> assertTrue ($ c-> areaVerifier ($ r)); 

Probarlo es muy fácil y se rompe a lo grande. Nos lanzamos una excepción cuando ejecutamos la prueba anterior.

PHPUnit 3.7.28 por Sebastian Bergmann. Excepción: mala zona! # 0 / paht /: /… /… /LspTest.php(18): Client-> areaVerifier (Object (Square)) # 1 [función interna]: LspTest-> testSquareArea ()

Entonces nuestro Cuadrado clase no es una Rectángulo después de todo. Rompe las leyes de la geometría. Falla y viola el Principio de Sustitución de Liskov..

Especialmente me encanta este ejemplo porque no solo viola el LSP, sino que también demuestra que la programación orientada a objetos no se trata de asignar la vida real a los objetos. Cada objeto en nuestro programa debe ser una abstracción sobre un concepto. Si tratamos de asignar objetos reales uno a uno a objetos programados, casi siempre fallaremos.

El principio de segregación de interfaz

El principio de responsabilidad única es sobre los actores y la arquitectura de alto nivel. El principio abierto / cerrado trata sobre el diseño de la clase y las extensiones de funciones. El Principio de Sustitución de Liskov se trata de subtipos y herencia. El Principio de Segregación de Interfaz (ISP) trata sobre la lógica empresarial a la comunicación con los clientes.

En todas las aplicaciones modulares debe haber algún tipo de interfaz en la que el cliente pueda confiar. Estas pueden ser entidades de interfaz reales u otros objetos clásicos que implementan patrones de diseño como Fachadas. No importa qué solución se utiliza. Siempre tiene el mismo alcance: comunicarse con el código del cliente sobre cómo usar el módulo. Estas interfaces pueden residir entre diferentes módulos en la misma aplicación o proyecto, o entre un proyecto como una biblioteca de terceros que sirve a otro proyecto. Una vez más, no importa. La comunicación es comunicación y los clientes son clientes, independientemente de los individuos reales que escriben el código.

Entonces, ¿cómo debemos definir estas interfaces? Podríamos pensar en nuestro módulo y exponer todas las funcionalidades que queremos que ofrezca..


Esto parece un buen comienzo, una excelente manera de definir lo que queremos implementar en nuestro módulo. ¿O es eso? Un comienzo como este llevará a una de dos posibles implementaciones:

  • Un gran Coche o Autobús clase de implementación de todos los métodos en el Vehículo interfaz. Solo las dimensiones absolutas de tales clases deberían decirnos que las evitemos a toda costa..
  • O, muchas clases pequeñas como Control de luces, Control de velocidad, o RadioCD Todos están implementando toda la interfaz, pero en realidad proporcionan algo útil solo para las partes que implementan..

Es obvio que ninguna de las soluciones es aceptable para implementar nuestra lógica de negocios..


Podríamos tomar otro enfoque. Rompe la interfaz en piezas, especializada para cada implementación. Esto ayudaría a usar clases pequeñas que se preocupan por su propia interfaz. Los objetos que implementan las interfaces serán utilizados por los diferentes tipos de vehículos, como el automóvil en la imagen de arriba. El coche utilizará las implementaciones pero dependerá de las interfaces. Por lo tanto, un esquema como el de abajo puede ser aún más expresivo..


Pero esto cambia fundamentalmente nuestra percepción de la arquitectura. los Coche Se convierte en el cliente en lugar de la implementación. Todavía queremos proporcionar a nuestros clientes formas de usar nuestro módulo completo, que es un tipo de vehículo.


Supongamos que resolvimos el problema de implementación y tenemos una lógica empresarial estable. Lo más fácil es proporcionar una única interfaz con todas las implementaciones y dejar que los clientes, en nuestro caso Estación de autobuses, Autopista, Conductor y así sucesivamente, para usar lo que quiera de la implementación de la interfaz. Básicamente, esto cambia la responsabilidad de selección de comportamiento a los clientes. Puede encontrar este tipo de solución en muchas aplicaciones antiguas..

El principio de segregación de interfaz (ISP) establece que ningún cliente debe ser obligado a depender de los métodos que no utiliza..

Sin embargo, esta solución tiene sus problemas. Ahora todos los clientes dependen de todos los métodos. ¿Por qué debería un Estación de autobuses ¿Depende del estado de las luces del bus, o de los canales de radio seleccionados por el conductor? No debería. Pero ¿y si lo hace? ¿Importa? Bueno, si pensamos en el Principio de Responsabilidad Única, es un concepto hermano de éste. Si Estación de autobuses depende de muchas implementaciones individuales, que ni siquiera utiliza, puede requerir cambios si cambia alguna de las implementaciones pequeñas individuales. Esto es especialmente cierto para los lenguajes compilados, pero todavía podemos ver el efecto del Control de luz cambio impactante Estación de autobuses. Estas cosas nunca deben pasar.

Las interfaces pertenecen a sus clientes y no a las implementaciones. Por lo tanto, siempre debemos diseñarlos de manera que se adapte mejor a nuestros clientes. Algunas veces podemos, otras veces no podemos conocer exactamente a nuestros clientes. Pero cuando podamos, debemos romper nuestras interfaces en muchas más pequeñas, para que satisfagan mejor las necesidades exactas de nuestros clientes..


Por supuesto, esto conducirá a cierto grado de duplicación. ¡Pero recuerda! Las interfaces son simplemente definiciones de nombres de funciones simples. No hay implementación de ningún tipo de lógica en ellos. Así que las duplicaciones son pequeñas y manejables..

Entonces, tenemos la gran ventaja de que los clientes dependen solo y solo de lo que realmente necesitan y usan. En algunos casos, los clientes pueden usar y necesitar varias interfaces, eso está bien, siempre y cuando utilicen todos los métodos de todas las interfaces de las que dependen..

Otro buen truco es que en nuestra lógica de negocios, una sola clase puede implementar varias interfaces si es necesario. Por lo tanto, podemos proporcionar una implementación única para todos los métodos comunes entre las interfaces. Las interfaces segregadas también nos obligarán a pensar más en nuestro código desde el punto de vista del cliente, lo que a su vez conducirá a un acoplamiento suelto y pruebas fáciles. Entonces, no solo hemos mejorado nuestro código para nuestros clientes, también lo hemos hecho más fácil para nosotros mismos para entender, probar e implementar.

Pensamientos finales

El LSP nos enseñó por qué la realidad no se puede representar como una relación uno a uno con los objetos programados y cómo los subtipos deben respetar a sus padres. También lo planteamos a la luz de los otros principios que ya conocíamos..

El ISP nos enseña a respetar a nuestros clientes más de lo que creíamos necesario. Respetar sus necesidades hará que nuestro código sea mejor y que nuestras vidas como programadores sean más fáciles.

Gracias por tu tiempo.