Fictizia / Master-en-Programacion-con-Python_ed1

FICTIZIA » Máster en Programación con Python — 1ª Edición
https://fictizia.com/formacion/master-programacion-python
GNU Affero General Public License v3.0
44 stars 27 forks source link

Principio de sustitución de Liskov #45

Open delapuente opened 5 years ago

delapuente commented 5 years ago

El otro día en clase, @ramoncorominas planteaba "¿cuando sí se cumple el principio de sustitución de Liskov?". Le puse en clase el ejempo de los múltiples decodificadoes de audio .ogg, .wav, .mp3... donde deberíamos poder sustituir uno por otro... ¡Nada más lejos de la realidad, como apuntaba Ramón! Y es cierto, planteé mal el ejemplo (para empezar porque esas clases serían hermanas, así que no hay herencia entre ellas).

Dejad que lo plantee de otra forma, saltando de lo más formal a lo más divugativo. Formalmente, el principio de sustitución dice:

What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

Me gustaría que nos fijáramos en este "What is wanted here..." que significa que podemos definit una forma de herencia o especialización (subtyping) definida por poder o no hacer esta sustitución. Tal clase de especialización se llama especialización de comportamiento (behavioral subtyping).

Este es el tipo de especialización que es deseable en un programa. Intuitivamente, porque la cosa va de reutilizar código, que suele estar en los métodos (comportamiento como decía @eun-plata cuando dió su defnición de objeto).

Esto no significa que la noción "un cuadrado es un rectángulo con los lados iguales" sea incorrecta bajo otras definiciones de especialización. Significa que para la definición de Python que hemos dado de un rectángulo:

class Rectangle:

    def __init__(self, w, h):
        self._w = w
        self._h = h

    def set_width(self, w):
        self._w = w

    def set_height(self, h):
        self._h = h

    def area(self):
        return self.w * self.h

Y del cuadrado:

class Square(Rectangle):

    def __init__(self, length):
        self._w = self._h = length

   def set_width(self, w):
        self._w = self._h = w

   def set_height(self, h):
        self._h = self._w = h

Tratar de ver un cuadrado como una especialización de comportamiento de un rectángulo no va a funcionar. El ejemplo venía con la prueba del test:

def test_area(rect):
    rect.set_width(5)
    rect.set_height(4)
    assert rect.area() == 20

Tan pronto como le pase a la función test_area un cuadrado, va a dejar de funcionar, pese a que la función maneja la API del rectángulo como cabe esperar.

La especialización por comportamiento permite la reutilización del código en la guisa todo o nada. Si en la clase derivada te sobran métodos, es mejor optar por la composición:

class Cuadrado:

    def __init__(self, l):
        self._rectangle = Rectangle(l, l)

    def set_length(self, l):
        self._rectangle.set_width(l)
        self._rectangle.set_height(l)

    def area(self):
        return self._rectangle.area()

@ramoncorominas, un ejemplo de "buena herencia" son aquellas clases que expanden capacidades. Por ejemplo, un rectángulo coloreado:

class ColoredRectangle(Rectangle):

    def __init__(self, w, h, color):
        super().__init__(w, h)
        self.color = color

    def paint(self):
        print(f'Draw a square of {self.color} color')