1 / 19
ago. 2017

Hola,

intentando mejorar mi desarrollo con POO he visto muchas veces comentar que la herencia no es una buena idea, que es mejor utilizar composición. ¿Por qué esto? ¿Cuándo está justificado utilizar herencia?

No es una característica que utilice mucho pero me llama la atención que siendo algo tan propio de la POO se rechace de forma tan tajante.

Un saludo.

Al principio por mi parte la herencia la usaba más de lo debido, a fin de cuentas la herencia es una característica importante de la POO, hasta que llegué a la conclusión, tras estudiar POO a conciencia, que sólo hay que utilizarla (quizás con alguna excepción) para utilizar polimorfismo.

Al utilizar herencia cuando podías utilizar composición lo que se hace realmente es fijar qué es y qué "comportamiento" tiene la clase en tiempo de compilación, mientras que si favoreces la composición sobre la herencia ese "comportamiento" puedes modificarlo en tiempo de ejecución y tu clase en vez de heredar (es lo mismo que la padre) lo que hace es componer (tiene ese comportamiento). Fijate que entrecomillo "comportamiento" porque asumo que parte de una clase tiene todo el comportamiento de otra.

La composición es más versatil a la hora de hacer cambios para añadir "comportamientos" al determinar que una clase tiene otras clases y no es esas otras clases también.

Por ejemplo:

Si A hereda de B, y quieres que también haga cosas de C, si vamos por herencia y no tenemos herencia múltiple al final se puede acabar haciendo un B extiende C y A extiende B (y con herencia múltiple podemos complicar todo incluso más), sin ser las 3 el mismo tipo de cosa y sólo lo hacemos para aprovechar comportamientos. La herencia ahí no es correcta porque la herencia debe ser únicamente para crear clases que son lo mismo y son intercambiables unas por otras. Es más, aunque sólo tuviesemos A y B si no son lo mismo y no son intercambiables es mejor utilizar composión: A tiene un B en vez de A es un tipo de B, porque no tiene por qué serlo.

Con composición sería más sencillo evitar eso ya que nuestra clase A tendría una clase B y una C y por delegación tendría los comportamientos de las 2, y en ningún caso A, B y C serían el mismo tipo de cosa porque nunca lo han sido. Esto favorece cambiar B y C en A incluso en tiempo de ejecución o añadir D si se da el caso. Mientras que si con herencia queremos que A haga también cosas de D ya entramos en algo muy chungo.

Esto es un resumen muy resumido, pero van por ahí los problemas de abusar de la herencia.

La herencia debe usarse para hacer jerarquía de funcionalidades. FIN.

Esto significa básicamente que en el ejemplo de las colecciones podrías decir que una linked list es una colección pero una colección no es una Linked List.

Hola,

No me puedo extender mucho ahora mismo pero dejo dos apuntes:

  1. La herencia no es algo propio de la POO, es algo característico de la implementación que han hecho algunos (muchos) lenguajes de la orientación a objetos, pero está lejos de ser una característica determinante de la orientación a objetos.
  2. Fruto de que no me puedo extender mucho, enlazo esto que resume un poquillo mi opinión:

Un saludo

Creo que este video también puede ayudar a entender mejor que la herencia es necesaria pero no hay que abusar de ella.

Claro, pero es porque Linked List es un tipo concreto de colección, eso es lo que se hace al usar ahí la herencia, es decir, son dos cosas del mismo tipo intercambiables entre sí. Realmente aquí lo que se está heredando es un tipo, es decir, si vas a iterar u obtener el número de elementos te da lo mismo hacerlo sobre una colección o sobre una linked list, deberían ser intercambiables. Luego como cada tipo de colección implemente cómo hacerlo debería dar lo mismo.

Si yo hago un count me tiene que dar lo mismo tenga una colección o cualquier cosa que extienda a la colección.

¿La colección no es una Linked List? Claro, es la clase padre, pero la linked list tampoco es una colección, es un tipo concreto de colección.

Otra forma de crear una linked list podría ser por composición con una colección. Esto a mi me gusta más si la linked list va a hacer cosas que no es capaz de hacer una colección, por lo que no serían intercambiables.

Y otro detalle a tener en cuenta sería que la colección fuese una clase abstracta o una interface, por lo que al no poder utilizar clases concretas de colección no habría problema en extender la linked list como un tipo concreto de colección aunque la linked list tenga nuevas funcionalidades ya que aquí sí que heredamos puramente un tipo.

Todo esto es muy matizable y luego habría que ver cada caso concreto, no es más que un resumen rápido.

Cierto, en Golang por ejemplo han eliminado la herencia haciendo un pequeño truco componiendo objetos para reutilizar comportamientos sin crear jerarquías, es una especie de decorator pattern.

Aquí mejor debería haber puesto que "la herencia debe ser únicamente para crear clases que son casos concretos del mismo tipo cuyos objetos son intercambiables unos por otros"

Muchas veces, cuando estamos explorando un problema en sus fases iniciales, es fácil ver cosas que pueden parecer el mismo tipo simplemente porque comparten ciertas propiedades o comportamientos.

El problema es que, conforme avanzamos en nuestro conocimiento del problema, descubrimos que esa similitud era simplemente circunstancial, era coincidencia "de nombre" pero no de intención, etc. y si hemos recurrido a un mecanismo como la herencia, el coste del cambio es muchísimo más alto frente a la composición.

separó este tema 7 ago., '17

4 posts fueron trasladados a un nuevo tema: Dudas con el principio de Liskov

¿Por lo que leo hasta el momento podemos decir que hay que tener cuidado con la herencia porque es menos flexible que la composición y si nos damos cuenta en un momento avanzado del desarrollo que la herencia no encaja con lo que queremos tenemos un lío montado considerable? Lo intento poner en mis palabras para entenderlo :sweat_smile:

Hace unos años hice un desarrollo que aún hoy está en producción cayendo en ese error. Al principio la herencia quedaba preciosa, pero es que no eran clases del mismo tipo aunque sí lo parecían, y claro, luego cuando hubo que cambiarlo se complicó un poco, luego otro poco, luego otro, y todos esos pocos hicieron un mucho que :scream: :scream: :scream: :scream:

Es un horror que ya es inmodificable, pero ahí sigue en producción. Yo creí que lo hacía bien porque usaba herencia: no mentorización en la empresa, no pair programing, no revisión de código por alguien con más conocimientos, ... Y luego a base de leer sobre POO entendí que debí usar composición y todo habría sido más claro, limpio y facil de ampliar.

A día de hoy sólo espero que no haga falta modificar ese código para evitar unos cuantos WTF.

No tiene por qué. P. ej. C++ tiene herencia privada que es herencia SIN polimorfismo. Son dos cosas totalmente separadas.
Solemos asociar herencia con polimorfismo porque la mayoría de lenguajes OOP lo implementan de esa manera. Pero de nuevo son decisiones del lenguaje fuera de lo que podríamos llamar OOP.

No veo por qué. ¿Cual es el problema que nos soluciona la herencia que no se puede solucionar usando composición para ese caso concreto?

No conocía la herencia privada de C++. Una dudas:

¿Se basa en que todos los métodos y atributos que se heredan son privados y sólo accesible para la clase hija, aunque sean públicos en la clase padre?

Al no haber polimorfismo entiendo que serían clases de diferente tipo, ¿es así?

¿Qué diferencia habría en utilizar por composición un objeto en vez de heredarlo de forma privada? Es decir, si B hereda A de forma privada, ¿Qué ventaja tendría respecto a componer B con un objeto A y ocultarlo al exterior? Se podrían utilizar sus mismos métodos y sólo estarían accesibles dentro de B. Si hablamos de que con la herencia privada heredamos y tenemos accesible tanto lo público como lo privado entonces sí sería diferente, pero se me haría rara esa finalidad. ¿Es posible heredar algo privado de esta forma que no sea protected? Esta pregunta la hago por mi desconocimiento en C++.

Yo me refería a usar herencia prácticamente solo para aprovechar el polimorfismo, y si no aprovechar composición si es posible.

Efectivamente. Dada esas dos clases, donde PrivatHer hereda privadamente de Base:

class Base {
	public:
		int isPublic;
};
class PrivateHer : Base {

};

Entonces lo siguiente da error:

PrivateHer pr;
auto a3 = pr.isPublic;    // error. member Base::isPublic is inaccesible

Y además no hay polimorfismo. Es decir:

Base b;
PrivateHer ph;
Base *pb = &b;
pb = &ph;     // Error, ya que un PrivateHer no se puede convertir a un Base
pb = static_cast<Base*>(&ph);      // Error por lo mismo de antes

(Si la herencia fuese pública no tendríamos esos dos errores)

Esa es la pregunta típica. De hecho es tan típica que hasta la FAQ de C++ la responde1 :slight_smile: El resúmen sería:

Use composition when you can, private inheritance when you have to

Vale, eso no explica las diferencias, pero ya deja claro que no es lo mismo. La herencia privada se diferencia de la composición en dos aspectos básicos: el primero es que te da acceso a las variables protegidas de la clase que heredas. Esa es la restricción: deberíamos usar solo herencia privada si requerimos acceso a las variables protegidas de la clase heredada. Eso abre algunos escenarios que serían bastante más complejos sin tener herencia privada. La otra diferencia es más técnica y es que en herencia privada el "objeto" correspondiente a la clase que heredamos se crea como parte de la cadena de constructores (en composición tenemos el control de como creamos los objetos que nos componen). Por lo que a C++ concierne la herencia privada es herencia, y técnicamente es tratada como tal, pero no se trata de una relación is-a.

En resúmen, la herencia privada es una herramienta más del lenguaje, de la que no se debe abusar (si lo hacemos, probablemente, tenemos algún problema en el diseño de las clases). Y, simplemente, la puse como ejemplo de que herencia no siempre tiene por qué implicar polimorfismo :wink:

Comentario tocahuevos del día (espero que nadie se moleste!) Solo un detalle, recordar que no es lo mismo heredar de un objeto (herencia pura y dura) que cumplir un contrato (implementación de interfaz). Cuando 3 clases implementan una interfaz, no heredan de ella.

Por mucho que en lenguajes como C# o C++ se use la misma sintaxis para heredar que para indicar la implementación de un contrato, esto es una (en mi opinión) mala decisión de diseño del lenguaje que a veces ayuda a confundir las cosas. Especialmente en C++.

Muchas veces puedes estar implementando la misma interfaz en múltiples clases que no tienen ninguna relación de herencia entre ellas y que, a su vez, pueden estar usando composición para acometer su detalle de implementación. Por ejemplo, las listas encadenadas con parámetros de tipo suelen depender de nodos y suelen tener un nodo _head de tipo T que no deja de ser un "componente" de la clase. Y lo tendrían igual aunque no implementasen la interfaz "colección". Son cosas distintas.

Hala pues, ya me callo...