Herencia de clases y el "Principio de Liskov" (actualizado 15/10/2007)


En realidad se llama "Principio de sustitución de Liskov" y el nombre completo es Bárbara H. Liskov (sí, hay mujeres en la informática y han hecho grandes aportes ;-)).

A grandes rasgos dice que el famoso "es un" (is_a) que generalmente usan los docentes para enseñarnos "herencia de clases" no es suficiente.

No basta con "ser" hay que "comportarse como tal".


Generalmente hago el mismo chiste cuando trato el tema en un curso: "un docente es una persona, pero no basta, debe comportarse como tal".

Un ejemplo más real: Sistema de liquidación de sueldos

Si tienes un sistema que liquida sueldos, una clase base Empleado (con atributos como "sueldo" que se usa para calcular su pago) y subclases Gerente, Desarrollador, Administrativa y Becario (asumamos que el último no recibe ninguna remuneración).

Usas polimorfismo y en tu clase Administracion tienes un método liquidarSueldo donde recibirás objetos de tipo Empleado. Bien, cuando en un momento recibes de tu lista de empleados un becario... ¿qué debe hacer el sistema? (ya que haces el cálculo en base al valor que viene en el atributo sueldo y este es heredado por todos desde la clase Empleado).
  1. ¿Emitir un comprobante impreso pero con valor 0?
  2. ¿Agregar un "if" preguntando si es becario, y no hacer nada?
  3. etc

Supongamos que el 1) no es aceptable por el gasto de papel y te piden que no se impriman (no tiene sentido), pasamos al punto 2), agregamos un "if".

¿No estamos "codificando a fuego" nuestro sistema para un comportamiento particular?

Estamos rompiendo el polimorfismo, justamente, el patrón estratégico más importante que tiene la POO. Cada vez que tenemos que cambiar nuestro código generamos nuevos costos y nuevos posibles errores.

Por eso cada vez nos cuesta más desarrollar un sistema, hasta que tenemos que tirarlo y desarrollar otro de cero... y empezamos otra vez con los problemas del mal diseño (code smell).

La base teórica

Los principios de diseño te dicen que "desarrolla cerrado al cambio y abierto a la extensión" (Principio Open/Closed, "Abierto / Cerrado").

Por ejemplo, tu código debe ser reutilizable (¿no es la idea esencial de la OO?) y con solo agregar código -sin tocar el existente- lograrás adecuarte a los cambios, a los nuevos requerimientos. No te olvides que nuestro código lo usan muchos otros objetos, si este cambiara, generaría un efecto en cadena, posiblemente, dejando de funcionar lo ya existente y tener que modificar más objetos.

Esta forma de "extensión" es agregar más clases a la herencia y el método liquidarSueldo no se modifica, pero si tu no haces correctas herencias, no puedes hacer lo primero, por lo tanto tu sistema se degradará en cascada.

Esta simple tontería el autor demuestra con muchos ejemplos que hacer "herencia por herencia", "por reutilizar código", no es suficiente y genera grandes problemas en los diseños.

Es una de las razones que me ven de mal humor cuando veo que implementan herencia a los golpes, prueba y error, con el argumento solo de "reutilizar código"

La conclusión final es...

El problema es que la herencia está mal formulada y que el Becario no se comporta como un Empleado, por lo tanto no puede ir en esa estructura jerárquica, de lo contrario nos va a obligar a desarmar nuestro sistema para tratar de ajustarlo a un comportamiento no adecuado (agregando condiciones explícitas para el mismo, hardcode) y este empezará a dejar de ser reusable.

No se puede hacer herencia por herencia, para reusar código. No se puede hacer herencia simplemente por que un "Becario es un Empleado", hay que estar seguro que además de "ser" se comporte como tal.

Nuestro becario, ya que no cobra una remuneración, no pude pasar por el sistema de liquidación de sueldos. La solución no es ajustar el sistema de sueldos para que lo permita.

Anexo otro artículo relacionado: Herencia Múltiple en PHP5

Nota: este post está basado en una discusión presentada en Foros del Web

8 comentarios:

pooof dijo...

Pero como haces al final con el liquidar sueldo?

Enrique Place dijo...

Estimado Pooof:

Pensé que había quedado obvio... el problema es que la herencia está mal formulada y que el Becario no se comporta como un Empleado, por lo tanto no puede ir en esa estructura, de lo contraro te va a obligar a desarmar tu sistema para tratar de ajustarlo a un comportamiento no adecuado (agregando condiciones explícitas para el mismo).

Voy a releer el post y hacerle algunos ajustes para que quede más claro ... gracias ;-)

Damián Galarza dijo...

Hola, se me ocurrió que se podrían crear dos clases: "Empleado asalariado" y "Empleado ad honorem". Estas dos serían sub clases de Empleado. Empleado asalariado tendría el método para liquidar sueldos, mientras que la otra no. Por supuesto que la clase Becario entraria en la segunda clase. Opino que se podría hacer esto para poder reutilizar el codigo de Empleado. Sino, simplemente sacar Becario de esa estructura.
Saludos. Muy bueno el post.

Damián Galarza dijo...

Me falto agregar que el método de liquidacion de sueldos recibiria solamente objetos del tipo "Empleado asalariado".
Ahora si, hasta luego.

Sofilogo dijo...

No Damian, si restringes el método de liquidación de sueldos a que solo reciba objetos del tipo empleado asalariado, pues ya se estaría incumpliendo el principio de sustitución de Liskov, que lo que dice precisamente es que este método puede recibir cualquier objeto de la estructura jerárquica y seguir funcionando de igual forma, si y solo si (en este caso) la clase EmpleadoAsalariado fuera un SUBTIPO de la clase Empleado.

Anonimo dijo...

Yo tengo pregunta tonta.

La clase Empleado seria abstracta?

Yo tengo una posible solucion a lo planteado: seria crear una clase nueva Empleado_no_asalariado quien sea heredada por la clase Becario, y en la case Administracion comprobar si el objeto ES de tipo empleado para realizar la liquidacion de sueldos, y en caso que no lo sea no calcular nada.

Se me ocurrio eso quizas estoy pifiando bastante, pero en caso que sea asi, no es molesto crear una nueva clase igual a Empleado con la unica diferencia de carecer del metodo liquidarSueldo ?

Espero que se haya entendido mi posible solucion.

Saludos!

José Luis dijo...

Bueno, se que estoy cometiendo un error de ética al reavivar un post viejo, pero:
¿el if sería tan malo?, justifico:
En el caso de que se hiciera sin software, administración tomaría los legajos de las personas que trabajan en la compañia y comenzaría a emitir cheques y asentar la operación en los libros. Cuando agarren el legajo de un becario dirian "oh! un becario, este no recibe pago." y seguiria con el proximo legajo de la pila...
Saludos.

José Luis dijo...

Por cierto muy buen post.

Entradas populares