viernes, 1 de agosto de 2014

EL PRINCIPIO DE LISKOV SUSTITUTION

El propósito de una abstracción es separar comportamiento e implementación. Es decir separar la abstracción de cómo esta es implementada, así implementaciones de la misma abstracción pueden ser sustituidas libremente.

El principio de Liskov Sustitution se vuelve muy importante en el diseño de un programa ya que de este depende mucho la mantenibilidad y la correcta funcionalidad de un sistema cuando nuevas implementaciones son creadas.

El principio dice lo siguiente:

Si tenemos una objeto o1 de la clase A y un objeto o2 de la clase B entonces en un programa escrito en función de B podemos sustituir el objeto o1 por o2 si alterar la funcionalidad del sistema si A es un subtipo de B.

public class LiskovSustitution {
    public static void main(String[] args) {
        OtroItem o1 = new OtroItem();
        Item o2 = new Item();
        hacerAlgo(o1); Aquí podemos reemplazar o1 por o2 sin afectar la funcionalidad.
    }

    static void hacerAlgo(Item b){
        b.update();
    }
}

class OtroItem extends Item{
    public void hacerOtraCosa(){
        System.out.println("Hacer otra cosa");
    }
}

class Item{
    public void update(){
        System.out.println("Item updated");
    }
}
El problema viene con un mal manejo de la herencia. Imaginen el caso donde se tiene una clase Set que hereda de List (podríamos decir que un Set es un List dada la herencia). Como ya sabemos la clase Set guarda solo una instancia del mismo objeto aunque este le sea pasado dos veces, es decir si hacemos Set.add(objeto) y de nuevo Set.add(objeto) solo una instancia es almacenada a diferencia de List que guardaría las dos instancias. Ahora bien imaginen el caso donde un método requiere de un List para trabajar pero dada la herencia es posible pasar un objeto de la clase Set, es muy probable que con esto cambiemos la funcionalidad de ese método si darnos cuenta hasta el punto en que el sistema quede inestable.

Con este ejemplo vemos como un subtipo cambia la funcionalidad de su tipo padre. Ademas de violar el principio de liskov sustitution también esta faltando al principio open close modification.

Tomando nuestro ejemplo anterior:

public class LiskovSustitution {
    public static void main(String[] args) {
        OtroItem o1 = new OtroItem();
        Item o2 = new Item();
        hacerAlgo(o1); Aquí podemos reemplazar o1 por o2.
    }

    static void hacerAlgo(Item b){
        b.update();
    }
}

class NoUpdatableItem extends Item{
    public void update (){
        throw UnsoportedOperation();
    }
}

class Item{
    public void update(){
        System.out.println("Item updated");
    }
}

Si un subtipo no tiene todas las operación que su tipo padre, entonces este no se debe considerar como un subtipo, es decir si NoUpdatableItem no soporta la operación update entonces no se puede decir que sea un Item. Esto es un indicativo que algo anda mal en nuestro diseño. Por otro lado si cada que creamos un nuevo subtipo la clase que hace uso del polimorfismo debe ser cambiada esto también es un indicativo que algo va mal en el diseño. Por ejemplo:

public class LiskovSustitution {
    public static void main(String[] args) {
        OtroItem o1 = new OtroItem();
        Item o2 = new Item();
        hacerAlgo(o1); Aquí podemos reemplazar o1 por o2.
    }

    static void hacerAlgo(Item b){
        if( ! b instanceof NoUpdatableItem)
b.update();
    }
}

class NoUpdatableItem extends Item{
    public void update (){
        throw UnsoportedOperation();
    }
}

class Item{
    public void update(){
        System.out.println("Item updated");
    }
}

Esta variante en el método static void hacerAlgo(Item b) tiene que hacer uso de instanceof para saber si una operación es permitida. Es decir, si el método que es cliente de nuestra clase tiene que saber el tipo de dato que se le pasa para saber que operaciones puede realizar es sin duda un mal diseño y una violación al princiopio que aquí estamos analizando.

Lo correcto para nuestro ejemplo seria separar los Items en dos tipos diferentes, UpdatableItems y NoUpdatableItems.

class Item{
    String nombre;
    public Item(final String nombre){
        this.nombre=nombre;
    }
}

class UpdatableItem extends Item{

    public UpdatableItem(String nombre) {
        super(nombre);
    }

    public void update(){
        System.out.println("Update item");
    }
}

class NoUpdatableItem extends Item{

    public NoUpdatableItem(String nombre) {
        super(nombre);
    }
}

Y llegaramos a crear subtipos de UpdatableItems estos subtipos deben mantener la misma funcionalidad que su clase padre para poder ser considerados subtipos de UpdatableItems.


Importante: este post es autoría de Carlos, quien no lo subió por causas de fuerza mayor provocadas por su voluntad.