statics-and-dynamics
Principio de sustitución Liskov: asegurando las Jerarquías de clase confiable
Table of Contents
En la programación orientada hacia el objeto, los sistemas de construcción que son robustos y adaptables son el desafío perpetuo. Entre los principios fundamentales que guían a los desarrolladores hacia ese objetivo está el Principio de sustitución Liskov (LSP).Concebido por Barbara Liskov en 1987, LSP es el tercer pilar de los cinco principios SOLID para el diseño de software, y se aborda una pregunta crítica: cuando creas una subclase
¿Cuál es el Principio de Sustitución Liskov?
El Principio de Sustitución Liskov establece que los objetos de una superclase deben ser reemplazables con objetos de sus subclases sin afectar la corrección del programa. En otras palabras, si una función o método está diseñado para trabajar con un tipo base, también debe trabajar con cualquier tipo derivado sin requerir modificación o producir efectos secundarios inesperados. Barbara Liskov primero articula esta idea en su discurso de apertura de 1987 en la Conferencia sobre Sistemas de Objetos, ProgramaciónLA
LSP es fundamentalmente sobre subtipificación conductual. No es suficiente que una subclase tenga las mismas firmas de método como su padre (conformidad sintáctica); la subclase debe también honrar las intenciones y limitaciones de la clase padre. Si una subclase cambia el comportamiento fundamental de un método padre, por ejemplo, al lanzar una excepción que el padre nunca lanza, devolviendo un valor que viola el contrato del padre, o que requiere entonces condiciones de sustitución más estrictas
Definición y antecedentes formales
La definición formal original de Barbara Liskov es la siguiente:
“Si por cada objeto o]1 del tipo S hay un objeto o2 de tipo T tal que para todos los programas P definidos en términos de T, el comportamiento de P no se cambia cuando o1] es sustituido por o[FLT2] [FLT] [
Esta definición enfatiza que un subtipo (S) debe ser sustituible para su supertipo (T) en cualquier programa (P) que está escrito en términos de T. El comportamiento observable del programa debe ser preservado. Este concepto está estrechamente relacionado con la metodología Diseño por Contrato (DbC), pionera por Bertrand Meyer, donde cada método tiene condiciones explícitas de ruptura (lo que debe ser verdad antes de que el método se ejecute) y pos-estructuras (lo que es necesario
Para más lectura, vea el original Liskov y Wing paper] que formalizó el concepto. Además, el artículo Wikipedia sobre LSP ofrece una buena visión general.
¿Por qué es importante el LSP?
Adherirse a LSP aporta varios beneficios críticos a los sistemas orientados a objetos:
- Reliability and Correctness: Código que utiliza un tipo de base puede confiar en que cualquier subclase se comportará de acuerdo con el contrato del tipo base. Esto evita errores sutiles que ocurren cuando una subclase introduce comportamiento inesperado.
- Polymorfismo: El polimorfismo es la capacidad de tratar objetos de diferentes clases a través de una interfaz común. Sin LSP, el polimorfismo se vuelve peligroso porque sustituir una subclase puede producir resultados incorrectos. LSP asegura que el código polimorfico funciona como se desea.
- Mantenibilidad y Extensibilidad: Cuando se evitan las violaciones de LSP, añadir nuevas subclases no requiere modificar el código existente que depende del tipo base. Esto se alinea con el Principio Abierto/Cerrado (OCP) – las entidades de software deben estar abiertas para su extensión pero cerradas para su modificación.
- Testabilidad:] Las pruebas de unidad escritas contra una clase base pueden ser reutilizadas para validar subclases. Si una subclase viola LSP, esas pruebas fallarán, revelando la inconsistencia temprana.
LSP no es sólo un concepto académico; tiene implicaciones prácticas directas. Por ejemplo, en un sistema de procesamiento de pagos, si usted tiene una clase base con un método , usted espera que todas las subclases (por ejemplo, ], ) para procesar pagos sin errores o efectos secundarios que la clase base no puede anticipar las violaciones aquí.
Violaciones comunes del Principio de Sustitución Liskov
Reconociendo las violaciones de LSP es el primer paso hacia la fijación de ellas. Aquí están algunos patrones típicos que rompen el principio:
Fortalecimiento de las condiciones previas
Si un método de clase base espera un parámetro entero, añadiendo una condición en la subclase que el entero debe ser positivo (mientras que la clase base acepta cualquier entero) fortalece la condición previa. Clientes que pasaron un número negativo a la clase base ahora fallarían con la subclase. ]Ejemplo:]
// Base class
class UserService {
public void assignRole(int userId) { ... }
}
// Subclass violation
class AdminService extends UserService {
@Override
public void assignRole(int userId) {
if (userId <= 0) throw new IllegalArgumentException("Invalid ID");
...
}
}
Condiciones de funcionamiento
Si la clase base garantiza un determinado valor de retorno o efecto secundario, una subclase que reduce esa garantía viola el LSP. Por ejemplo, un método de clase base puede siempre devolver una cadena no nula; una subclase que devuelve en algunos casos debilita la condición post.
Lanzando nuevas excepciones
Las subclases no deben lanzar excepciones que la clase base no tire (a menos que esas excepciones sean subclases de excepciones ya permitidas). Si los clientes de la clase base capturan solamente , y una subclase lanza una , la sustitución causará accidentes inesperados.
Métodos de eliminación o eliminación que deben ser heredados
Si una subclase anula un método para no hacer nada (cuerpo vacío) o para lanzar un , esa es una violación clara. La subclase no se comporta como la clase base prevista.
Ejemplo clásico: Rectángulo, Plaza y Trampa de Área
El ejemplo más citado de violación de LSP implica formas geométricas. Muchos libros de texto comienzan con una clase que tiene y métodos, y luego crear una subclase que anula estos para hacer cumplir la anchura == altura. Aquí está el problema:
// Base class
class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
// Subclass violation
class Square extends Rectangle {
@Override
public void setWidth(int w) {
width = height = w;
}
@Override
public void setHeight(int h) {
width = height = h;
}
}
Ahora considera una función que funciona con un :
void resize(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
assert r.getArea() == 50; // Expect 50
}
Cuando pasa un , el método establece ancho a 5 y luego altura a 10 – pero el cuadrado está sobresierto establece anchura a 10 también, por lo que el cuadrado termina con ancho=10, altura=10, área=100. La afirmación falla. no es substituible para porque cambia el área de rectángulo.
La solución: Evitar la herencia de . En cambio, diseñar una clase abstracta con un método común , y tener ambos y implementar un principio de ruptura igual ] de forma independiente.
Ejemplo en el mundo real: Integración de la puerta de pago
Imagínese un sistema de comercio electrónico con una clase base :
abstract class PaymentGateway {
public abstract void charge(double amount);
public void refund(double amount) { /* default implementation */ }
}
Las subclases incluyen y . Un cliente que procesa los pagos pueden llamar y más tarde . Se produce una violación si se anula para lanzar una excepción porque la API de PayPal requiere un ID de reembolso, no sólo una cantidad.
Cómo arreglar: Cualquiera que sea (a) que asegure el contrato de la clase base incluye la posibilidad de que no se pueda apoyar (por ejemplo, hacer que devuelva un éxito booleano o declare una excepción comprobada), o (b) rediseñe la jerarquía para que no todos los gateways apoyen los reembolsos. Por ejemplo, introduzca una interfaz
Cómo seguir el Principio de Sustitución Liskov en la Práctica
Implementar LSP requiere disciplina en diseño y pruebas. Aquí están las pautas accionables:
- ]Diseño por contrato:] Especifique claramente las condiciones previas, las condiciones de publicación e invariantes para los métodos de clase base. Documente lo que cada método espera y garantice. A continuación, asegure que cada subclase cumple. Herramientas como Contratos en .NET o JML para Java pueden ayudar a hacer cumplir estas reglas.
- Favor Interfaces sobre Clases abstractas: Las interfaces definen un contrato sin detalles de implementación. Están alineadas naturalmente con LSP porque cualquier clase de implementación debe cumplir toda la interfaz. Con clases abstractas, es más fácil introducir dependencias accidentalmente.
- Use Composición Sobre la Inherencia: Cuando una subclase necesita anular el comportamiento hasta el punto de romper el contrato base, a menudo es mejor usar la composición. Por ejemplo, en lugar de heredar de , tiene una clase que expone internamente una pero sí
- Test for Substitutability: Escribe pruebas parametizadas que ejecutan los mismos escenarios contra tipos de base y derivados. Si una prueba pasa con el tipo de base pero falla con un tipo derivado, usted tiene una violación LSP. Esta práctica es especialmente poderosa cuando se usa dobles de prueba.
- Comprobar Tipos de Regreso Covariantes: Algunos idiomas permiten tipos de retorno covariantes (por ejemplo, un método subclase puede devolver un tipo más específico que la base). Esto está bien siempre y cuando no cambie la condición post. Asegúrese de que el objeto devuelto aún satisface todas las expectativas del valor de retorno del tipo base.
- Evitar la eliminación de métodos concretos indiscriminadamente: Si usted siente la necesidad de anular un método concreto en una subclase, cuestione si la herencia es la herramienta correcta. Tal vez la clase base era demasiado concreta. Haga el método abstracto o virtual sólo cuando usted piensa que las subclases cambiar el comportamiento de una manera controlada.
LSP y otros principios SOLID
La LSP está profundamente interconectada con los otros principios SOLID, especialmente el Principio Abierto/Closed (OCP) y el Principio de Inversión de la Dependencia (DIP):
- LSP y OCP: OCP afirma que las clases deben estar abiertas para la extensión pero cerradas para la modificación. LSP asegura que las extensiones (subclases) no rompen el código existente que utiliza el tipo de base. Sin LSP, la adición de una nueva subclase requeriría modificar el código del cliente para manejar el nuevo comportamiento, violando OCP.
- LSP y DIP: DIP aconseja dependiendo de abstracciones, no de concreciones. LSP es esencial aquí porque la abstracción (interfacio o clase base) debe ser estable y confiable. Si las subclases violan LSP, la abstracción ya no es una dependencia confiable, y el sistema se vuelve frágil.
- LSP y Segregación de Interfaz (ISP): ISP alienta interfaces pequeñas y enfocadas. Esto es naturalmente compatible con LSP porque una pequeña interfaz define un contrato estricto que es más fácil de honrar. Una clase que implementa una interfaz de tamaño puede luchar para cumplir todas las partes, lo que conduce a violaciones como lanzar .
Para una visión general de los principios SOLID, consulte El artículo SOLID de Wikipedia].
Conclusión
El Principio de Sustitución Liskov es mucho más que una buena teórica; es una herramienta práctica para construir sistemas orientados a objetos que son seguros de extender y fácil de mantener. Al asegurar que las subclas se comportan de una manera que es sustituible para sus clases padres, los desarrolladores pueden confiar en el polimorfismo sin miedo a errores ocultos. El principio alienta el pensamiento cuidadoso sobre jerarquías de clase, dando lugar a diseños que favorecen la composición y los contratos robustos, la sucesión, claramente definidos.
Recuerde, las violaciones de LSP a menudo aparecen como inconsistencias sutiles en el comportamiento: un método que lanza una excepción inesperada, un método que silenciosamente no hace nada, o un método que altera el estado de una manera que la clase base nunca pretendía. Al ser consciente de estas banderas rojas, y al aplicar las directrices discutidas anteriormente, Robert puede crear jerarquías que resisten la prueba del tiempo.
Referencias externas utilizadas en este artículo: