and language-agnostic oop inheritance composition aggregation

language-agnostic - and - uml aggregation



¿Prefieres la composición sobre la herencia? (30)

¿Por qué preferir la composición sobre la herencia? ¿Qué compensaciones hay para cada enfoque? ¿Cuándo deberías elegir la herencia sobre la composición?


¿Por qué preferir la composición sobre la herencia?

Ver otras respuestas.

¿Cuándo deberías elegir la herencia sobre la composición?

Siempre que la frase "un Bar es un Foo, y un Bar puede hacer todo lo que un Foo puede hacer" tiene sentido.

La sabiduría convencional dice que si la frase "un bar es un Foo" tiene sentido, entonces es un buen indicio de que la elección de la herencia es apropiada. Por ejemplo, un perro es un animal, por lo tanto, tener la clase Dog heredada de Animal es probablemente un buen diseño.

Desafortunadamente, esta simple prueba de "is-a" no es confiable. El problema Circle-Ellipse es un gran contraejemplo: aunque un círculo es una elipse, es una mala idea tener la clase Circle heredada de Ellipse, porque hay cosas que los elipses pueden hacer pero los círculos no. Por ejemplo, las elipses pueden estirarse, pero los círculos no pueden. Entonces, mientras que puedes tener ellipse.stretch() , no puedes tener circle.stretch() .

Esta es la razón por la que una mejor prueba es "un Bar es un Foo, y un Bar puede hacer todo lo que un Foo puede hacer ". Esto realmente significa que Foo se puede utilizar polimorfamente. La prueba "is-a" es solo una condición necesaria para el uso polimórfico, y generalmente significa que todos los captadores de Foo tienen sentido en Bar. La prueba adicional "lo puede hacer todo" significa que todos los creadores de Foo también tienen sentido en Bar. Esta prueba adicional generalmente falla cuando una barra de clase "es-un" Foo, pero le agrega algunas restricciones, en cuyo caso no debe usar la herencia, porque Foo no se pudo usar polimorfamente. En otras palabras, la herencia no se trata de compartir propiedades, sino de compartir la funcionalidad. Las clases derivadas deben ampliar la funcionalidad de las clases base, no restringirlas.

Esto es equivalente al principio de sustitución de Liskov :

Las funciones que usan punteros o referencias a clases base deben poder usar objetos de clases derivadas sin saberlo


"Prefiero la composición sobre la herencia" es un principio de diseño, que dice que no abuses de la herencia cuando no encaja.

Si no existe una relación jerárquica del mundo real entre dos entidades, no utilice la herencia, sino que utilice la composición. La composición representa la relación "HAS A".

Por ejemplo, el auto TIENE ruedas, cuerpo, motor, etc. Pero si hereda aquí, se convierte en CAR IS A Wheel, lo cual es incorrecto.

Para más explicaciones, refiérase a composición preferente a herencia.


Con todos los beneficios innegables proporcionados por la herencia, aquí están algunas de sus desventajas.

Desventajas de la herencia:

  1. No puede cambiar la implementación heredada de las súper clases en tiempo de ejecución (obviamente porque la herencia se define en tiempo de compilación).
  2. La herencia expone una subclase a los detalles de la implementación de la clase principal, por eso a menudo se dice que la herencia rompe la encapsulación (en el sentido de que realmente necesita centrarse en las interfaces, no en la implementación, por lo que no siempre se prefiere reutilizar por subclasificación).
  3. El acoplamiento apretado provisto por la herencia hace que la implementación de una subclase esté muy ligada a la implementación de una superclase que cualquier cambio en la implementación principal obligará a la subclase a cambiar.
  4. La reutilización excesiva por subclasificación puede hacer que la pila de herencia sea muy profunda y confusa también.

Por otro lado, la composición de objetos se define en tiempo de ejecución a través de objetos que adquieren referencias a otros objetos. En tal caso, estos objetos nunca podrán alcanzar los datos protegidos de los demás (sin interrupción de la encapsulación) y se verán obligados a respetar la interfaz de los demás. Y en este caso también, las dependencias de implementación serán mucho menores que en caso de herencia.


Cuando desea "copiar" / Exponer la API de la clase base, utiliza la herencia. Cuando solo quieras "copiar" la funcionalidad, usa la delegación.

Un ejemplo de esto: desea crear una pila a partir de una lista.Stack solo tiene pop, push y peek. No debe usar la herencia ya que no desea la funcionalidad push_back, push_front, removeAt, et al. - en una pila.


En Java o C #, un objeto no puede cambiar su tipo una vez que se ha creado una instancia.

Por lo tanto, si su objeto debe aparecer como un objeto diferente o comportarse de manera diferente según el estado o las condiciones de un objeto, entonces use Composición : consulte Patrones de diseño de State y Strategy .

Si el objeto debe ser del mismo tipo, use Herencia o implemente interfaces.


En pocas palabras, estoy de acuerdo con "Prefiero la composición sobre la herencia", muy a menudo para mí suena como "Prefiero las papas antes que la coca-cola". Hay lugares para la herencia y lugares para la composición. Necesitas entender la diferencia, entonces esta pregunta desaparecerá. Lo que realmente significa para mí es "si va a utilizar la herencia; piénselo de nuevo, lo más probable es que necesite composición".

Debes preferir las papas a la coca cola cuando quieras comer, y la coca cola a las papas cuando quieres beber.

Crear una subclase debería significar más que una forma conveniente de llamar a los métodos de superclase. Debe usar la herencia cuando la subclase "es-una" súper clase, tanto estructural como funcionalmente, cuando se puede usar como superclase y usted va a usar eso. Si no es el caso, no es herencia, sino otra cosa. La composición es cuando tus objetos consisten en otro, o tiene alguna relación con ellos.

Entonces, para mí, parece que si alguien no sabe si necesita herencia o composición, el problema real es que no sabe si quiere beber o comer. Piensa más en tu dominio del problema, entiéndelo mejor.


La herencia crea una fuerte relación entre una subclase y una super clase; La subclase debe conocer los detalles de implementación de las súper clases. Crear la súper clase es mucho más difícil, cuando tienes que pensar en cómo se puede extender. Debe documentar los invariantes de clase con cuidado e indicar qué otros métodos reemplazables utilizan internamente los métodos.

La herencia es a veces útil, si la jerarquía realmente representa una relación es-una-es. Se relaciona con el principio abierto-cerrado, que establece que las clases deben cerrarse para su modificación pero estar abiertas a la extensión. De esa manera puedes tener polimorfismo; para tener un método genérico que se ocupe del supertipo y sus métodos, pero a través del envío dinámico se invoca el método de la subclase. Esto es flexible y ayuda a crear una indirección, que es esencial en el software (para saber menos sobre los detalles de la implementación).

Sin embargo, la herencia se sobreutiliza fácilmente y crea una complejidad adicional, con duras dependencias entre clases. Además, comprender qué sucede durante la ejecución de un programa es bastante difícil debido a las capas y la selección dinámica de llamadas a métodos.

Yo sugeriría usar la composición por defecto. Es más modular y ofrece el beneficio de un enlace tardío (puede cambiar el componente dinámicamente). También es más fácil probar las cosas por separado. Y si necesita utilizar un método de una clase, no está obligado a tomar cierta forma (principio de sustitución de Liskov).


La herencia es bastante atractiva, especialmente cuando se trata de tierras procesales y, a menudo, se ve engañosamente elegante. Quiero decir, todo lo que necesito hacer es agregar este bit de funcionalidad a alguna otra clase, ¿verdad? Bueno, uno de los problemas es que

la herencia es probablemente la peor forma de acoplamiento que puede tener

Su clase base rompe la encapsulación al exponer los detalles de la implementación a las subclases en forma de miembros protegidos. Esto hace que su sistema sea rígido y frágil. Sin embargo, el defecto más trágico es la nueva subclase que trae consigo todo el bagaje y la opinión de la cadena de herencia.

El artículo, Herencia es malo: la falla épica del DataAnnotationsModelBinder , muestra un ejemplo de esto en C #. Muestra el uso de la herencia cuando se debería haber utilizado la composición y cómo se puede refactorizar.


La herencia es muy poderosa, pero no puedes forzarla (ver: el problema de la elipse de círculo ). Si realmente no puede estar completamente seguro de una verdadera relación de subtipo "is-a", entonces es mejor ir con la composición.


Mi regla general: antes de usar la herencia, considere si la composición tiene más sentido.

Razón: la subclasificación generalmente significa más complejidad y conexión, es decir, más difícil de cambiar, mantener y escalar sin cometer errores.

Una respuesta mucho más completa y concreta de Tim Boudreau de Sun:

Los problemas comunes al uso de la herencia como lo veo son:

  • Los actos inocentes pueden tener resultados inesperados : el ejemplo clásico de esto son las llamadas a métodos reemplazables del constructor de la superclase, antes de que se hayan inicializado los campos de instancia de subclases. En un mundo perfecto, nadie lo haría. Este no es un mundo perfecto.
  • Ofrece tentaciones perversas para que los subclases hagan suposiciones sobre el orden de las llamadas a métodos y tales , tales suposiciones tienden a no ser estables si la superclase puede evolucionar con el tiempo. Véase también mi analogía tostadora y cafetera .
  • Las clases se hacen más pesadas : no necesariamente sabes qué trabajo está haciendo tu superclase en su constructor, o cuánta memoria va a usar. Por lo tanto, construir un objeto liviano inocente podría ser mucho más costoso de lo que piensas, y esto puede cambiar con el tiempo si la superclase evoluciona
  • Se fomenta una explosión de subclases . La carga de clases cuesta tiempo, más clases cuestan memoria. Esto puede no ser un problema hasta que esté tratando con una aplicación en la escala de NetBeans, pero ahí tuvimos problemas reales, por ejemplo, los menús fueron lentos porque la primera visualización de un menú provocó una carga masiva de clases. Arreglamos esto moviéndonos a una sintaxis más declarativa y otras técnicas, pero también nos costó un tiempo arreglarlo.
  • Hace que sea más difícil cambiar las cosas más tarde , si ha hecho pública una clase, cambiar la superclase romperá las subclases, es una opción con la cual, una vez que haya hecho público el código, está casado. Entonces, si no está modificando la funcionalidad real de su superclase, tendrá mucha más libertad para cambiar las cosas más adelante si las usa, en lugar de extender lo que necesita. Tomemos, por ejemplo, subclasificando JPanel - esto suele ser incorrecto; y si la subclase es pública en algún lugar, nunca tendrá la oportunidad de revisar esa decisión. Si se accede a ella como JComponent getThePanel (), aún puede hacerlo (sugerencia: exponer modelos para los componentes dentro de su API).
  • Las jerarquías de objetos no se escalan (o hacerlos escalar más tarde es mucho más difícil que planear con anticipación) : este es el problema clásico de "demasiadas capas". Entraré en esto a continuación, y cómo el patrón AskTheOracle puede resolverlo (aunque puede ofender a los puristas de OOP).

...

Mi opinión sobre qué hacer, si permite la herencia, que puede tomar con un grano de sal es:

  • No exponga campos, nunca, excepto constantes.
  • Los métodos serán abstractos o finales.
  • No llame a ningún método del constructor de superclase.

...

todo esto se aplica menos a los proyectos pequeños que a los grandes, y menos a las clases privadas que a las públicas


No encontré una respuesta satisfactoria aquí, así que escribí una nueva.

Para comprender por qué " preferir la composición a la herencia", primero debemos recuperar el supuesto omitido en este lenguaje abreviado.

Hay dos beneficios de la herencia: subtipos y subclases

  1. Subtitulado significa que se ajusta a una firma de tipo (interfaz), es decir, un conjunto de API, y uno puede anular parte de la firma para lograr el subtipo de polimorfismo.

  2. La subclasificación significa la reutilización implícita de implementaciones de métodos.

Con los dos beneficios viene dos propósitos diferentes para hacer la herencia: orientado a los subtipos y la reutilización del código.

Si la reutilización del código es el único propósito, las subclases pueden dar una más de lo que necesita, es decir, algunos métodos públicos de la clase principal no tienen mucho sentido para la clase secundaria. En este caso, en lugar de favorecer la composición sobre la herencia, se exige la composición. Aquí es también de donde viene la noción "is-a" vs. "has-a".

Entonces, solo cuando se propone el subtipo, es decir, para usar la nueva clase más adelante de manera polimórfica, nos enfrentamos al problema de elegir la herencia o la composición. Este es el supuesto que se omite en el lenguaje abreviado en discusión.

Subtipo es ajustarse a una firma de tipo, esto significa que la composición siempre tiene que exponer no menos cantidad de API del tipo. Ahora las compensaciones se inician:

  1. La herencia proporciona una reutilización directa del código si no se anula, mientras que la composición tiene que volver a codificar cada API, incluso si es un simple trabajo de delegación.

  2. La herencia proporciona una recursión abierta y directa a través del sitio polimórfico interno, es decir, invoca el método de anulación (o incluso el type ) en otra función miembro, ya sea pública o privada (aunque no se discouraged ). La recursión abierta se puede simular a través de la composición , pero requiere un esfuerzo adicional y puede que no siempre sea viable (?). Esta answer a una pregunta duplicada habla algo similar.

  3. La herencia expone a los miembros protegidos . Esto rompe la encapsulación de la clase padre, y si se usa por subclase, se introduce otra dependencia entre el hijo y su padre.

  4. La composición tiene el ajuste de la inversión de control, y su dependencia se puede inyectar dinámicamente, como se muestra en el patrón decorador y el patrón proxy .

  5. La composición tiene el beneficio de combinator-oriented programación combinator-oriented , es decir, trabajar de una manera similar al patrón compuesto .

  6. La composición sigue inmediatamente la programación a una interfaz .

  7. La composición tiene el beneficio de una herencia múltiple fácil.

Teniendo en cuenta los compromisos anteriores, preferimos la composición sobre la herencia. Sin embargo, para las clases estrechamente relacionadas, es decir, cuando la reutilización implícita del código realmente hace beneficios, o se desea el poder mágico de la recursión abierta, la herencia será la elección.


Otra razón muy pragmática para preferir la composición sobre la herencia tiene que ver con su modelo de dominio y asignarlo a una base de datos relacional. Es realmente difícil asignar la herencia al modelo SQL (terminas con todo tipo de soluciones piratas, como crear columnas que no siempre se usan, usar vistas, etc.). Algunos ORML intentan lidiar con esto, pero siempre se complica rápidamente. La composición se puede modelar fácilmente a través de una relación de clave externa entre dos tablas, pero la herencia es mucho más difícil.


Personalmente aprendí a preferir siempre la composición a la herencia. No hay un problema programático que puedas resolver con herencia que no puedas resolver con la composición; aunque es posible que tenga que usar Interfaces (Java) o Protocolos (Obj-C) en algunos casos. Como C ++ no sabe nada de eso, tendrá que usar clases base abstractas, lo que significa que no puede deshacerse por completo de la herencia en C ++.

La composición es a menudo más lógica, proporciona una mejor abstracción, mejor encapsulación, mejor reutilización del código (especialmente en proyectos muy grandes) y es menos probable que rompa algo a distancia simplemente porque realizó un cambio aislado en cualquier parte del código. También hace que sea más fácil mantener el " Principio de Responsabilidad Única ", que a menudo se resume como " Nunca debe haber más de una razón para que una clase cambie ", y significa que cada clase existe para un propósito específico y debería Sólo tienen métodos que están directamente relacionados con su propósito. Además, tener un árbol de herencia muy superficial hace que sea mucho más fácil mantener la visión general incluso cuando su proyecto comienza a ser realmente grande. Muchas personas piensan que la herencia representa nuestro mundo real bastante bien, pero esa no es la verdad. El mundo real usa mucha más composición que herencia. Casi todos los objetos del mundo real que puedes tener en tu mano han sido compuestos de otros objetos del mundo real más pequeños.

Sin embargo, hay desventajas de la composición. Si omite la herencia por completo y solo se enfoca en la composición, notará que a menudo tiene que escribir un par de líneas de código adicionales que no eran necesarias si había usado la herencia. También a veces se ve obligado a repetirlo y esto viola el principio DRY (DRY = Don''t Repeat Yourself). Además, la composición a menudo requiere delegación, y un método simplemente llama a otro método de otro objeto sin ningún otro código alrededor de esta llamada. Estas "llamadas de método doble" (que pueden extenderse fácilmente a las llamadas de método triple o cuádruple e incluso más lejos que eso) tienen un desempeño mucho peor que la herencia, donde simplemente hereda un método de su padre. Llamar a un método heredado puede ser igual de rápido que llamar a uno no heredado, o puede ser un poco más lento, pero generalmente es más rápido que dos llamadas de método consecutivas.

Es posible que haya notado que la mayoría de los idiomas OO no permiten la herencia múltiple. Si bien hay un par de casos en los que la herencia múltiple realmente puede comprarle algo, pero esas son más bien excepciones que la regla. Siempre que se encuentre en una situación en la que piense que "la herencia múltiple sería una característica realmente genial para resolver este problema", generalmente se encuentra en un punto en el que debería replantearse la herencia por completo, ya que incluso puede requerir un par de líneas de código adicionales. Una solución basada en la composición generalmente resultará mucho más elegante, flexible y a prueba de futuro.

La herencia es realmente una característica interesante, pero me temo que se ha usado en exceso en los últimos años. Las personas trataron la herencia como el único martillo que puede clavarlo todo, sin importar si en realidad fue un clavo, un tornillo o quizás algo completamente diferente.


Piense en la contención como una relación. Un automóvil "tiene un" motor, una persona "tiene un" nombre, etc.

Piense en la herencia como una relación. Un automóvil "es un" vehículo, una persona "es un" mamífero, etc.

No tomo crédito por este enfoque. Lo tomé directamente de la Segunda edición de Code Complete de Steve McConnell , Sección 6.3 .


Si entiendes la diferencia, es más fácil de explicar.

Código de procedimiento

Un ejemplo de esto es PHP sin el uso de clases (particularmente antes de PHP5). Toda la lógica está codificada en un conjunto de funciones. Puede incluir otros archivos que contengan funciones de ayuda y así sucesivamente, y conducir su lógica de negocios pasando datos en funciones. Esto puede ser muy difícil de manejar a medida que la aplicación crece. PHP5 intenta remediar esto ofreciendo un diseño más orientado a objetos.

Herencia

Esto fomenta el uso de las clases. La herencia es uno de los tres principios del diseño OO (herencia, polimorfismo, encapsulación).

class Person { String Title; String Name; Int Age } class Employee : Person { Int Salary; String Title; }

Esta es la herencia en el trabajo. El Empleado "es una" Persona o hereda de la Persona. Todas las relaciones de herencia son relaciones "is-a". El empleado también sombrea la propiedad Título de la Persona, lo que significa Empleado. El título devolverá el Título para el Empleado, no la Persona.

Composición

La composición se favorece sobre la herencia. Para decirlo muy simplemente, usted tendría:

class Person { String Title; String Name; Int Age; public Person(String title, String name, String age) { this.Title = title; this.Name = name; this.Age = age; } } class Employee { Int Salary; private Person person; public Employee(Person p, Int salary) { this.person = p; this.Salary = salary; } } Person johnny = new Person ("Mr.", "John", 25); Employee john = new Employee (johnny, 50000);

La composición es típicamente "tiene una" o "usa una" relación. Aquí la clase Empleado tiene una Persona. No hereda de Person, sino que le pasa el objeto Person, por lo que "tiene una" Person.

Composición sobre herencia

Ahora diga que desea crear un tipo de administrador para que termine con:

class Manager : Person, Employee { ... }

Este ejemplo funcionará bien, sin embargo, ¿qué pasaría si la Persona y el Empleado declararan el Title ? ¿Debe Manager.Title devolver "Manager of Operations" o "Mr."? Bajo la composición esta ambigüedad se maneja mejor:

Class Manager { public string Title; public Manager(Person p, Employee e) { this.Title = e.Title; } }

El objeto Manager está compuesto como un empleado y una persona. El comportamiento del título es tomado del empleado. Esta composición explícita elimina la ambigüedad entre otras cosas y encontrarás menos errores.


Supongamos que un avión tiene solo dos partes: un motor y alas.
Luego hay dos formas de diseñar una clase de avión.

Class Aircraft extends Engine{ var wings; }

Ahora tu aeronave puede comenzar con las alas fijas.
Y cambiarlos a alas rotativas sobre la marcha. Es esencialmente
Un motor con alas. Pero que tal si quisiera cambiar.
¿El motor sobre la marcha también?

O bien el Engine clase base expone un mutador para cambiar su
propiedades, o rediseño de Aircraft como:

Class Aircraft { var wings; var engine; }

Ahora, también puedo reemplazar mi motor sobre la marcha.


Prefiera la composición sobre la herencia, ya que es más maleable / fácil de modificar más adelante, pero no utilice un enfoque de componer-siempre. Con la composición, es fácil cambiar el comportamiento sobre la marcha con Dependency Injection / Setters. La herencia es más rígida, ya que la mayoría de los idiomas no le permiten derivar de más de un tipo. Así que el ganso está más o menos cocido una vez que se deriva de TypeA.

Mi prueba de ácido para lo anterior es:

  • ¿Desea TypeB exponer la interfaz completa (todos los métodos públicos no menos) de TypeA de modo que TypeB se pueda usar donde se espera TypeA? Indica Herencia .

por ejemplo, un biplano Cessna expondrá la interfaz completa de un avión, si no más. Así que eso hace que sea apropiado derivar de Avión.

  • ¿TypeB quiere solo una parte del comportamiento expuesto por TypeA? Indica necesidad de composición.

Por ejemplo, un pájaro puede necesitar solo el comportamiento de vuelo de un avión. En este caso, tiene sentido extraerlo como una interfaz / clase / ambos y convertirlo en un miembro de ambas clases.

Actualización: acabo de volver a mi respuesta y parece que ahora está incompleta sin una mención específica del Principio de Sustitución de Liskov de Barbara Liskov como prueba para "¿Debo heredar de este tipo?"



Estas dos formas pueden vivir juntas muy bien y realmente apoyarse mutuamente.

La composición es solo un juego modular: creas una interfaz similar a la clase padre, creas un nuevo objeto y le delegas las llamadas. Si estos objetos no necesitan conocerse, es una composición bastante segura y fácil de usar. Hay tantas posibilidades aquí.

Sin embargo, si la clase principal necesita acceder a las funciones proporcionadas por la "clase secundaria" para programadores inexpertos, puede parecer que es un excelente lugar para usar la herencia. La clase padre solo puede llamar a su propio resumen "foo ()" que es sobrescrito por la subclase y luego puede dar el valor a la base abstracta.

Parece una buena idea, pero en muchos casos es mejor simplemente darle a la clase un objeto que implemente foo () (o incluso establecer el valor proporcionado foo () manualmente) que heredar la nueva clase de alguna clase base que requiera La función foo () se especificará.

¿Por qué?

Porque la herencia es una mala manera de mover la información .

La composición tiene una ventaja real aquí: la relación se puede revertir: la "clase principal" o el "trabajador abstracto" pueden agregar cualquier objeto "secundario" específico implementando cierta interfaz + cualquier elemento secundario se puede configurar dentro de cualquier otro tipo de padre, que acepte su tipo . Y puede haber cualquier número de objetos, por ejemplo, MergeSort o QuickSort podrían ordenar cualquier lista de objetos implementando una interfaz de comparación abstracta. O para decirlo de otra manera: cualquier grupo de objetos que implemente "foo ()" y otro grupo de objetos que puedan hacer uso de objetos que tengan "foo ()" pueden jugar juntos.

Puedo pensar en tres razones reales para usar la herencia:

  1. Tienes muchas clases con la misma interfaz y quieres ahorrar tiempo al escribirlas
  2. Tienes que usar la misma clase base para cada objeto
  3. Es necesario modificar las variables privadas, que en ningún caso pueden ser públicas.

Si esto es cierto, entonces es probable que sea necesario usar la herencia.

No hay nada malo en usar la razón 1, es muy bueno tener una interfaz sólida en sus objetos. Esto se puede hacer usando la composición o con herencia, no hay problema, si esta interfaz es simple y no cambia. Usualmente la herencia es bastante efectiva aquí.

Si el motivo es el número 2, se pone un poco complicado. ¿Realmente solo necesitas usar la misma clase base? En general, solo usar la misma clase base no es lo suficientemente bueno, pero puede ser un requisito de su marco, una consideración de diseño que no se puede evitar.

Sin embargo, si desea usar las variables privadas, el caso 3, entonces puede estar en problemas. Si considera que las variables globales son inseguras, entonces debería considerar el uso de la herencia para obtener acceso a las variables privadas también inseguras . Tenga en cuenta que las variables globales no son todas tan malas; las bases de datos son esencialmente un gran conjunto de variables globales. Pero si puedes manejarlo, entonces está bastante bien.


Estoy de acuerdo con @Pavel, cuando él dice, hay lugares para la composición y hay lugares para la herencia.

Creo que la herencia debería usarse si su respuesta es afirmativa a cualquiera de estas preguntas.

  • ¿Es tu clase parte de una estructura que se beneficia del polimorfismo? Por ejemplo, si tenía una clase de Forma, que declara un método llamado draw (), claramente necesitamos que las clases de Círculo y Cuadrado sean subclases de Forma, de modo que sus clases de cliente dependerían de la Forma y no de subclases específicas.
  • ¿Necesita su clase reutilizar las interacciones de alto nivel definidas en otra clase? El patrón de diseño del método de la plantilla sería imposible de implementar sin herencia. Creo que todos los marcos extensibles utilizan este patrón.

Sin embargo, si su intención es puramente la de reutilizar el código, lo más probable es que la composición sea una mejor opción de diseño.


Para abordar esta pregunta desde una perspectiva diferente para los programadores más nuevos:

La herencia a menudo se enseña temprano cuando aprendemos programación orientada a objetos, por lo que se ve como una solución fácil a un problema común.

Tengo tres clases que necesitan alguna funcionalidad común. Entonces, si escribo una clase base y todas las heredan de ella, todas tendrán esa funcionalidad y solo necesitaré mantenerla en un solo lugar.

Suena bien, pero en la práctica casi nunca funciona, por una de las varias razones:

  • Descubrimos que hay otras funciones que queremos que tengan nuestras clases. Si la forma en que agregamos funcionalidad a las clases es a través de la herencia, tenemos que decidir: ¿la agregamos a la clase base existente, aunque no todas las clases que la heredan necesitan esa funcionalidad? ¿Creamos otra clase base? Pero ¿qué pasa con las clases que ya heredan de la otra clase base?
  • Descubrimos que solo para una de las clases que se hereda de nuestra clase base queremos que la clase base se comporte de manera un poco diferente. Así que ahora volvemos y jugueteamos con nuestra clase base, tal vez agregando algunos métodos virtuales, o incluso peor, algún código que diga: "Si soy heredado de tipo A, haz esto, pero si soy heredado de tipo B, haz eso . " Eso es malo por muchas razones. Una es que cada vez que cambiamos la clase base, cambiamos efectivamente cada clase heredada. Así que realmente estamos cambiando las clases A, B, C y D porque necesitamos un comportamiento ligeramente diferente en la clase A. Por más cuidadoso que pensemos, podríamos romper una de esas clases por razones que no tienen nada que ver con esas clases
  • Podríamos saber por qué decidimos hacer que todas estas clases se hereden unas de otras, pero podría no (probablemente no) tener sentido para otra persona que tenga que mantener nuestro código. Podríamos obligarlos a tomar una decisión difícil: ¿hago algo realmente feo y desordenado para hacer el cambio que necesito (ver el punto anterior) o simplemente reescribo un montón de esto?

Al final, vinculamos nuestro código con algunos nudos difíciles y no obtenemos ningún beneficio de ello excepto que podemos decir: "Genial, aprendí sobre la herencia y ahora lo usé". Eso no está destinado a ser condescendiente porque todos lo hemos hecho. Pero todos lo hicimos porque nadie nos dijo que no lo hiciéramos.

Tan pronto como alguien me explicó "favorecer la composición sobre la herencia", recordé cada vez que intentaba compartir la funcionalidad entre clases utilizando la herencia y me di cuenta de que la mayoría de las veces no funcionaba bien.

El antídoto es el principio de responsabilidad única . Piense en ello como una restricción. Mi clase debe hacer una cosa. Yo debo ser capaz de dar mi clase un nombre que de alguna manera describe que una cosa que hace. (Hay excepciones para todo, pero las reglas absolutas a veces son mejores cuando estamos aprendiendo). Por lo tanto, no puedo escribir una clase base llamada ObjectBaseThatContainsVariousFunctionsNeededByDifferentClasses. Cualquiera que sea la funcionalidad distinta que necesito debe estar en su propia clase, y luego otras clases que necesitan esa funcionalidad pueden depender de esa clase, no heredarla.

A riesgo de simplificar en exceso, eso es composición: componer varias clases para trabajar en conjunto. Y una vez que formamos ese hábito, encontramos que es mucho más flexible, mantenible y comprobable que el uso de la herencia.


A pesar de que se prefiere la composición, me gustaría resaltar las ventajas de la herencia y los inconvenientes de la composición .

Ventajas de la herencia:

  1. Establece una relación lógica " ES A" . Si Automóvil y Camión son dos tipos de Vehículo (clase base), la clase infantil ES Una clase base.

    es decir

    El coche es un vehículo

    Camion es un vehiculo

  2. Con la herencia, puede definir / modificar / ampliar una capacidad

    1. La clase base no proporciona implementación y la subclase tiene que anular el método completo (resumen) => Puede implementar un contrato
    2. La clase base proporciona una implementación predeterminada y la subclase puede cambiar el comportamiento => Puede volver a definir el contrato
    3. La subclase agrega extensión a la implementación de la clase base al llamar a super.methodName () como primera declaración => Puede extender un contrato
    4. La clase base define la estructura del algoritmo y la subclase anulará una parte del algoritmo => Puede implementar Template_method sin cambios en el esqueleto de la clase base

Contras de la composición:

  1. En la herencia, la subclase puede invocar directamente el método de clase base aunque no esté implementando el método de clase base debido a la relación ISA . Si usa la composición, debe agregar métodos en la clase contenedora para exponer la clase contenida API

Por ejemplo, si el Coche contiene Vehículo y si tiene que obtener el precio del Coche , que se ha definido en Vehículo , su código será así.

class Vehicle{ protected double getPrice(){ // return price } } class Car{ Vehicle vehicle; protected double getPrice(){ return vehicle.getPrice(); } }


Además de tener / tiene una consideración, uno también debe considerar la "profundidad" de la herencia que debe atravesar su objeto. Cualquier cosa más allá de cinco o seis niveles de herencia profunda puede causar problemas inesperados de lanzamiento y boxeo / desempaquetado, y en esos casos podría ser conveniente componer su objeto.


Como mucha gente dijo, primero comenzaré con la verificación: si existe una relación "is-a". Si existe usualmente verifico lo siguiente:

Si la clase base puede ser instanciada. Es decir, si la clase base puede ser no abstracta. Si puede ser no abstracto prefiero la composición.

Ej. 1. El contador es un empleado. Pero no usaré la herencia porque se puede crear una instancia de un objeto Employee.

Por ejemplo, 2. Book is a SellingItem. Un SellingItem no puede ser instanciado, es un concepto abstracto. Por lo tanto voy a utilizar la herencia. El SellingItem es una clase base abstracta (o interfaz en C #)

¿Qué opinas sobre este enfoque?

Además, apoyo la respuesta de @anon en ¿Por qué usar la herencia?

La razón principal para usar la herencia no es como una forma de composición, es para que pueda obtener un comportamiento polimórfico. Si no necesita polimorfismo, probablemente no debería usar la herencia.

@MatthieuM. dice en https://softwareengineering.stackexchange.com/questions/12439/code-smell-inheritance-abuse/12448#comment303759_12448

El problema con la herencia es que se puede usar para dos propósitos ortogonales:

interfaz (para el polimorfismo)

Implementación (para reutilización de código)

REFERENCIA

  1. ¿Qué clase de diseño es mejor?
  2. Herencia vs. Agregación

Cuando tienes una relación is-a entre dos clases (por ejemplo, dog es un canino), vas por herencia.

Por otro lado, cuando tiene una relación de adjetivo con o entre algunas clases (el estudiante tiene cursos) o (cursos de estudios para maestros), eligió la composición.


El subtipo es apropiado y más poderoso donde los invariantes se pueden enumerar , de lo contrario, use la composición de la función para la extensibilidad.


La composición v / s La herencia es un tema amplio. No hay una respuesta real para lo que es mejor, ya que creo que todo depende del diseño del sistema.

En general, el tipo de relación entre el objeto proporciona mejor información para elegir uno de ellos.

Si el tipo de relación es "IS-A", entonces la herencia es un mejor enfoque. de lo contrario, el tipo de relación es "HAS-A", entonces la composición se aproximará mejor.

Depende totalmente de la relación de la entidad.


La herencia es un mecanismo muy poderoso para la reutilización del código. Pero necesita ser utilizado correctamente. Yo diría que la herencia se usa correctamente si la subclase es también un subtipo de la clase padre. Como se mencionó anteriormente, el Principio de Sustitución de Liskov es el punto clave aquí.

La subclase no es lo mismo que el subtipo. Puede crear subclases que no sean subtipos (y esto es cuando debe usar la composición). Para entender qué es un subtipo, comencemos a dar una explicación de qué es un tipo.

Cuando decimos que el número 5 es de tipo entero, afirmamos que 5 pertenece a un conjunto de valores posibles (como ejemplo, vea los valores posibles para los tipos primitivos de Java). También estamos indicando que hay un conjunto válido de métodos que puedo realizar en el valor como la suma y la resta. Y, finalmente, estamos diciendo que hay un conjunto de propiedades que siempre están satisfechas, por ejemplo, si agrego los valores 3 y 5, obtendré 8 como resultado.

Para dar otro ejemplo, piense en los tipos de datos abstractos, Conjunto de enteros y Lista de enteros, los valores que pueden contener están restringidos a los enteros. Ambos soportan un conjunto de métodos, como add (newValue) y size (). Y ambos tienen diferentes propiedades (clase invariante), Conjuntos no permite duplicados, mientras que Lista sí permite duplicados (por supuesto, hay otras propiedades que ambos satisfacen).

El subtipo es también un tipo, que tiene una relación con otro tipo, llamado tipo principal (o supertipo). El subtipo debe satisfacer las características (valores, métodos y propiedades) del tipo principal. La relación significa que en cualquier contexto donde se espera el supertipo, puede ser sustituible por un subtipo, sin afectar el comportamiento de la ejecución. Vayamos a ver un código para ejemplificar lo que estoy diciendo. Supongamos que escribo una lista de enteros (en algún tipo de pseudo lenguaje):

class List { data = new Array(); Integer size() { return data.length; } add(Integer anInteger) { data[data.length] = anInteger; } }

Luego, escribo el Conjunto de enteros como una subclase de la Lista de enteros:

class Set, inheriting from: List { add(Integer anInteger) { if (data.notContains(anInteger)) { super.add(anInteger); } } }

Nuestra clase Conjunto de enteros es una subclase de Lista de enteros, pero no es un subtipo, ya que no satisface todas las características de la clase Lista. Los valores y la firma de los métodos se cumplen, pero las propiedades no. El comportamiento del método add (Integer) se ha cambiado claramente, sin conservar las propiedades del tipo principal. Piensa desde el punto de vista del cliente de tus clases. Es posible que reciban un conjunto de enteros donde se espera una lista de enteros. Es posible que el cliente desee agregar un valor y obtener ese valor agregado a la Lista, incluso si ese valor ya existe en la Lista. Pero ella no obtendrá ese comportamiento si el valor existe. Una gran sorpresa para ella!

Este es un ejemplo clásico de un uso incorrecto de la herencia. Usa la composición en este caso.

(un fragmento de: usa la herencia adecuadamente ).


Una forma sencilla de entender esto sería que la herencia debería usarse cuando necesita que un objeto de su clase tenga la misma interfaz que su clase principal, de modo que pueda tratarse como un objeto de la clase principal (conversión ascendente) . Además, las llamadas de función en un objeto de clase derivada seguirían siendo las mismas en todas partes del código, pero el método específico a llamar se determinaría en tiempo de ejecución (es decir, la implementación de bajo nivel difiere, la interfaz de alto nivel sigue siendo la misma).

La composición se debe utilizar cuando no necesita que la nueva clase tenga la misma interfaz, es decir, desea ocultar ciertos aspectos de la implementación de la clase que el usuario de esa clase no necesita conocer. Por lo tanto, la composición es más compatible con la encapsulación (es decir, oculta la implementación), mientras que la herencia se destina a la abstracción (es decir, proporciona una representación simplificada de algo, en este caso, la misma interfaz para un rango de tipos con diferentes elementos internos).


Una regla empírica que he escuchado es que la herencia debe usarse cuando se trata de una relación "is-a" y una composición cuando es un "has-a". Incluso con eso, siento que siempre deberías inclinarte hacia la composición porque elimina mucha complejidad.