Table of Contents
Diseño de sistemas extensibles utilizando el Principio de sustitución Liskov
La construcción de sistemas extensibles sigue siendo un reto persistente en la ingeniería de software. A medida que evolucionan los requisitos, la capacidad de añadir nuevos comportamientos sin reescribir código existente separa arquitecturas mantenibles de los más frágiles. El Principio de sustitución Liskov (LSP) proporciona una base rigurosa para lograr este objetivo mediante la definición de reglas precisas para el comportamiento de subtipo.
Comprender el principio de sustitución de Liskov
Barbara Liskov introdujo el principio que lleva su nombre en un documento de conferencia de 1987 titulado "Data Abstraction and Hierarchy." La definición formal declara: "Si para cada objeto o1 del tipo S hay un objeto o2 del tipo T tal que para todos los programas P definidos en términos de T, el comportamiento de P se comporta sin cambios cuando o1 se sustituye por o2, entonces S es un subtipo derivado de trabajo.
El principio se extiende más allá de las simples firmas de método. LSP exige compatibilidad conductual: la subclase no sólo debe tener los mismos métodos sino también honrar las suposiciones que el código del cliente hace acerca de la clase base. Esto incluye precondiciones (lo que debe ser cierto antes de llamar un método), condiciones post (lo que debe ser cierto después), e invariantes (condiciones que permanecen constantes durante toda la vida del objeto).
Una manera concreta de pensar en LSP es la relación "is-a". Si usted afirma que un es un , entonces cada función que opera en un debe funcionar sin cambios con una . La violación clásica es el problema del Rectangle-Square, donde un [FC4] se extiende ancho [LT] [6]
Las cuatro condiciones clave de LSP
Para garantizar la compatibilidad con subtipos conductuales, LSP impone cuatro condiciones específicas que las subclases deben satisfacer. Estas condiciones, derivadas del principio de Diseño por Contrato, proporcionan una lista de verificación para evaluar jerarquías de clase.
No se pueden fortalecer las condiciones previas
Una condición previa es una condición que debe tener antes de que se invoque un método. Si el método de clase base permite que el parámetro sea cualquier entero, una subclase que restrinja a números enteros positivos fortalece la condición previa. El código de cliente escrito contra la clase base puede pasar un número entero negativo y esperar que funcione, pero la subclase lo rechazará.
Las condiciones posteriores no pueden debilitarse
Las condiciones de publicación definen lo que el método garantiza después de la ejecución. Si el método de clase base garantiza un valor de retorno no nulo, una subclase que a veces devuelve null debilita la condición post. Los clientes que dependen del contrato de clase base fallarán con una excepción de puntero nulo. Las subclases deben asegurar que las condiciones de puestos sean al menos tan fuertes como las de la clase base.
Los invariantes deben conservarse
Los invariantes son condiciones que permanecen fieles para la vida del objeto. Por ejemplo, un tiene el invariante que los elementos se ordenan siempre. Si una subclase viola ese invariante (por ejemplo, insertando un elemento fuera de orden), rompe las expectativas del programa. Las subclases deben mantener todos los invariantes de la clase base, incluso si añaden nuevos comportamientos.
La limitación de la historia
Los objetos tienen una historia de cambios estatales. La limitación de la historia indica que la subclase no debe permitir cambios estatales que la clase base prohíbe. Por ejemplo, si una clase base no tiene métodos de setter, una subclase que añade un setter viola el LSP porque el código cliente puede asumir inmutabilidad. El límite de la historia suele pasar por alto pero es crítico cuando se trata de objetos mutables en sistemas orientados a objetos objeto.
Por qué LSP es crítico para la extensibilidad
La extensibilidad se basa en la capacidad de añadir nuevos componentes sin modificar a los clientes existentes. Cuando se honra el LSP, el polimorfismo funciona como se pretende. Una nueva subclase puede conectarse a un código antiguo con cambios cero. Esto reduce el riesgo de regresión y acelera el desarrollo. Sin LSP, la jerarquía de clase base se vuelve frágil. Los desarrolladores deben inspeccionar cada subclase para entender comportamiento especial, lo que conduce a mantener la sobrecarga y aumentar el potencial de errores.
Considere un sistema que procesa pagos. Una clase base define un método . Subclases como y implementar el método. Si todas las subclases siguen LSP, agregando un nuevo es sencillo. Pero si una subclase lanza una excepción cuando la cantidad excede un límite (a diferencia de la clase de principio de cliente que espera siempre será prede principio de un contrato).
LSP también fomenta el diseño por contrato, que mejora la documentación y la comunicación de equipo. Los desarrolladores pueden confiar en la especificación de la clase base sin leer cada aplicación subclase. Esto es particularmente valioso en grandes bases de código con muchos colaboradores. Además, LSP admite el escalado del sistema permitiendo que los componentes sean intercambiados por razones de rendimiento o características sin alterar la arquitectura general.
Violaciones comunes del LSP y cómo evitarlas
Reconociendo las violaciones de los LSP es esencial para la escritura de sistemas de mantenimiento. A continuación se presentan patrones frecuentes que rompen el principio, junto con estrategias para fijarlos.
El problema de rectángulo cuadrado
Como se mencionó, modelar una plaza como subclase de rectángulo viola el LSP porque la plaza limita la anchura y la altura a ser igual. Un mejor diseño es hacer tanto rectángulo como clases separadas cuadradas que implementan una interfaz compartida , o utilizar un método de fábrica que devuelve objetos apropiados. Evite forcing jerarquías de herencia que no satisfacen estrictamente la compatibilidad conductual.
Subclase lanza excepciones no previstas
Si el método de clase base no declara excepciones, un método de subclase que lanza una excepción comprobada viola el LSP. Incluso lanzar una excepción no comprobada como puede sorprender a los clientes si la clase base nunca lo hizo. Subclases debe lanzar sólo excepciones que la clase base permite, o ninguna en absoluto. Use excepciones verificadas sabiamente, y documentar comportamiento de excepción en el contrato base.
Método Sobrescribir Devoluciones Tipo de Weaker
En idiomas como Java y C#, se permiten tipos de retorno covariantes (un método subclase puede devolver un tipo más específico). Sin embargo, el revés no es: devolver un tipo más débil o menos específico rompe el contrato. Por ejemplo, si la clase base devuelve un , una subclase que devuelve un viola el LSP. Asegurar que los tipos de retorno son al menos tan específicos como la clase base.
Subclase Quita el comportamiento
A veces una subclase anula un método con un cuerpo vacío, eliminando efectivamente la funcionalidad. Si el cliente se basa en ese método teniendo un efecto, el comportamiento cambia. Por ejemplo, un que extiende un mutable y anula para no hacer nada que rompa el contrato de la clase base. En lugar, considere utilizar un enfoque de segregación de interfaz o composición.
Fortalecimiento de las condiciones previas
Comúnmente visto cuando los métodos de sobrescribir que aceptan parámetros opcionales. Si la clase base acepta para un parámetro, una subclase que arroja una excepción en crea una violación de precondición. Documentar si ] está permitido y mantener esa asignación en todas las subclases es crítica.
Para evitar violaciones, comience con interfaces que definen comportamientos mínimos y enfocados. Composición favorita sobre la herencia cuando la relación "es-a" es cuestionable. Escribe pruebas de contrato que verifiquen el comportamiento base y subclase, y ejecuten en integración continua.
Aplicando LSP en Diseño de Sistema
El diseño de LSP requiere un pensamiento deliberado tanto en la arquitectura como en la implementación. Aquí están las directrices prácticas para incorporarse a su flujo de trabajo de desarrollo.
Uso de contratos de abstracto
Define clases de base o interfaces que expresan el comportamiento esperado sin implementación. Incluye documentación de condiciones previas, condiciones post e invariantes. En idiomas que apoyan Diseño por Contrato (como Eiffel), puedes hacer cumplir estos requisitos contractualmente. En la mayoría de los idiomas principales, confía en la documentación y pruebas unitarias.
Preferencias Composición sobre la herencia
Cuando la relación entre dos clases no es estrictamente "es-a", use la composición. Por ejemplo, en lugar de una que se extiende , tiene una clase que contiene una con dimensiones iguales. Esto evita la violación del LSP por completo. La composición también tiende a producir sistemas más flexibles que son más fáciles de probar.
Escribe los Tests de Contrato
Crear una suite de prueba para la clase base que deben pasar todas las subclases. Estas pruebas deben validar que las condiciones previas, las condiciones posteriores y los invariantes sostienen. Por ejemplo, una prueba para puede verificar que el valor devuelto es positivo para las dimensiones dadas. Cualquier subclase debe pasar las mismas pruebas para asegurar el cumplimiento de LSP. Esta técnica, a menudo llamada "prueba substitutiva", captura las violaciones tempranamente.
Uso de Subtítulos Comportamiento
Cuando herede, considere primero el aspecto conductual. Pregunta: "Si sustituyo una instancia de la clase base con esta subclase, ¿los clientes notarán alguna diferencia en el comportamiento?" Si la respuesta es sí, rediseñe la herencia. Sigue el principio de menos sorpresa.
Refactor cuando se encuentran violaciones
Durante las revisiones de código o después de las fallas de prueba, vuelva a factorar la jerarquía. Extraiga el comportamiento común en una clase base abstracta o una interfaz, y empuje el comportamiento especializado en clases separadas. Utilice el patrón Método de implementación] para asegurar que las subclases sigan un algoritmo consistente al tiempo que permite variaciones en pasos específicos.
LSP en lenguajes de programación moderna
La forma en que se aplica el LSP varía según las distintas lenguas debido a las diferencias en los sistemas de escritura, los modelos de herencia y el manejo de las excepciones.
Java y C#
Ambos idiomas soportan interfaces y clases abstractas. Usa interfaces para contratos abstractos y asegura que las clases de implementación satisfagan todas las condiciones. Las violaciones de LSP mencionadas anteriormente (desperdiciamiento de la vista, fortalecimiento de la precondición) son comunes en Java y C#. Usar la anotación en Java o la ] palabra clave en C# para evitar firmas accidentales de método.
TipoScript
El sistema de clasificación estructural de TipoScript hace que LSP sea aún más crítico. Puesto que la compatibilidad de tipo se basa en la estructura en lugar de la jerarquía nominal, una clase que tiene los mismos métodos pero el comportamiento diferente puede ser reemplazable sintácticamente pero no conductualmente. Los desarrolladores deben hacer cumplir manualmente LSP escribiendo pruebas y documentando contratos.
Python
Python es de tipo dinámico, lo que significa que las violaciones de LSP sólo se hacen evidentes en tiempo de ejecución. Sin controles de compilador, escriba pruebas de unidad robustas y utilice clases de base abstractas (ABC) del módulo para definir los métodos requeridos. El pato de Python ya asume LSP, por lo que la consistencia conductual es esencial.
Vamos.
Go utiliza interfaces implícitamente. Un tipo satisface una interfaz si implementa todos los métodos. LSP in Go se aplica por el hecho de que las interfaces son pequeñas y enfocadas. Aún así, ser cauteloso: si dos tipos satisfacen la misma interfaz pero se comportan de manera diferente, el código cliente que espera que el contrato fallará. Escribe pruebas para contratos de interfaz.
LSP y otros principios SOLID
LSP no existe en forma aislada. Interacciona con los otros cuatro principios SOLID de maneras importantes.
Principio de Responsabilidad Única (RP)
SRP ayuda a mantener las clases enfocadas, lo que reduce las posibilidades de violar LSP. Una clase con una sola responsabilidad es más fácil subtipo sin cambiar accidentalmente el comportamiento. Por ejemplo, separar la lógica de validación del almacenamiento de datos hace tanto la base como las subclases más simples.
Principio abierto/Cerrado (OCP)
OCP afirma que las clases deben estar abiertas para la extensión pero cerradas para la modificación. LSP permite al OCP permitiendo que las subclases extiendan el comportamiento sin alterar el código existente. Si se viola el LSP, no puede agregar sin peligro nuevas subclases sin cambiar clientes, rompiendo así el OCP. Los dos principios están estrechamente vinculados.
Principio de Segregación Interfaz (ISP)
ISP alienta a las interfaces de grasa a que se rompan en pequeñas y específicas, lo que reduce la probabilidad de que una subclase debe implementar métodos que son irrelevantes, lo que a menudo conduce a violaciones de LSP (por ejemplo, implementaciones vacías o lanzadas). Al diseñar interfaces pequeñas, evita forzar subclases a romper contratos.
Principio de Inversión de Dependencias (DIP)
DIP aconseja dependiendo de abstracciones, no de concreciones. Cuando usted depende de interfaces, LSP asegura que cualquier aplicación concreta puede ser sustituida libremente. Sin LSP, la capa de abstracción se vuelve infiel, y los desarrolladores pueden depender de implementaciones directamente, violando DIP.
Pruebas para el cumplimiento de LSP
Verificar LSP no siempre es sencillo. Sin embargo, las metodologías de pruebas sistemáticas pueden ayudar a detectar violaciones tempranamente. Aquí están las estrategias para incorporar pruebas LSP en su flujo de trabajo.
Crear una clase de prueba de contrato de base
Escribe una clase de prueba abstracta o una suite de prueba que ejecute el contrato de clase base. Para cada condición previa y posterior, escribe una prueba. Por ejemplo, si el método de clase base en un lanza una excepción cuando la pila está vacía, incluye una prueba que verifica eso. Luego, para cada subclase, ejecuta las mismas pruebas. Si cualquier subclase falla, viola LSP.
Uso de pruebas basadas en la propiedad
Herramientas como QuickCheck (para Haskell, también disponibles en otros idiomas a través de bibliotecas como o ) generan insumos aleatorios y verifican que los invariantes sostienen. Para LSP, puede expresar propiedades como: "Para cualquier secuencia válida de llamadas de método, el estado después de llamar un método en la subclase coincide con el comportamiento de la clase base".
Comportamiento invariable
Algunos idiomas permiten afirmaciones de tiempo de ejecución. En Java, puede utilizar la palabra clave o una biblioteca como . En C#, o . Estos cheques verifican invariantes y precondiciones durante el desarrollo, capturando violaciones tempranamente.
Ejemplos del LSP en acción
Muchas bibliotecas y marcos estándar dependen de la LSP para funcionar correctamente. Entendimiento de estos ejemplos profundiza el reconocimiento por el principio.
Marco de colecciones Java
] interfaz define el comportamiento de las colecciones ordenadas. Subclases como , , y 'CopyOnWriteArrayList` todos se adhieren al contrato de interfaz. Ellos apoyan , , , etc., con el código esperado de la ruptura:4
Capas de acceso de base de datos
Al implementar los repositorios de bases de datos, una interfaz de base define métodos como y . Implementaciones concretas para MySQL, PostgreSQL y almacenamiento en memoria siguen el mismo contrato. LSP asegura que el intercambio de la base de datos subyacente no altera la lógica de aplicación. Esto es clave para probar y flexibilidad.
Corriente de procesamiento de tuberías
En la programación funcional, las transformaciones en flujos (como mapa, filtro, reducción) se definen por contratos. Cualquier función transmitida a debe ser una transformación pura que preserva el invariante del flujo (por ejemplo, no modificando el estado externo). Esto es LSP aplicado a subtipado de funciones: el tipo de función define un contrato, y las implementaciones deben satisfacerlo.
Conclusión
El Principio de Sustitución Liskov es más que un concepto teórico, es una herramienta práctica para diseñar software extensible y sostenible. Al asegurar que las subclases puedan estar para sus clases base sin alterar el comportamiento correcto, los desarrolladores construyen sistemas que crecen orgánicamente con nuevas características. Adherirse a LSP reduce errores de integración, mejora la claridad del código y se alinea con la filosofía SOLID más amplia.
Para aplicar el LSP eficazmente, concéntrese en contratos de comportamiento, escriba pruebas completas y use la composición cuando la herencia se siente forzada. Reconoce las cuatro condiciones —precondiciones, condiciones post, invariantes y limitaciones de la historia— como cheques para cada nueva subclase. Con diligencia, LSP se convierte en una parte natural de su proceso de diseño, lo que conduce a arquitecturas que son resistentes y adaptables.
Para más lectura, explore el papel original de Barbara Liskov (]Data Abstract and Hierarchy), Robert C. Martin ]] discusión de los principios SOLID y Martin Fowler ] ] [[Leer más]].