Apr 30

Composición sobre herencia, mi “pequeño” error.

En la programación orientada a objetos hay un principio que dice "preferir la composición sobre la herencia". El motivo tradicional para hacer esta afirmación es que la herencia es una dependencia fuerte entre clases. La clase hija depende fuertemente de la clase padre y la necesita para funcionar. Cualquier cambio en la clase padre puede tener efectos inesperados en cualquiera de las clases hijas y no tenemos, entonces, tanta libertad para cambiar el comportamiento de la clase padre.

Si además trabajamos en proyectos grandes con bastante gente implicada, la pega comentada puede convertirse en fuente grave de problemas. En un grupo grande es casi imposible saber si otra persona se le ha ocurrido heredar de nuestra clase o qué efectos secundarios puede tener si modificamos una clase que necesitamos modificar. Sí, todo esto se evita con un buen diseño, con test automáticos, con gente responsable…. cosas ideales que nunca se dan en proyectos grandes y que sólo se dan en unos pocos proyectos pequeños llevados a cabo por uno, dos o tres programadores buenos.

Otro problema añadido que no se comenta tanto, es la gran dificultad de leer el código cuando hay herencia, cuantos más niveles de herencias, peor. Para saber qué hace una clase, no podemos fijarnos sólo en esa clase, sino en sus clases padre. Y cuando queremos seguir un trozo de código que usa una clase, tampoco podemos fiarnos de esa clase usada, sino que tenemos que asegurarnos muy mucho de qué posibles clases hijas se están instanciando y están realmente siendo usadas ahí. Cuando el código es grande y hay muchos niveles de herencias, casi es imposible seguir el código y se hace imprescindible un buen debugger para seguirlo en tiempo de ejecución. Y arrancar un debugger en un proyecto grande, con varios ejecutables servidor que se comunican entre sí, ejecutables cliente distintos que se comunican con los servidores, puede ser realmente difícil.

Si no está muy justificada, siempre es mucho mejor la composición. Si nuestra clase A necesita la funcionalidad de una clase B, es mejor instanciarla que heredar de ella.

Por supuesto, para no perder la potencia de la orientación a objetos, si prevemos que es necesario, podemos hacer que la clase A use interfaces cuya implementación alguien le pasará desde fuera. De esta forma, la clase A podrá usar en un momento dado una clase B que implemente la interfaz, o una clase C que implemente la interfaz. Eso sí, salvo que esté muy justificado, siempre interfaces (nunca otras clase o clases abstractas) e implementación directa de las interfaces, sin varios niveles de herencias.

Comento todo esto porque hace poco hice una reflexión sobre el código que tenemos heredado de proyectos anteriores. En un momento dado, hace mucho, me dieron un grupo de gente nueva para desarrollar tres proyectos similares entre si, pero para clientes distintos y que debíamos hacer a la vez (plazos de entrega similares).

Yo, con toda mi buena intención, pensé en hacer los proyectos por capas. Las capas más básicas serían aquellas cosas comunes a todos los proyectos y según fuésemos subiendo de nivel, iríamos implementando las cosas específicas de cada proyecto. Y basé estas capas en herencia. El resultado es que una clase específica de un proyecto heredaba de una clase de la capa de abajo que a su vez heredaba de otra clase de la capa de más abajo y así sucesivamente (menos mal que java no permite herencia múltiple).

En esos proyectos todo iba bien y de hecho salieron bien dentro de lo que cabe. El grupo de desarrolladores y yo mismo nos conocíamos bien la estructura de herencias, qué se podía tocar, qué no se podía tocar y dónde había que tocar para incrementar la funcionalidad o cambiar la configuración. Basándose en las capas comunes, el desarrollo fue además bastante rápido.

Pero pasa el tiempo. Ese código se ha seguido usando para otros proyectos (las capas básicas), pero los desarrolladores han ido desapareciendo, cambiando de departamento o empresa, llevando sus propios proyectos … y los nuevos desarrolladores, incluso los más experimentados, se las ven y se las desean para ver dónde demonios se hace una cosa. Empiezan por la clase del proyecto y tienen que ir bajando por el árbol de herencias hasta llegar a clase padres que son realmente complejas. ¿Por qué son complejas? Porque tienen que ser lo suficientemente versátiles como para soportar a todos las clases hijas de todos los proyectos pasados y actuales. Esas clases abusan de tipos "Object", de Hashtables que no tienen definidas las posibles claves en ningún sitio, de paneles swing que no tienen nada porque se construyen sobre la marcha en tiempo de ejecución, etc, etc.

Resumiendo, un código que nos ayudó mucho en los tres primeros proyectos, pero que se fue convirtiendo en una pesadilla cuando los desarrolladores originales dejaron de estar disponibles y a los pocos que quedamos nos da "yuyu" de mantener, porque incluso a nosotros mismos nos resulta complejo con el paso del tiempo. Y eso sólo por las herencias, aparte de la posible complejidad de la algorítmica implicada.

Entradas relacionadas:

4 Responses to “Composición sobre herencia, mi “pequeño” error.”

  1. Gadelan Says:

    Comparto completamente tus pensamientos. Yo diría más: ¿es necesaria la herencia?

    La herencia hace tres cosas (que me acuerde ahora):
    1) Es una composición encubierta: La clase derivada contiene la clase base. Por eso tus desarrolladores tuvieron que mirar en el árbol de herencia hacia arriba. Para ver qué tenían en su clase.
    2) Es una delegación encubierta: Muy informalmente. Si llamas a un método en una clase y no está definido en esa clase, se busca en la clase base automáticamente. Si usamos la composición hemos de delegar uno a uno los métodos. Así que aquí lo que es la herencia es una abreviatura. Nada más.
    3) Es una conversión encubierta: Una clase derivada puede usarse como una clase base. Esto se puede conseguir añadiendo un método que devuelva la clase base. De nuevo, la herencia es una abreviatura.

    Existe el detalle de la vinculación dinámica, pero se puede resolver… ¡con los interfaces!

    Entonces… ¿para qué sirve la herencia? Es sólo una abreviatura de otros mecanismos del lenguaje. Complica la vida al desarrollador. Realizas operaciones encubiertas que quizás tú no quieras hacer. ¿Merece la pena?

  2. Fran Says:

    Al final lo que comentas sobre usar interfaces e implementaciones no es más que usar inyección de dependencias como se hace en algunos frameworks como spring.

  3. Rodrigo Says:

    Y diría más, el trabajar con interfaces e inyección de dependencias facilita mucho el trabajo a la hora de crear tests unitarios

  4. Arturo Says:

    Recuerdo una entrevista (de hace mucho) a James Gosling en la que le preguntaban qué cambiaría si tuviese que rediseñar Java. La respuesta fue que eliminaría la herencia y forzaría a que todo se hiciese a base de Interfaces.

    Yo no sé si llegaría a tanto, pero por experiencia las jerarquías de clases de muchos niveles llegan a ser infernales.

Leave a Reply