El principio de sustitucion de liskov:
Las funciones/metodos que usen punteros o referencias a clases base (super clases) deben poder usar objetos de clases derivadas (sub-clases) sin saberlo.
En su version original:
Si para cada objeto O1 de tipo T1 hay un objeto O2 de tipo T2 que para todo programa P definido en terminos de T2, el comportamiento de P no cambia cuando O2 es sustituido por O1 entonces T1 es subtipo de T2.
Yo lo recuerdo de la siguiente manera:
Tiene que ser posible cambiar un tipo por sus subtipos.
En los siguiente ejemplos voy a utilizar peseudo codigo intentando que sea lo mas parecido a Java o C#.
Un ejemplo que no cumple con este principio:
//pseudo codigo
public void CalcularArea(Figura unaFigura)
{
if(unaFigura.class == Cuadrado)
{
return unaFigura.CalcularAreaCuadrado()
}
if(unFigura.class == Circulo)
{
return unaFigura.CalcularAreaDeCirculo()
}
}
En el ejemplo anterior .class me devuleve la clase de la cual es instancia unaFigura, en los distintos lenguajes se hace de forma distinta pero por claridad lo hice de esa forma.
Claramente este codigo esta mal formulado, por cada nueva subclase de Figura debemos
modificar el codigo de CalcularArea. Lo que viola el principio de cerrado al cambio y abierto
a la expancion.
Veamos otro ejemplo no tan obvio.
El tipico ejemplo que se da para entender este principio es el del rectangulo y el cuadrado.
Supongamos que tenemos la siguiente clase
public class Rectangulo
{
float ancho;
float alto;
public void SetAncho(float unAncho)
{
ancho = unAncho;
}
public void SetAlto(float unAlto)
{
alto = unAlto;
}
public float Area()
{
return alto * ancho;
}
}
Supongamos que tenemos agregar una nueva caracteristica/ feature a nuestro programa.
Por ejemplo una clase Cuadrado. Tal vez podriamos hacer lo siguiente, subclasificar
cuadrado de rectangulo.
public class Cuadrado : Rectangulo
{
public void SetAncho(float unAncho)
{
ancho = unAncho;
alto = unAncho;
}
public void SetAlto(float unAlto)
{
alto = unAlto;
ancho = unAlto;
}
}
Ahora podemos definir un cuadrado definiendo su alto o su ancho. Tal vez es algo raro como
quedo el codigo, eso es un indicio de que no debe ser la mejor manera, pero prosigamos.
En la gran mayoria de los casos el codigo funciona bien. Pero para el siguiente caso, no:
public void TestDeArea(Figura unaFigura)
{
unaFigura.SetAlto(10);
unaFigura.SetAncho(2);
assert(20, unaFigura.Area()) //se esperaba 20, se obtuvo 4
}
Si al TestDeArea le pasamos un cuadrado, primero configuramos u obtenemos un cuadrado de lado 10 y luego al settear el ancho mutamos el cuadrado en otro de lado 2 por lo tanto el retorno del area será 4. En apariencia el conjunto de cuadrados en un subconjunto del conjunto de rectangulos, por ser un caso particular donde el alto y el ancho son iguales.
Pero en realidad no es un subconjunto. Mas bien rectangulo y cuadrado representan dos conjuntos distintos con una zona de interseccion. De hecho se puede pensar en el cuadrado como un caso particular de un rectangulo pero ¿un cuadrado es un rectangulo? Yo diria que no porque justamente el rectangulo se caracteriza por tener sus lados vertical y horizontal distintos, asi que no podriamos decir que todo cuadrado es un rectangulo, por lo tanto los cuadrados no son un subconjunto de los rectangulos.
Para finalizar resolvamos el ejemplo como debe ser.
public class Medidor
{
public void CalcularArea(Figura unaFigura)
{
return unaFigura.Area();
}
}
public abstract class Figura
{
public float Area();
}
public class Rectangulo : Figura
{
float ancho;
float alto;
public void SetAncho(float unAncho)
{
ancho = unAncho;
}
public void SetAlto(float unAlto)
{
alto = unAlto;
}
public float Area()
{
return alto * ancho;
}
}
public class Cuadrado : Figura
{
float lado;
public void SetRadio(float unLado)
{
lado = unLado;
}
public float Area()
{
return lado ^ lado;
}
}
public class Circulo : Figura
{
float radio;
public void SetRadio(float unRadio)
{
radio = unRadio;
}
public float Area()
{
return Pi() * radio ^ 2;
}
}
De esta forma podemos seguir agregando figuras y no tenemos que cambiar el codigo
de la clase Medidor. Este ejemplo tal vez no sea el mejor para mostrar como se aplica el principio de
Liskov porque la clase Figura no se puede instanciar ya que es abtracta. Pero creo que la idea se entiende.
Como colorario podemos decir que cuando subclasificamos una clase esta debe ser si o si como su super clase. Si subclasificamos Perro de Animal, esta bien por que todos los Perros son Animales, si subclasificamos PerroSalchicha de Perro esta bien porque todo PerroSalchicha es un Perro. En el caso de querer subclasificar cuadrado de rectangulo, los cuadrados no son rectangulos es por eso que esta mal.
Si se te ocurren mejores ejemplos o tienen alguna duda escribelos en los comentarios con gusto intentare responderlos.
Comentarios
Publicar un comentario