
Bárbara Liskov es una informática y matemática nacida en California en 1939, famosa por sus contribuciones en el mundo de los lenguajes de programación.
La profesora Liskov pertenece a la National Academy of Engineering de los Estados Unidos. En 2004 ganó la Medalla John Von Neumann, premio de ciencias de computación establecido por la dirección del IEEE, por «su contribución fundamental a los lenguajes de programación, metodologías de programación y sistemas distribuidos».
En 2008 se convirtió en la segunda mujer en la historia en ganar el premio Turing, considerado el Nóbel en ciencias de computación, por «su contribución a los fundamentos teóricos y prácticos en el diseño de lenguajes de programación y sistemas, especialmente relacionados con la abstracción de datos, tolerancia a fallos y computación distribuida».
En 2018, la Universidad Politécnica de Madrid le otorgó un doctorado honoris causa.
Además de sus artículos científicos, Bárbara Liskov ha publicado varios libros. De ellos, merece la pena mencionar en este contexto su libro “Program Development in Java: Abstraction, Specification, and Object-Oriented Design“. Se trata de un libro fácil de leer y con el que se puede aprender mucho acerca del diseño de aplicaciones en Java.
Quizás, la contribución a la programación por la que es más conocida es por su Principio de Sustitución de Liskov (Liskov Substitution Principle, LSP). El principio fue introducido por primera vez por Barbara Liskov en una conferencia en 1987, titulada “Data Abstraction and Hierarchy”. El principio fue reformulado y publicado formalmente en un artículo conjunto de Barbara Liskov y Jeannette Wing en 1994. Ese artículo se titula “A Behavioral Notion of Subtyping“, publicado en ACM Transactions on Programming Languages and Systems (TOPLAS), y es donde aparece la definición matemática que se cita habitualmente:
“Let
Φ(x)be a property provable about objectsxof typeT. ThenΦ(y)should be true for objectsyof typeSwhereSis a subtype ofT.”
En términos de clases Java, el Principio de Sustitución de Liskov indica que, en cualquier contexto en el que se utilice un objeto de tipo T, tiene que ser posible utilizar en su lugar un objeto de tipo S, siendo S un subtipo de T (ya sea por herencia o por implementación de interfaz), sin que el programa funcione de manera inadecuada.”
El LSP es uno de los principios más importantes en relación con la arquitectura de software. Tiene fama de ser difícil de entender y es muy frecuente ver aplicaciones que lo violan flagrantemente.
En este artículo voy a hacer una introducción básica, explicada de manera sencilla mediante algún ejemplo, sobre cómo aplicar este principio a las jerarquías de clases de nuestros programas escritos en Java. El LSP es mucho más complejo de lo que aquí vamos a mostrar. Quizás en artículos posteriores profundice un poco más en el tema.
Primer ejemplo: jerarquía de archivos
Supongamos que estamos implementando una aplicación que maneja distintos tipos de archivos. Podríamos plantear una clase abstracta Archivo, que sirviera de base a los diferentes tipos de archivos, con los métodos correspondientes a las distintas operaciones que se pueden hacer con los archivos. Entre estos métodos podrían estar los métodos abstractos abrir() e imprimir():
public abstract class Archivo {
public abstract void abrir();
public abstract void imprimir();
/** Otros métodos **/
}
Ahora, podríamos implementar un par de clases concretas que deriven de Archivo, por ejemplo, ArchivoTexto y ArchivoImagen:
public class ArchivoTexto extends Archivo {
@Override
public void abrir() {
System.out.println("Abriendo ArchivoTexto");
}
@Override
public void imprimir() {
System.out.println("Imprimiendo ArchivoTexto");
}
/** Otros métodos **/
}
public class ArchivoImagen extends Archivo {
@Override
public void abrir() {
System.out.println("Abriendo ArchivoImagen");
}
@Override
public void imprimir() {
System.out.println("Imprimiendo ArchivoImagen");
}
/** Otros métodos **/
}
Nuestra aplicación podría tener una clase, a la que hemos llamado GestorArchivos, que dispusiera de distintos métodos para realizar operaciones con los archivos. Uno de esos métodos podría ser el método imprimir(), que recibe un array de archivos y los va abriendo e imprimiendo:
public class GestorArchivos {
public void imprimir(Archivo[] archivos) {
for(Archivo archivo: archivos) {
archivo.abrir();
archivo.imprimir();
}
}
/** Otros métodos **/
}
El programa cliente, podría ser parecido al siguiente:
public class Main_1 {
public static void main(String[] args) {
GestorArchivos gestor = new GestorArchivos();
Archivo a1 = new ArchivoTexto();
Archivo a2 = new ArchivoImagen();
Archivo[] archivos = {a1, a2};
gestor.imprimir(archivos);
}
}
El diagrama UML de la aplicación es el siguiente:

La salida de este programa sería:

Hasta este momento, todo está correcto. El programa cliente GestorArchivos utiliza los objetos del tipo Archivo a través del array que recibe como argumento en su método imprimir(). Los elementos de ese array pueden ser de cualquier tipo derivado de Archivo y todo funciona correctamente.
La aplicación crece…
Supongamos que la aplicación necesita ahora gestionar un nuevo tipo de archivos: ArchivoAudio. Podríamos derivar el tipo ArchivoAudio de nuestra clase abstracta Archivo. El problema es que este tipo de archivos no se puede imprimir y, por la jerarquía de herencia impuesta, está obligado a implementar el método imprimir().
Podríamos pensar en una solución que consistiera en lanzar una excepción si se intenta imprimir un objeto del tipo ArchivoAudio:
public class ArchivoAudio extends Archivo {
@Override
public void abrir() {
System.out.println("Abriendo ArchivoAudio");
}
@Override
public void imprimir() {
throw new UnsupportedOperationException();
}
}
Esta solución supone un trastorno: si se envía uno de estos archivos al método imprimir() de la clase GestorArchivos, el programa se interrumpiría.
Podríamos razonar de la siguiente manera: podemos modificar el método imprimir() de la clase GestorArchivos de forma que, si recibe un objeto del tipo ArchivoAudio, no intente imprimirlo, para que no se lance la excepción ni se interrumpa el programa:
public class GestorArchivos {
public void imprimir(Archivo[] archivos) {
for (Archivo archivo : archivos) {
if ((archivo instanceof ArchivoAudio) == false) {
archivo.abrir();
archivo.imprimir();
}
}
}
}
Esto es una violación del Principio de Sustitución de Liskov: nuestra jerarquía debería permitir la utilización de cualquier objeto de un subtipo de Archivo en el programa cliente, sin que el cliente se tenga que preocupar de discernir la clase concreta de la que se trata. Imagina que en el futuro se necesita gestionar otros tipos de archivos no imprimibles, por ejemplo archivos de vídeo. Nos veríamos obligados a modificar de nuevo la clase cliente GestorArchivos para incorporar una nueva excepción en todos los métodos en los que fuera necesario.
Esta violación del LSP es una señal de que la jerarquía que hemos planteado no es adecuada para el problema que queremos resolver. La clase base Archivo establece el contrato que deben cumplir las clases derivadas: tener métodos para abrir y para imprimir el archivo. Pero la clase ArchivoAudio incumple dicho contrato: los archivos de audio no se pueden imprimir.
La solución correcta es crear una jerarquía cuyos contratos se cumplan. Podemos partir de una clase abstracta Archivo con un método abrir() y derivar de ella una segunda clase abstracta ArchivoImprimible que incorpora un método imprimir() y sirve de clase base de todos los tipos de archivos que se pueden imprimir.
La solución correcta podría ser la que se refleja en el siguiente esquema:

Con esta nueva jerarquía, podríamos modificar el método imprimir() de GestorArchivos de la siguiente forma:
public class GestorArchivos {
public void imprimir(Archivo[] archivos) {
for (Archivo archivo : archivos) {
if (archivo instanceof ArchivoImprimible) {
ArchivoImprimible imprimible =
(ArchivoImprimible)archivo;
imprimible.abrir();
imprimible.imprimir();
}
}
}
}
Esta solución no viola el LSP. La comprobación que se hace dentro del método imprimir() no es la de si el objeto archivo es de una clase concreta, sino si dispone de cierta capacidad. No obstante, la observación del código permite intuir la solución más limpia: cambiar el tipo de parámetro del método imprimir(), para que solo pueda recibir objetos del tipo ArchivoImprimible:
public class GestorArchivos {
public void imprimir(ArchivoImprimible[] archivos) {
for (ArchivoImprimible archivo : archivos) {
archivo.abrir();
archivo.imprimir();
}
}
}
Ahora, el método imprimir() del programa cliente siempre recibe el tipo de archivos adecuado. No obstante, según las circunstancias de la aplicación, podría ser más conveniente una u otra solución. Las dos son correctas y ninguna de ellas viola el LSP.
La solución de jerarquía planteada también se podría hacer en base a un Interface Imprimible, en lugar de la clase abstracta ArchivoImprimible. Dejo a ejercicio del lector la solución a que daría lugar este otro planteamiento de la jerarquía.
Una violación más sutil del LSP
Siguiendo con el ejemplo anterior, supongamos que hubiera un tipo de archivos que, sí se pueden imprimir, pero necesitan una configuración previa. Pensemos en una hoja de cálculo: antes de imprimir, hay que establecer qué filas y columnas queremos incluir en la impresión. Podríamos implementar la clase HojaDeCalculo de la siguiente forma:
public class HojaDeCalculo extends ArchivoImprimible {
private boolean configurado;
public void configurar() {
configurado=true;
}
@Override
public void imprimir() {
if(configurado==false) {
throw new IllegalStateException();
}
System.out.println("Imprimiendo HojaDeCalculo...");
}
@Override
public void abrir() {
System.out.println("Abriendo HojaDeCalculo");
}
}
La clase HojaDeCalculo, en su método imprimir(), lanza una excepción si no se ha llamado previamente al método configurar(). Podríamos hacer lo siguiente en la clase GestorArchivos:
public class GestorArchivos {
public void imprimir(ArchivoImprimible[] archivos) {
for (ArchivoImprimible archivo : archivos) {
archivo.abrir();
if(archivo instanceof HojaDeCalculo) {
((HojaDeCalculo)archivo).configurar();
}
archivo.imprimir();
}
}
}
Como seguramente has intuido, este código viola el LSP: el programa cliente no tiene que identificar la clase concreta sobre la que está operando. En este caso, ademas de identificar una clase concreta, tiene que saber qué operaciones tiene que hacer en ella para poder usar el método imprimir().
La diferencia con el caso de los archivos imprimibles es que ahora el problema no está en la jerarquía, que podría ser adecuada. El problema es que estamos imponiendo que la clase GestorArchivo tiene que saber cómo operar un tipo de archivos concreto.
Piensa que este tipo de situación se podría dar en otros tipos de archivos: archivos que es necesario comprimir antes de enviar por correo electrónico, archivos que es necesario codificar en JSON o XML antes de enviarlos por Internet o cualquier otra situación. Y el programa cliente no es el que tiene que gestionar todas esas peculiaridades. Si lo hiciera, con cada nuevo tipo de archivo, habría que volver a codificar la clase GestorArchivos.
La solución es que sean las propias clases las que gestiones su estado con las operaciones que tengan que hacer. Podríamos modificar el método imprimir() de la clase HojaDeCalculo para que, si el archivo no está configurado, ella misma llame al método configurar():
@Override
public void imprimir() {
if(configurado==false) {
configurar();
}
System.out.println("Imprimiendo HojaDeCalculo...");
}
Ahora, el código de GestorArchivos puede operar perfectamente con cualquier ArchivvoImprimible que reciba:
public class GestorArchivos {
public void imprimir(ArchivoImprimible[] archivos) {
for (ArchivoImprimible archivo : archivos) {
archivo.abrir();
archivo.imprimir();
}
}
}
Hasta aquí, esta pequeña introducción al Principio de Sustitución de Liskov. Hay otros aspectos de este principio y otros tipos de violaciones del mismo que seguramente trataremos en otros artículos. Por el momento, basta con que entiendas el planteamiento más simple de la aplicación del LSP.
Santiago Higuera. 23 de abril de 2026.
















