LSP: la esencia del principio de sustitución de Barbara Liskov en el desarrollo

Autor: IT Sectr Publicado: 2026-05-12 Tiempo de lectura: 9 min

LSP (Liskov Substitution Principle) — el tercer principio de SOLID, que define las condiciones para una herencia correcta en la programación orientada a objetos. El principio fue formulado por Barbara Liskov en 1987 y formalizado como: si S es un subtipo de T, entonces los objetos de tipo T pueden ser reemplazados por objetos de tipo S sin cambiar las propiedades del programa. Como se señala en el libro de Robert Martin Clean Architecture (2017), el principio de sustitución exige que una subclase no debilite el contrato de la clase base.

Puntos clave

  • LSP — principio de sustitución de Liskov, tercer principio de SOLID sobre herencia correcta
  • Subclase debe conservar el contrato de la clase base — precondiciones y postcondiciones
  • Violación de LSP se manifiesta en el «problema del cuadrado y el rectángulo» y excepciones lanzadas
  • Composición suele ser preferible a la herencia para cumplir LSP
  • Diseño por Contrato (Design by Contract) — forma formal de verificar LSP

¿Qué es LSP (Liskov Substitution Principle)?

LSP (Liskov Substitution Principle) — principio de sustitución formulado por Barbara Liskov en la conferencia OOPSLA de 1987. Definición formal: sea q(x) una propiedad demostrable de los objetos x de tipo T. Entonces q(y) debe ser demostrable para los objetos y de tipo S, donde S es un subtipo de T. En términos simples: los objetos de una subclase deben comportarse de modo que el código que trabaja con la clase base siga funcionando correctamente también con la subclase.

En la práctica, LSP significa que una subclase no debe violar el contrato de la clase base. El contrato incluye precondiciones (qué se requiere para llamar a un método), postcondiciones (qué se garantiza después de la llamada) e invariantes (condiciones que se mantienen durante la vida del objeto). Una subclase puede fortalecer precondiciones o debilitar postcondiciones — esto es precisamente una violación de LSP.

Un ejemplo clásico de violación de LSP es un cuadrado que hereda de un rectángulo. El método setWidth del rectángulo establece el ancho, mientras que en el cuadrado establece tanto el ancho como la altura. Un cliente que espera el comportamiento de un rectángulo (cambiar un lado no afecta al otro) obtiene un resultado inesperado. Un cuadrado no es un subtipo válido de un rectángulo.

Condiciones formales de LSP

LSP establece tres condiciones para una herencia correcta: las precondiciones de la subclase no pueden ser más fuertes que las precondiciones de la clase base (la subclase no exige más), las postcondiciones de la subclase no pueden ser más débiles que las postcondiciones de la clase base (la subclase garantiza no menos), y los invariantes de la clase base deben conservarse en la subclase. Estas condiciones se conocen como la regla de Diseño por Contrato según Bertrand Meyer.

Si al menos una condición se viola, el código que usa polimorfismo puede fallar. El compilador no verifica contratos semánticos, solo sintácticos. Por lo tanto, LSP es una cuestión de disciplina arquitectónica, no de tipado estático.

Cómo funciona el principio de sustitución de Liskov

El mecanismo de LSP se basa en la compatibilidad de comportamiento de los tipos. Si la clase S hereda de la clase T, el código cliente debe poder usar S donde se espera T sin cambiar su comportamiento. Esto incluye no solo las firmas de los métodos sino también su semántica.

LSP no prohíbe que una subclase agregue nuevo comportamiento. Está prohibido violar las expectativas del código escrito para la clase base. Si la clase base garantiza que el método save no lanza excepciones, la subclase no debe lanzarlas. Si la clase base devuelve un valor no negativo, la subclase no debe devolver uno negativo.

En proyectos reales, LSP se viola con mayor frecuencia al agregar lógica condicional en los métodos de la subclase: «si condición — lanzar excepción», «si condición — devolver null». Cada una de estas «sorpresas» socava el polimorfismo y obliga al código cliente a verificar el tipo del objeto antes de llamar — lo que contradice la propia idea del diseño orientado a objetos.

En proyectos móviles, una violación típica de LSP ocurre al crear ViewModels base. Si BaseViewModel garantiza que el método onCleared libera todos los recursos, y una subclase sobrescribe este método como vacío — cualquier código que confíe en la limpieza de recursos a través de una llamada polimórfica a onCleared funcionará incorrectamente. LSP exige que la subclase llame a super.onCleared() o realice el mismo trabajo por sí misma. La composición mediante LifecycleObserver es una alternativa que elimina la violación de LSP en la gestión del ciclo de vida.

Señales de violación de LSP en el código

Indicadores principales de violación de LSP incluyen: verificar el tipo del objeto mediante instanceof o is antes de llamar a un método, implementaciones de métodos vacías (stubs), lanzar NotImplementedError o UnsupportedOperationException, devolver null en lugar de un valor. Cada uno de estos patrones señala que la subclase no es un subtipo válido.

Otra señal común es la herencia con el objetivo de reutilizar código en lugar de modelar una relación «es-un» (is-a). La clase Bird tiene un método fly(). La clase Penguin hereda de Bird y sobrescribe fly() como vacío o lanzando una excepción. Esto es una violación de LSP: un pingüino no es un subtipo válido de ave.

En el desarrollo móvil, LSP se viola al crear clases base ViewHolder, Fragment o ViewController con métodos stub. Si una subclase no usa la mitad de los métodos de la clase base — la herencia se eligió incorrectamente. La composición o la segregación de interfaces resuelve el problema de forma más correcta.

Prueba de LSP

Una prueba simple para verificar LSP: escriba una prueba unitaria para la clase base que verifique su contrato (valores devueltos, excepciones, efectos secundarios). Ejecute esta prueba para cada subclase. Si la prueba falla — LSP se viola. Este enfoque se llama «prueba a través del contrato de la clase base».

En proyectos Android, dicha prueba es útil para ViewModel y Repository. Si BaseViewModel garantiza un estado Loading antes de un error, y una subclase lanza un error sin Loading — la prueba detectará la violación de LSP en la etapa de CI.

Ejemplos de LSP en desarrollo móvil

Veamos un ejemplo de Android con el manejo de ClickListener. Una violación de LSP ocurre cuando la implementación base garantiza algo y la subclase lo viola.

kotlin
// Clase base con garantía: onClick será llamado
open class BaseClickListener {
    open fun onClick(view: View) {
        // manejo básico
    }
}

// Violación de LSP: la subclase agrega una condición que lanza una excepción
class RestrictedClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (!isLoggedIn) {
            throw IllegalStateException("Not logged in")
        }
        super.onClick(view)
    }
}

// Solución correcta: contrato no violado
class ConditionalClickListener : BaseClickListener() {
    override fun onClick(view: View) {
        if (isLoggedIn) {
            super.onClick(view)
        }
    }
}

Un ejemplo de iOS con el protocolo DataSource demuestra una violación de LSP al devolver nil en lugar de datos:

swift
// Protocolo con contrato: devuelve datos o error
protocol DataProvider {
    func fetchData() async throws -> [String]
}

// Violación de LSP: devuelve nil sin error
class SilentFailProvider: DataProvider {
    func fetchData() async throws -> [String] {
        return [] // array vacío en lugar de error
    }
}

// Cumplimiento correcto de LSP
class NetworkProvider: DataProvider {
    func fetchData() async throws -> [String] {
        throw NetworkError.timeout
    }
}

Regla práctica: si una subclase no puede cumplir el contrato de la clase base, no debería ser una subclase. Una alternativa es extraer una interfaz con un contrato mínimo e implementarla en cada tipo a su manera.

LSP y herencia: cuándo elegir composición

La composición es preferible a la herencia en situaciones donde la relación «es-un» (is-a) es ambigua o condicional. Un ejemplo clásico: ¿es Manager un Employee? Sí. Pero ¿es Square un Rectangle válido? LSP dice «no». Si duda de la corrección de la herencia — elija composición.

En el desarrollo móvil, la composición se usa a menudo mediante inyección de dependencias: en lugar de heredar el comportamiento de una clase base, una clase lo recibe a través de un constructor. Una ViewModel no hereda de Repository sino que lo acepta como dependencia. Esto elimina la violación de LSP por definición — sin herencia, no hay violación del contrato.

Señales de que la herencia debe reemplazarse por composición: la subclase no usa algunos métodos de la clase base, la subclase sobrescribe métodos con stubs vacíos, el código cliente verifica el tipo del objeto mediante instanceof. En estos casos, la herencia se eligió incorrectamente y LSP se viola.

Solución mediante interfaces

Las interfaces resuelven el problema de LSP sin herencia: cada tipo implementa exactamente los métodos que necesita. En lugar de una clase base común Bird con un método fly() (donde Penguin no vuela) — una interfaz Flyable que solo implementan las aves voladoras. Penguin implementa Bird sin el método fly() — LSP no se viola.

En la arquitectura Android, este enfoque se aplica mediante interfaces segregadas de UseCase: en lugar de un gran UseCase con métodos getAll, getById, save, delete — interfaces separadas GetItemsUseCase, SaveItemUseCase. Un cliente depende solo de la interfaz necesaria, y cualquier clase que implemente esa interfaz es correcta desde la perspectiva de LSP.

Preguntas frecuentes

¿En qué se diferencia LSP de la herencia simple?

La herencia es un mecanismo del lenguaje; LSP es una regla para el uso correcto de ese mecanismo. La herencia garantiza compatibilidad de firmas (sintaxis); LSP exige compatibilidad de comportamiento (semántica). La herencia sin LSP produce polimorfismo que se rompe en tiempo de ejecución.

¿Siempre viola LSP el null en una subclase?

Si la clase base garantiza un retorno no nulo — sí. Si el contrato permite null (valor opcional) — no. LSP no prohíbe null; prohíbe debilitar el contrato. Estudie la documentación de la clase base y verifique si el contrato de la subclase es compatible.

¿Cómo se aplica LSP a los protocolos en Swift?

LSP se aplica a los protocolos de la misma manera que a las clases. Una implementación de protocolo debe cumplir el contrato semántico: si un protocolo define un método como non-throwing, la implementación no debe lanzar errores. Swift no verifica esto a nivel de compilador — la responsabilidad recae en el desarrollador.

¿Puede violarse LSP al usar sealed class?

Las sealed class en Kotlin son un caso especial porque la jerarquía está cerrada y es conocida por el compilador. LSP se aplica a sealed class en menor medida porque todos los subtipos se enumeran explícitamente en la expresión when. Un error de una subclase sellada será local, no un error polimórfico oculto.

¿Cómo probar el cumplimiento de LSP en un proyecto?

Escriba una prueba parametrizada para la clase base que se ejecute para todas sus subclases. La prueba verifica contratos de comportamiento clave: valores devueltos, excepciones, estados. Si la prueba falla en una de las subclases — LSP se viola. En CI, dicha prueba previene la regresión del código polimórfico.

Resumen

  • LSP (Liskov Substitution Principle) — principio de sustitución, tercero en SOLID, sobre compatibilidad semántica de la herencia
  • Subclase debe conservar el contrato de la clase base: precondiciones, postcondiciones e invariantes
  • Verificaciones instanceof y sobrescrituras vacías de métodos son las principales señales de violación de LSP
  • Composición e interfaces resuelven el problema de LSP donde la herencia es incorrecta
  • El problema del cuadrado y el rectángulo es un ejemplo clásico de incompatibilidad de subtipos
  • Una prueba de contrato para la clase base, ejecutada para todas las subclases, detecta violaciones de LSP en CI
  • Sealed class en Kotlin reducen los riesgos de LSP gracias a una jerarquía cerrada conocida por el compilador

Desarrollaremos una aplicación móvil llave en mano

IT Sectr crea aplicaciones para iOS y Android para startups y empresas desde 2017. Le asesoraremos y le propondremos la mejor solución.

Discutir el proyecto

Lea también