DEV Community

Victor Manuel Pinzon
Victor Manuel Pinzon

Posted on • Updated on

SOLID: Principio de Sustitución de Liskov

Esta es una continuación a la serie de principios SOLID.

El principio de sustitución de Liskov es el principio más técnico de los cinco principios SOLID. Es el que más ayuda a escribir aplicaciones desacopladas, lo cual es elemental en el diseño de componentes reutilizables.

El tercer principio SOLID fue definido por Barbara Liskov de la siguiente manera:

Sea ϕ(x) una propiedad comprobable acerca de los objetos x de tipo T. Entonces ϕ(y) debe ser verdad para los objetos y del tipo S, donde S es un subtipo de T.

La definición dada por Liskov esta basada en el concepto de contrato e implementación definido por Bertrand Meyer, el cual establece los siguientes tres principios básicos:

  • Un contrato y una implementación deben de definir de antemano una pre-condición, la cual es una condición que debe de ser garantizada siempre en la llamada de una rutina del contrato establecido.
  • Un contrato y una implementación deben de definir de antemano una post-condición, la cual es una condición de salida de la rutina.
  • Mantener una determinada propiedad, asumida a la entrada y garantizada a la salida.

El concepto de contrato e implementación son los fundamentos de herencia y polimorfismo en los lenguajes de programación orientados a objetos.

En 1996, Robert C. Martin redefinió el concepto dado por Liskov de la siguiente forma:

Funciones que usan punteros de referencia a clases base deben de poder usar objetos de clases derivadas sin saberlo.

La redefinición dada por Bob Martin ayudó a simplificar el concepto y a su adopción por parte de los desarrolladores.

Para facilitar la comprensión de este principio veamos el siguiente ejemplo donde una estructura de clases viola el principio de sustitución.

Violación del principio de sustitución de Liskov

Como desarrollador de una entidad bancaria se te solicita implementar el sistema de manejo de cuentas bancarias. El cual, en la primer fase, se implementará la cuenta básica y la premium. La diferencia entre ambas es que las cuentas premium generan acumulación de puntos prefiero cada vez que se realiza un depósito.

Para llevar a cabo este desarrollo, defines la siguiente clase abstracta:

public abstract class CuentaBancaria {

    /**
     * Método encargado de realizar los depositos monetarios.
     * @param monto         Monto a depositar.
     */
    public abstract void depositar(double monto);

    /**
     * Método encargado de retirar el monto especificado.
     * @param monto         Monto a retirar.
     * @return              Retorna verdadero en casos exitosos,
     *                      falso en caso contrario.
     */
    public abstract boolean retirar(double monto);
}
Enter fullscreen mode Exit fullscreen mode

La clase abstracta define la obligatoriedad en las clases derivadas de definir el funcionamiento de los métodos abstractos de depositar y retirar.

Para el manejo de las cuentas bancarias básicas y premium, se definen las siguientes clases:

public class CuentaBancariaBasica extends CuentaBancaria {

    private double saldo;

    @Override
    public void depositar(double monto) {
        this.saldo += monto;
    }

    @Override
    public boolean retirar(double monto) {
        if(this.saldo < monto)
            return false;
        else{
            this.saldo -= monto;
            return true;
        }

    }

}
Enter fullscreen mode Exit fullscreen mode
public class CuentaBancariaPremium extends CuentaBancaria {

    private double saldo;
    private int puntosPrefiero;

    @Override
    public void depositar(double monto) {
        this.saldo += monto;
        incrementarPuntosPrefiero();
    }

    @Override
    public boolean retirar(double monto) {
         if(this.saldo < monto)
            return false;
        else{
            this.saldo -= monto;
            incrementarPuntosPrefiero();
            return true;
        }
    }

    private void incrementarPuntosPrefiero(){
        this.puntosPrefiero++;
    }

}
Enter fullscreen mode Exit fullscreen mode

Por favor, tomar en cuenta que las clases definidas en este ejemplo están muy lejanas a incluir todo lo necesario de una implementación real.

A las cuentas basicas y premium se les realiza un cobro administrativo anual de $25.00. Para implementar esta politica se implementa la clase de servicio de retiros.

import java.util.ArrayList;
import java.util.List;

public class ServicioRetiros {

    public static final double MONTO_GASTO_ADMON = 25.00;

    public void cargarDebitarCuentas(){

        CuentaBancaria ctaBasica = new CuentaBancariaBasica();
        ctaBasica.depositar(100.00);

        CuentaBancaria ctaPremium = new CuentaBancariaPremium();
        ctaPremium.depositar(200.00);

        List<CuentaBancaria> cuentas = new ArrayList();

        cuentas.add(ctaBasica);
        cuentas.add(ctaPremium);

        debitarGastosAdmon(cuentas);

    }

    private void debitarGastosAdmon(List<CuentaBancaria> cuentas){
        cuentas.stream()
                .forEach(cuenta -> cuenta.retirar(ServicioRetiros.MONTO_GASTO_ADMON));
    }
}
Enter fullscreen mode Exit fullscreen mode

Una vez finalizada la implementación de las cuentas básicas y premium, se te solicita la implementación de las cuentas a largo plazo. Las características de este tipo de cuentas son las siguientes:

  • Las cuentas bancarias de deposito a largo plazo están exentas de cobros administrativos.
  • Las cuentas bancarias de deposito a largo plazo no permiten retiros manuales. Si un cliente desea retirar lo abonado a su cuenta, lo debe de realizar mediante una gestión diferente en las sucursales del banco.

Como desarrollador encargado del sistema de cuentas bancarias, decides utilizar la misma clase abstracta definida en los ejemplos anteriores.

public class CuentaLargoPlazo extends CuentaBancaria{

    private double saldo;

    @Override
    public void depositar(double monto){
        this.saldo += monto;
    }

    @Override
    public boolean retirar(double monto){
        throw new UnsupportedOperationException("No permite la operación de debito");
    }   
}
Enter fullscreen mode Exit fullscreen mode

En este punto es donde resalta la violación al principio de sustitución de Liskov, ya que las cuentas a largo plazo no deben de extender el funcionamiento de la operación retirar. Esto significa que si intentamos forzar la extensión de la clase debemos de dejar el método retirar como un método vacio o que lance una excepción de método no soportado. Además, se generaría un problema en el servicio de debito de gastos administrativos, ya que si por error se incluyera una cuenta a largo plazo se daría una excepción de ejecución.

Se le podría dar la vuelta al error e incluir una condicional en el método de "debitarGastosAdmon" para que ignore a las cuentas a largo plazo.

private void debitarGastosAdmon(List<CuentaBancaria> cuentas){
        for(CuentaBancaria cuenta : cuentas){
            if(cuenta instanceof CuentaLargoPlazo)
                continue;
            else
                cuenta.retirar(ServicioRetiros.MONTO_GASTO_ADMON));
        }
    }
Enter fullscreen mode Exit fullscreen mode

Sin embargo, la inclusión de la condicional viola el principio de abierto/cerrado debido a que cada vez que se incluya una cuenta bancaria con diferente funcionamiento, habrá que modificar el código para condicionarlo acorde a la clase.

Implementación del Principio de Sustitución de Liskov

El problema integral del ejemplo anterior es que la cuenta bancaria a largo plazo no es una cuenta regular, al menos no del tipo que se definió en la clase abstracta. Existe una prueba de razonamiento inductivo que se puede utilizar en estos casos "Si grazna como un pato, camina como un pato y se comporta como un pato, entonces, ¡seguramente es un pato!". La prueba del pato es importante bajo el concepto del principio de sustitución de Liskov debido a que la cuenta bancaria a largo plazo, luce como una cuenta bancaria regular, pero no se comporta como una cuenta regular, ya que no permite retiros. Por lo que inferimos que las cuentas a largo plazo no son cuentas bancarias regulares.

Para cumplir con el principio de sustitución de Liskov se deben de tomar en cuenta
las siguientes consideraciones:

  • Los tres tipos de cuentas permiten la acción de depósito.
  • Únicamente se permite realizar retiros en cuentas bancarias básicas y premium. Esto significa que existen dos tipos de cuentas, las cuentas bancarias retirables y las no retirables.
  • Se define una clase abstracta con el único método de depositar.
  • Se definirá una segunda clase abstracta que extenderá el comportamiento de la primera clase abstracta e incluirá la acción de retirar.
  • Las cuentas bancarias del tipo básico y premium serán del tipo de cuenta bancaria retirable. Mientras que las cuentas bancarias a largo plazo seran del tipo cuenta bancaria, la cual únicamente permite la acción de depósito.

Para llevar a cabo los cambios de estructura se define la clase abstracta CuentaBancaria, la cual es la clase básica para todas las cuentas y tiene un único método para realizar la acción de deposito.

public abstract class CuentaBancaria {

    /**
     * Método encargado de realizar los depositos monetarios.
     * @param monto         Monto a depositar.
     */
    public abstract void depositar(double monto);
}
Enter fullscreen mode Exit fullscreen mode

Se extiende el funcionamiento de la clase de CuentaBancaria para definir el comportamiento de las cuentas bancarias que permiten retiros. La clase CuentaBancariaRetirable es también una clase abstracta que define el método retirar.

public abstract class CuentaBancariaRetirable extends CuentaBancaria {

    /**
     * Método encargado de retirar el monto especificado.
     * @param monto         Monto a retirar.
     * @return              Retorna verdadero en casos exitosos,
     *                      falso en caso contrario.
     */
    public abstract boolean retirar(double monto);
}
Enter fullscreen mode Exit fullscreen mode

Las cuentas bancarias básicas y premium extenderán el uso de la clase de CuentaBancariaRetirable, la que a su vez extiende la clase CuentaBancaria. Esto permite que las clases de cuentas básicas y premium tengan las acciones de deposito y retiro.

public class CuentaBancariaBasica extends CuentaBancariaRetirable {

    private double saldo;

    @Override
    public void depositar(double monto) {
        this.saldo += monto;
    }

    @Override
    public boolean retirar(double monto) {
        if(this.saldo < monto)
            return false;
        else{
            this.saldo -= monto;
            return true;
        }

    }

}
Enter fullscreen mode Exit fullscreen mode
public class CuentaBancariaPremium extends CuentaBancariaRetirable {

    private double saldo;
    private int puntosPrefiero;

    @Override
    public void depositar(double monto) {
        this.saldo += monto;
        incrementarPuntosPrefiero();
    }

    @Override
    public boolean retirar(double monto) {
         if(this.saldo < monto)
            return false;
        else{
            this.saldo -= monto;
            incrementarPuntosPrefiero();
            return true;
        }
    }

    private void incrementarPuntosPrefiero(){
        this.puntosPrefiero++;
    }

}
Enter fullscreen mode Exit fullscreen mode

Debido a que las cuentas a largo plazo únicamente permiten depósitos, se extenderá el uso de la clase CuentaBancaria.

public class CuentaBancariaLargoPlazo extends CuentaBancaria {

    private double saldo;

    @Override
    public void depositar(double monto) {
        this.saldo += monto;
    }

}
Enter fullscreen mode Exit fullscreen mode

La clase encargada de proveer el servicio de retiros utilizará únicamente las clases que extiendan el funcionamiento de CuentaBancariaRetirable.

import java.util.ArrayList;
import java.util.List;

public class ServicioRetiros {

    public static final double MONTO_GASTO_ADMON = 25.00;

    public void cargarDebitarCuentas(){

        CuentaBancariaRetirable ctaBasica = new CuentaBancariaBasica();
        ctaBasica.depositar(100.00);

        CuentaBancariaRetirable ctaPremium = new CuentaBancariaPremium();
        ctaPremium.depositar(200.00);

        List<CuentaBancariaRetirable> cuentas = new ArrayList();

        cuentas.add(ctaBasica);
        cuentas.add(ctaPremium);

        debitarGastosAdmon(cuentas);

    }

    private void debitarGastosAdmon(List<CuentaBancariaRetirable> cuentas){
        cuentas.stream()
                .forEach(cuenta -> cuenta.retirar(ServicioRetiros.MONTO_GASTO_ADMON));
    }
}
Enter fullscreen mode Exit fullscreen mode

El método encargado de debitar los gastos administrativos ahora recibe un listado de objetos que extienden el funcionamiento de la clase CuentaBancariaRetirable. Es decir, nosotros podemos sustituir los objetos de CuentaBancariaRetirable por cuentas básicas o premium, siempre y cuando estas extiendan el uso de la clase abstracta CuentaBancariaRetirable, pero no podemos hacer la sustitución por objetos que solo extienden la clase CuentaBancaria, tal como el caso de las cuentas a largo plazo.

El diseño de la estructura de nuestras clases también refuerza los requerimientos funcionales dados por el cliente. Ya que las cuentas a largo plazo, al extender el funcionamiento de la clase CuentaBancaria permiten que únicamente se pueda realizar depositos en este tipo de cuentas.

Importancia del Principio de Sustitución de Liskov

El principio de sustitución de Liskov nos permite identificar generalizaciones incorrectas realizadas durante las primeras fases del desarrollo. El principio de Liskov, al igual que el principio de inversión de dependencia, son fundamentos del concepto de inyección de dependencias, el cual es utilizado ampliamente en muchos frameworks actuales.

Las violaciones al principio de Liskov son fáciles de detectar, como regla general puedes utilizar las siguientes pistas:

  • Se está violando el principio de Liskov cuando se introducen condicionales acorde al tipo del objeto, tal como se mostró en el ejemplo del condicional instanceof.
  • Una estructura de clases no se encuentra en cumplimiento con el principio si al extender el comportamiento de una clase abstracta se definen métodos vacíos o métodos que lancen excepciones por no tener implementación.

Si deseas ampliar tu conocimiento acerca del principio de sustitución de Liskov, puedes consultar el blog de Robert C. Martin.

En la próxima publicación discutiremos acerca del Principio de Segregación de Interfaces.

Top comments (0)