¿Es la herencia una mala práctica en OOP? Muchos lugares que enseñan patrones de diseño dicen que optan por la composición sobre la herencia, pero ¿qué pasa cuando varias clases comparten la lógica de una clase abstracta como en el patrón de diseño del Método de plantilla?

Respuesta corta: demonios sí. Respuesta más larga: si tiene problemas para caminar, es posible que necesite muletas (herencia).

Lo que sigue es una lectura muy larga para Quora, pero has hecho una pregunta sobre la que la gente ha discutido durante cincuenta años, por lo que darte una respuesta simple sería injusto. Además, porque creo que este es un tema muy importante en nuestra industria, voy a pedirle un favor. Cuando haya terminado de leer esto, haga clic en “Votación a favor” o deje una respuesta que indique por qué no está de acuerdo. Y por favor comparte esto. La herencia tiene su lugar, pero es tan problemático que las personas deben ser conscientes de estos problemas.

Para una respuesta más completa, regresemos en el tiempo a 1967. Fue entonces cuando se lanzó Simula 67, que ofrece el primer código orientado a objetos “moderno”. Vale la pena tener en cuenta que esto fue hace casi 50 años (a partir de este escrito) y uno pensaría que hemos aprendido mucho sobre la programación OO, pero en realidad, todavía tenemos mucho más que aprender.

En Simula 67 tuvimos clases, polimorfismo, encapsulación y herencia. Para la OO basada en la clase, los primeros tres han resistido la prueba del tiempo y han sido más o menos “no controvertidos” durante cinco décadas. La herencia fue difícil casi desde el primer día. Cuando tienes una práctica de programación que la gente está discutiendo y debatiendo durante cincuenta años, vale la pena echarle otro vistazo. En particular, dado que Alan Kay, el inventor del término “programación orientada a objetos”, tenía serias reservas sobre la herencia, hasta el punto de dejar la herencia completamente fuera de la definición.

¿Lo tengo? Al científico informático que nombró la programación OO y se le considera uno de sus inventores, no le gusta la herencia. Creo que sería una tontería descartar sus preocupaciones.

La herencia viola la encapsulación

Aquí hay solo un ejemplo de por qué eso es problemático. Como regla general, si una clase está abierta para subclasificar (he notado que declarar clases como “final” se usa principalmente como un truco de rendimiento y no por razones comerciales concretas), la clase no debería saber sobre sus descendientes, aunque el los descendientes, por supuesto, necesitan saber sobre sus antepasados. Ahora imagine que tiene una cadena de herencia donde Child hereda de Parent heredando de GrandParent .

Child naturalmente, se basa en un comportamiento de GrandParent , pero en realidad, solo debe tener en cuenta la clase de los Parent . Esto se debe a que el contrato presentado por el Parent debe ser inviolable (abierto para la extensión, no cambiar) y si el Parent luego decide heredar de una clase diferente, el Child aún debe trabajar. En la práctica, cuanto más heredes, mayor será el comportamiento que estarás tomando y que tal vez no necesites, y es difícil estar seguro exactamente de dónde proviene ese comportamiento. Esto hace que las cadenas de herencia profundas sean más difíciles de depurar y mantener.

Entonces, el autor de la clase Parent implementa un método público foo() después de verificar que la clase GrandParent no lo hace. La clase Child ahora hereda Parent.foo() , pero luego el autor de la clase GrandParent dice “Necesito implementar GrandParent.foo() “. Dado que una clase generalmente no debe conocer a sus descendientes, no tienen idea de que este método público se anulará más adelante. Mientras tanto, la clase Child se da cuenta de que necesitan GrandParent.foo() y no Parent.foo() , pero si pueden o no llamar a ese método directamente depende del idioma. Si la clase Child puede llamar a GrandParent.foo() directamente, eso es una violación grave de encapsulación (solo deben saber sobre Parent y Parent debe tener la libertad de heredar de otra clase). Si no pueden llamarlo directamente, pueden verse obligados a cortar y pegar el comportamiento de GrandParent.foo() , o encontrar alguna otra solución incómoda.

Y luego GrandChild hereda de Child y quiere implementar la llamada Parent.foo() . Ups

Debido a que ha vinculado estrechamente las clases secundarias con las clases para padres, ha violado la encapsulación y lo anterior es simplemente un ejemplo de cómo las cosas pueden salir mal. Puede visitar Google para obtener muchas otras explicaciones de por qué la herencia viola la encapsulación.

Debo agregar que la gente suele estar en dos campos sobre esto. Aquellos que escriben y lanzan muchos pequeños módulos / bibliotecas / paquetes / lo que sea, a menudo no experimentan el dolor anterior. Sin embargo, aquellos que trabajan en sistemas a gran escala (lo hago), son más propensos a ver estos problemas de primera mano. Es como un principiante escribiendo un guión de 100 líneas y sin darse cuenta de los problemas con las variables globales: ciertas malas prácticas no escalan, pero hasta que escalas, puede ser difícil ver por qué son malas.

La herencia múltiple es una abominación

La herencia única tiene muchos problemas y apenas he arañado la superficie, pero la herencia múltiple (MI) es tan problemática que muchos idiomas la prohíben por completo, pero intentan ofrecerle algunas alternativas (por ejemplo, Java e interfaces). Otros idiomas, como Perl, permiten la herencia múltiple, pero generalmente se le advierte que no la use. Por qué no?

Imagine la siguiente jerarquía de herencia:

Si su papel de la Copier atasca, puede pedirle que halt() para evitar daños. Afortunadamente, no tiene que escribir ese método porque ya está implementado en Printer . Desafortunadamente, debido a que la herencia generalmente se implementa a través de la búsqueda en profundidad del árbol de herencia, ha llamado al método PoweredDevice.halt() , eliminando la energía de la Copier completo, perdiendo todos los otros trabajos en cola. ¡Increíble!

Hay algo llamado linealización C3 que intenta implementar una búsqueda inteligente de amplitud de la jerarquía de herencia para el método más adecuado para sus necesidades, pero todo lo que hace es mitigar el problema. Encontrará el siguiente método que parece coincidir más claramente con lo que busca su código; no puede garantizar que encontrará el código que realmente hace lo que necesita que haga. Por lo tanto, si bien es un algoritmo bien definido, todavía es solo una ruptura heurística y heurística, tarde o temprano. Vea mis comentarios anteriores sobre “escala”.

Podría agregar que algunos consideran que el término “linealización C3” es un poco demasiado “comp-sci speak” para ellos, así que ofrezco un nombre alternativo: “trapear el Titanic”. Cuando tienes que saltar a través de aros tan extremos para evitar un problema, ¿tal vez es mejor solucionar el problema?

¿Qué es la herencia?

Una subclase generalmente se considera una versión más específica de una clase principal. Por ejemplo, un Perro podría ser una subclase de Mamífero que, a su vez, podría ser una subclase de Animal . Los perros no heredan de los árboles solo porque quieres un método bark() . Ten eso en mente.

Si bien no puedo encontrar la cita en este momento (disculpas), los creadores del lenguaje de programación Beta solo admiten herencia única. Cuando estaban investigando cómo admitir la herencia múltiple (MI), descubrieron que ninguno de los ejemplos que pudieron encontrar era sobre la creación de versiones más específicas de las cosas. En cambio, se trataba de compartir comportamientos. Por lo tanto, MI en el mundo real parecía violar la intención de la herencia en general y los desarrolladores Beta simplemente lo omitieron (para ser justos, también hubo consideraciones prácticas).

¿Qué significa esto? Bueno, el ornitorrinco es un mamífero, pero pone huevos. Los animales que ponen huevos se llaman “ovíparos”. La mayoría de los mamíferos gestan a sus crías en el útero y son “vivas”. Claramente, es cierto que un ornitorrinco patito es un mamífero, pero no es vivas. Si desea modelar su ciclo reproductivo, ¿cómo comparte ese comportamiento complejo sin copiarlo? ¿Vas a usar herencia múltiple? Tal vez tengas una clase EggLaying (¡las arañas también la usan!). Así que tienes:

clase DuckBilledPlatypus isa Mamífero isa EggLaying {

}

Excepto … EggLaying es un adjetivo, no un sustantivo. ¿Por qué es eso incluso una clase? ¿Y debería heredar de eso antes o después de la otra clase?

Si bien el ejemplo anterior es tonto, muestra un problema crítico con la herencia: los problemas del mundo real no se ajustan perfectamente a la estructura de herencia en forma de árbol.

Pero te escucho decir “¡ah, ja! ¡Es un hombre de paja porque olvidaste la delegación que puede resolver fácilmente este problema!

No, no olvidé la delegación.

El problema con la delegación

Me gusta la delegación y la uso, pero también se abusa de ella. Aquí hay un ejemplo del mundo real.

Hace mucho tiempo trabajé para una empresa que necesitaba auditar sus objetos para ver quién estaba cambiando los datos. Eso era simple: acababan de heredar sus objetos de su clase Audited . Un día tuvieron un problema en el que tenían una clase que necesitaba ser Audited , pero solo para escribir una línea en un archivo de registro. Heredaba de una clase que no tenía Audited en la jerarquía de herencia. Se vieron obligados a recurrir a la herencia múltiple para implementar esta característica y eso significó que la clase en cuestión también recogió algunos comportamientos “adicionales” de Audited que no necesitaba y definitivamente no quería. Finalmente, se señaló que Auditado debería ser algo en lo que deberíamos delegar en lugar de heredar.

Esta solución era mejor, pero tenía el problema de que:

  1. Debes escribir un código de andamio adicional para implementar la delegación
  2. ¿Y por qué es un comportamiento puro, sin estado, una clase en primer lugar?

Piensa en ese último punto. Tiene sentido tener una instancia particular de, por ejemplo, una casa, una orden o un perro, pero ¿qué es una instancia de “auditado”? Esa pregunta es completamente no sensorial, excepto en los tortuosos términos de modelado OO que tenemos hoy.

¿Cuál es la raíz del problema?

Recuerde que hemos estado discutiendo sobre la herencia durante cincuenta años . Para cualquiera que no esté de acuerdo con mis puntos anteriores, puede pasar los próximos cincuenta años discutiendo con personas que sí están de acuerdo con esos puntos. Recuerda: cincuenta años de discusiones . Incluso el desarrollador más intransigente debería detenerse ante eso.

El núcleo del problema con la herencia es que las clases en realidad tienen dos propósitos. Primero, son agentes de responsabilidad. Las clases deben ser responsables de las cosas de las que son responsables y, como puede decir cualquier persona que trabaje en sistemas a gran escala, las clases asumen más responsabilidad con el tiempo y esto tiende a aumentar el tamaño de las clases.

¿Lo tengo? Las clases como agentes de responsabilidad conducen a clases más grandes .

Sin embargo, cuando hereda de una clase, es un agente de reutilización de código. Si mi Dog hereda de Mammal , no quiero descubrir que alguien haya implementado Mammal.play_mp3(filename) . De hecho, incluso si todos los métodos de Mammal son apropiados para esa clase, si solo quiero un GuardDog , hay una gran cantidad de comportamiento en la clase Mammal que probablemente no necesito o no quiero para modelar.

Las clases como agentes de reutilización de código deberían preferir clases más pequeñas .

Entonces hay una tensión fundamental en las clases cuando se usa con herencia. El secreto es desacoplar la responsabilidad y el comportamiento de la clase. Esto suena extraño, pero es sencillo.

Una forma de “resolver” esto era las interfaces, pero eso no funcionó porque las interfaces simplemente declararon la responsabilidad sin comportamiento y muchos desarrolladores se han quejado sobre el código duplicado en las aplicaciones Java debido al uso intensivo de interfaces (Java ahora permite que las interfaces también proporcionen una implementación, pero esto es solo una solución parcial).

La delegación fue interesante, pero a menudo conduce a muchas clases que puede crear instancias que, sin embargo, no deberían ser clases (vea el ejemplo Audited arriba).

Las mixinas, que provenían de una variante de Lisp llamada “Sabores”, fueron una idea interesante. Proporcionan pequeños fragmentos de comportamiento que puede “mezclar” con cualquier clase que desee. Sin embargo, en realidad no ofrecen una seguridad compositiva y tienen los mismos problemas de encapsulación que la herencia, y problemas similares a la herencia de diamantes.

Usando Ruby como un ejemplo más familiar, si su clase Person mezcla tanto en sus mixins Serializable como Storable y ambos tienen un método .marshal() , ¿cuál obtiene? Si fuera herencia múltiple, obtendría el método de la primera clase de la que heredó. ¡Con los mixins, lo obtienes de la última clase en la que te mezclaste porque Ruby implementa mixins bajo el capó como herencia única!

irb (main): 026: 0> Person.ancestors
=> [Persona, Serializable, Almacenable, Objeto, Kernel]

¡Sorpresa! También puede ver esta parte de una charla que di sobre la programación OO donde describo algunos problemas más con los mixins de Ruby.

La Programación Orientada a Aspectos (AOP) ha sido otro intento de resolver esto y, aunque era novedoso, tenía un problema central: en lugar de que la acción a distancia se considerara un error, ¡AOP lo convirtió en un concepto de primera clase! Todavía recuerdo a un amigo depurando una aplicación AspectJ roscada que falló al azar. Parece que algún desarrollador cambió el nombre de un método de manera que un corte de punto ya no coincidía y, como resultado, el consejo no pudo aplicarse a ese método, lo que significa que no estaba bloqueado y que diferentes hilos podían acceder al mismo tiempo.

Si ninguna de las cosas de AOP que escribí tenía sentido, déjame parafrasearlo: AOP fue un experimento que accidentalmente se abrió paso en la naturaleza y, cuando lo encontraste, debería ser golpeado brutalmente con martillos. Si debe usar AOP, solo aplique consejos para aumentar un programa, no lo modifique. Si el programa no se ejecuta correctamente sin Aspects, eso debería considerarse un error; esa es una de las razones por las que los aspectos a menudo se muestran solo para el registro u otros problemas triviales. (Nota al margen: uno de los autores originales del primer artículo de AOP es un amigo mío. Si eres él y estás leyendo esto, discutiremos sobre eso más adelante).

La solución

Recomiendo los rasgos de estilo Smalltalk. Se describieron por primera vez en Rasgos: Unidades de comportamiento componibles y es el documento en el que la mayoría parece confiar cuando se implementan rasgos en otros idiomas. Desafortunadamente, deberían confiar en Rasgos: El modelo formal, un documento que aclara muchas cosas (pero no todas) que son vagas en el primer documento.

En resumen, los rasgos, o “roles”, como a veces se los conoce, son unidades de comportamiento componibles. Son como mixins, pero los métodos se agregan directamente a su clase y si se encuentra otro método del mismo tipo (nombre + firma, pero depende del idioma), el código no se compila a menos que indique explícitamente qué implementación necesita . No más conjeturas heurísticas.

Entonces, la clase declara qué responsabilidades tiene, pero cualquier comportamiento que deba compartirse (como la serialización) entra en un rasgo / función.

Hay mucho más que eso, pero rutinariamente construí sistemas grandes usando solo roles y sin herencia (o limitada) y descubrí que los sistemas son más predecibles, más flexibles y más fáciles de entender. Además, encontrará que cada vez más idiomas los implementan.

Para una descripción más corta, escribí una Introducción de cinco minutos a los rasgos de estilo Smalltalk. Es bastante limitado, pero le da la esencia del problema.

¿Es la herencia una mala práctica en OOP?”

No, la herencia no es una “mala práctica”.

Sin embargo, es posible practicar mal la herencia.


Estimado lector … ¿cómo pasó de “preferir la composición a la herencia” a “la herencia es una mala práctica?”

La herencia es una herramienta, la composición es una herramienta.

Ambos tienen sus propósitos.

Hay muchas buenas razones para usar la herencia.


Al usar cualquier construcción de programación dada … debe saber y poder articular claramente por qué.

Si usa la herencia … debería poder explicar por qué es la elección correcta.

Puedes … hay muchas situaciones donde está.


No confundas una guía con una verdad absoluta.

La herencia no debe usarse para compartir código. Aunque una de las ventajas de la herencia es que puede poner un código común en la clase padre, compartir el código no debería ser la razón por la que usa la herencia.

La herencia debe usarse para modelar clases que comparten comportamiento, no código. Claro, cuando 2 clases tienen un comportamiento común, pueden compartir código. Sin embargo, lo contrario no es cierto. El hecho de que 2 clases compartan código no significa que compartan el comportamiento.

Permítanme ilustrar con un ejemplo. Digamos que está haciendo 2 clases: una es una calculadora financiera que necesita calcular Promedio y Sumas de grandes conjuntos de datos, y otra es un simulador de vuelo que necesita calcular Promedio y Sumas de grandes conjuntos de datos. Podrías decir hmm, necesito compartir código entre estas 2 clases, debería hacer que hereden de la misma clase . Entonces, terminas haciendo esto

clase MathUtils {
suma de funciones (gran conjunto de datos) {……}
función avg (gran conjunto de datos) {… ..}
}
clase FinancialCalculator extiende MathUtils {……}
clase FiightSimulator extiende MathUtils {……}

Puede llamar a las funciones de suma y promedio desde su FinancialCalculator y FLight Simulator, ¿verdad? Comparte el código poniéndolo en la clase base, perfecto, ¿verdad? ¡¡No!!

El problema aquí es el tiempo extra, su jerarquía de herencia se vuelve cada vez más complicada. Terminas con 30 clases impares con una estructura de herencia profunda de 4 niveles. Una vez que eso sucede, se hace difícil rastrear qué código va a dónde

Al final del día, lo que tiene sentido es escribir código de la manera que refleje cómo piensas sobre ellos en la vida real. El objetivo principal del diseño es hacer que el código sea fácil de entender. Si solo tiene algún sentido ahora, tendrá aún menos sentido más adelante. Las calculadoras financieras y los simuladores de vuelo no tienen nada que ver entre sí. No deberían estar unidos el uno al otro. Es mejor hacer esto

clase MathUtils {
suma de funciones (gran conjunto de datos) {……}
función avg (gran conjunto de datos) {… ..}
}

clase FinancialCalculator
// usa Math.sum y Math.avg
}
La clase FiightSimulator extiende SumAndAveragemodule {
// usa Math.sum y Math.avg
}

Esto es mucho más fácil de entender y proporciona reutilización de código. Tiene una biblioteca MathUtility que proporciona funciones matemáticas de propósito general. Fiancial Calculator usa las funciones matemáticas, y FLight Simulator usa las funciones matemáticas

Es importante entender las relaciones. La calculadora financiera UTILIZA funciones de utilidad coincidentes. Flight Simulator utiliza funciones de utilidad matemática. Una relación de uso denota composición, no herencia

Este tipo de pregunta surge de una recomendación incomprendida, a menudo convertida en dogma, incluso fuera de la iglesia que la definió.

Lo primero para evitar malentendidos es reconocer primero que la palabra herencia se usa en dos cosas bien distintas:

  • Como una construcción de lenguaje , es solo una forma de importar código de una clase a otra sin darle un nombre. Está observando más que “membresía anónima” y no tiene nada que ver con OOP. Múltiples lenguajes de herencia (como C ++) pueden hacer esto para crear “objetos compuestos”, sujetos a un despacho basado en deducciones de tipo estático.
  • Como abstracción de paradigma OOP , es una de las herramientas necesarias para implementar la sustitución de objetos (el otro requisito es el despacho de llamadas basado en el tipo de tiempo de ejecución, también conocido como ” llamadas virtuales “)

Enseñar a usar “composición sobre herencia” es lo que hace que vuelva a escribir código una y otra vez para volver a implementar interfaces dentro de objetos que tengan comportamientos similares o idénticos … ya que no puede heredar todos los comportamientos a la vez: hereda la interfaz, incrusta el comportamiento y escribe la interfaz implementación para despachar llamada de interfaz a los comportamientos.

Y esto lleva a la recomendación de ” herencia = es una ” vs ” composición = tiene una “.

Buen método, hasta que comiences pregúntate: ” ¿ Tengo el pelo corto o tengo el pelo corto?” (Ser y tener puede cambiar su papel …) convirtiéndose en un respeto ateo a las religiones basadas en paradigmas.

No es una debilidad de la herencia, sino una debilidad de los lenguajes de herencia única que no tienen un mecanismo de inyección de código en las clases. Típico para Java o C #. La recomendación de usar principalmente composición surge para superar esa debilidad.

C ++ tiene herencia múltiple : puede heredar múltiples interfaces al heredar múltiples implementaciones de comportamiento, y tiene herencia virtual para resolver “diamantes”. Una arquitectura correctamente diseñada puede usar la herencia con éxito incluso fuera de la escuela tradicional de OOP de Java . C ++ moderno también puede usar la herencia incluso sin despacho dinámico, para “etiquetar” los tipos bajo la deducción de tipo en función genérica. Nuevamente, esto no tiene nada que ver con OOP, pero sigue siendo herencia .

D tiene mixin : una clase puede heredar interfaces, así como ” fragmentos de implementación “. Esto hace que la repetición de código y las repeticiones de código sean cada vez menos necesarias, de una manera incluso mejor que la herencia múltiple de C ++.

Pero nunca menos … todavía hay alguien considerando heredar de std::string una mala práctica porque “no tiene un destructor virtual y eliminarlo es un comportamiento indefinido”

En 18 años (1998 -fist c ++ standard- a 2016 -now-) nunca eliminé -d an std::string .
No tiene ningún método virtual (no solo el destructor): no es un objeto sustituible. Ni siquiera es un objeto relacionado con OOP. Entonces, ¿de qué están hablando? Heredar de él es solo para importar la implementación de métodos en otro objeto más amplio. Ni mas ni menos. Y “componer” con él, significa reescribir 50 métodos para implementar solo un despacho pasivo. ¿Es esto real “reutilización de código”?

Moraleja: olvide las recomendaciones dogmáticas: ¡sea pragmático!

Hay casos para la herencia “es una”, composición “parte de” y secuencias de comandos simples, por ejemplo, funciones de JavaScript sin ninguna OOP. Reconocer cada caso, que puede requerir cambiar a un idioma diferente, por ejemplo, combinar Java y Groovy o Java y JavaScript, es el verdadero arte de la programación en lugar de responder “correctamente” a las preguntas teóricas de OOP y patrones de diseño en entrevistas de trabajo dentro del contexto de un solo idioma .

El mundo real, OOP fue diseñado para modelar (si alguien hizo un esfuerzo para aplicarlo a procesos comerciales complejos en lugar de escribir DTO tontos y servicios planos que los consumen) se compone de cosas más a menudo, que exhibir árboles de ascendencia orgánica. Aquí es de donde vino la “composición sobre la herencia”. Es importante reconocer y modelar ambos tipos de relaciones correctamente. Todo eso sale por la puerta una vez que alguien se gradúa y comienza su carrera típica de corrección de errores de TI e integración de sistemas heredados.

No está mal. Pero una fuente de maldad.

Conducir un automóvil a 70 MPH no es malo. Pero no es seguro hacerlo todo el tiempo.

La herencia como solución para crear comportamientos personalizados se ha usado en exceso. Y los peligros que esto trae no siempre se enseñan.

Si observa un código OOP incorrecto, los programadores inexpertos a menudo crean jerarquías profundas de herencia, y terminan en problemas profundos basados ​​en la herencia.

He aquí por qué es malo:

En un árbol profundo de herencia, el código se vuelve complejo y opaco. Cuando se llama a un método, no está claro dónde está el código que manejará el método. Puede volverse confuso rápidamente.

Obtienes más de lo que necesitas. Una nueva subclase que necesita hacer una cosa, a menudo heredará funcionalidad y datos que no necesita. Esto crea fragilidad e invita al abuso.

¿Llamar super o no? Cuando anula un método esencial, y eso se anula en algunos casos, el método de anulación debe llamar super. Pero la mayoría de los idiomas no tienen una manera de hacer cumplir esto.

No hay piezas reparables por el usuario en el interior. Algunas clases simplemente no están diseñadas para ser subclasificadas. No están diseñados de esa manera.

La subclasificación puede ser muy poderosa y realmente efectiva. Pero funciona mejor en jerarquías superficiales y con relaciones muy claramente definidas. Donde el creador de la superclase controla lo que debería ser anulado (y lo que no debería ser).

Los diseñadores de idiomas comparten parte de la culpa. Subclasificar es generalmente fácil. Pero los patrones de composición necesitan más esfuerzo.

De ningún modo. Pero la herencia está mal implementada en la mayoría de los idiomas, especialmente la herencia múltiple.

La herencia expresa una taxonomía. Para usar bien la herencia, debe acertar su taxonomía. No es bueno calzar una taxonomía incorrecta en una herencia.

Las taxonomías expresan elementos comunes en clases genéricas y diferencias en clases específicas. Esto también se basa en la teoría de conjuntos: conjuntos y subconjuntos.

La herencia expresa relaciones fijas entre entidades, es decir, propiedades que son intrínseca e invariablemente verdaderas en un sistema. Si sus relaciones no son tan fijas, usar la herencia es una mala elección.

La herencia también es la base de la reutilización del código. Los puntos en común no tienen que reescribirse en varios lugares. Esto ayuda con la comprensión y corrección del software.

Ortogonal a la herencia es el diseño de la interfaz. Las interfaces de clase deben ser pequeñas y limpias. Esto es más fundamental que la herencia, pero la herencia ofrece una forma de organizarlo.

Las taxonomías limpias y las interfaces limpias van juntas.

La herencia tiene una mala reputación, principalmente porque es mal utilizada por los graduados de CS sin experiencia en el mundo real. Déjame darte un ejemplo perfecto de buena herencia:

En django, necesito que la clase auth.user contenga información adicional, use una fuente diferente para la autenticación o, de alguna otra manera, se comporte de manera diferente al comportamiento nativo. Simplemente subclases auth.user, anulo cualquier método que necesite cambiar y le digo a django que use mi clase en lugar de la clase nativa. Todas las demás funciones siguen siendo las mismas, pero ahora me autentico con mi fuente de datos en lugar de la nativa. Hecho.

No hay mejor manera de hacerlo de manera tan simple y directa. Con las interfaces tengo que escribir todos los métodos necesarios de mi nueva clase de usuario. ¿Estoy anidado con 20 capas de herencia? No claro que no. He subclasificado un solo objeto para que se comporte de manera diferente a la predeterminada.

En lenguajes como C #, es útil cuando necesita poder tratar dos objetos diferentes de la misma manera, por ejemplo, si tiene una clase de vehículo, que está subclasificada para automóvil y camión. Cuando necesite tratarlos de la misma manera, puede obligarlos a ser vehículos y tratarlos como si fueran el mismo tipo de objeto. El resto del tiempo puede tratarlos como diferentes tipos de cosas según sea necesario.

La herencia es útil, pero como cualquier herramienta si se usa en exceso o se usa en circunstancias incorrectas, puede crear una amplia gama de problemas.

Algo que verá muchos de los principales desarrolladores en estos días, y con el que estoy totalmente de acuerdo, es que la herencia de las interfaces es buena, y la herencia de la implementación debe abordarse con precaución. (Tenga en cuenta que en algunos contextos, la herencia de interfaz se llama “subtipo”, y la herencia de implementación se llama simplemente “herencia”.) La herencia de implementación rápidamente conduce al problema de la clase base frágil si no se controla cuidadosamente. Creo que gran parte de la mala prensa que recibe OOP se debe al uso excesivo de la herencia de implementación, creando espaguetis de código con la base y las clases derivadas que interactúan de formas complejas. Ciertamente, hay lugares donde la herencia de implementación es apropiada, pero debe preguntarse cómo sería una solución con composición y si eso es realmente mejor. Por lo general lo es.

Tomando su ejemplo del Método de plantilla, debería considerar el Patrón de estrategia en su lugar. Resuelven problemas similares, pero a menudo este último dará más flexibilidad ya que usa composición en lugar de herencia.

La herencia es una práctica de diseño perfectamente válida.

Muchas personas que afirman que no les gusta la herencia tienen un hacha para moler, como si no les gustara la programación orientada a objetos, o aprendieron a programar en un lenguaje que usa algún otro paradigma y simplemente no pueden entenderlo.

Incluso me gusta la herencia para compartir la implementación, lo que algunas personas dicen que es una mala idea. Diré que el uso compartido de implementación o la reutilización funcionan mejor cuando tienes una idea bastante buena de cómo debe funcionar tu jerarquía de clases. Es decir, cuando pasas tiempo haciendo diseño. Si desea comenzar a escribir código y no tener tiempo para refactorizarlo, las jerarquías de sus clases estarán menos motivadas y será difícil compartirlas. La composición puede funcionar mejor si te gusta codificar de esta manera.

Mucha gente dice SÍ, pero yo digo NO. La herencia es, en mi opinión, una característica de programación increíble y la uso tanto como puedo. Es genial porque hace que tu código sea mucho más simple y puedes trabajar con un alto nivel de abstracción. Además, el compilador hace toda la magia detrás de las cortinas.

Sin embargo, debo decir que he visto muchos, muchos malos usos de la herencia y ESO es realmente peligroso. Supongo que la gente estaba tratando tan desesperadamente de usar la herencia que simplemente no intentaron realmente entenderla.

La herencia es simple de usar, pero no tan simple de usar bien , porque la herencia se trata de generalización y especialización de conceptos . Entonces, uno tiene que entender los conceptos que se están implementando y la relación entre ellos. A veces, la relación no es herencia, pero las personas la implementan como herencia. En este caso, espere algunos resultados catastróficos.

Pasé mucho tiempo entrenando personas y esto aparece una y otra vez.

Diré esto: los casos en que la herencia es realmente apropiada y útil son bastante raros. Aquí, específicamente, estoy hablando de herencia sin rasgos ni interfaces.

La realidad es que en este mundo los requisitos cambian rápidamente, y con frecuencia para los “más complicados” que los menos. Nuevos requisitos de seguridad, nuevos back-end para soporte, nuevos protocolos, reglas comerciales adicionales.

La herencia de clase única tiende a ser inflexible y, por lo tanto, quebradiza.

¡Has dado un ejemplo perfecto de dónde NO se debe usar la herencia!

El patrón de método de plantilla se implementa mejor usando lambdas. Un gran ejemplo es el patrón de fijación de préstamos.

Simplemente pasando la función del método de plantilla, puedo proporcionar una función de cualquier clase, no solo las que heredan de alguna clase.

Casi no he visto casos en los que no pueda reemplazar la herencia con un enfoque de combinación / interfaz / funcional más elegante, y termine con un sistema más flexible. Eso no significa que no use la herencia, pero solo la elijo como último recurso, después de trabajar con otras alternativas.

Hay algunos casos en que la composición no es útil. Por ejemplo:

Solicitud de clase abstracta pública {

nulo protegido allRequestsMustDoThat () {
// código
}

comportamiento vacío abstracto ();
}

HTTPRequest de clase pública extiende Solicitud {

@Anular
comportamiento nulo () {
// comportamiento http
}
}

FTPRequest de clase pública extiende Solicitud {

@Anular
comportamiento nulo () {
// Comportamiento FTP
}
}

ejecución nula pública (Solicitud [] solicitudes) {
para (Solicitud r: solicitudes) {
r.allRequestsMustDoThat ();
}
}

Sin herencia, debe incluir tres servicios, HTTPRequestService, FTPRequestService y RequestService y, en el método de ejecución, verifique la instancia de una solicitud y llame al servicio adecuado con respecto al tipo.

Ahora digamos que tiene cien tipos de solicitud, usando composición, está sucio.

Usando la herencia, debe agregar nuevas clases de tipo de solicitud. Bastante suave

Tiendo a preferir la composición a la herencia, pero a veces la herencia es útil.

Ninguna herencia no es “mala”, simplemente está de moda decir que sí. Mi filosofía sobre las características del lenguaje es que si una característica está integrada en el diseño, es un juego justo usar esa característica, sin importar lo que el “gurú” del día tenga que decir. Nunca entendí realmente la tendencia de las personas que no estaban en el proceso de diseño de un lenguaje en particular pensando que pueden imponer su opinión a la comunidad de desarrolladores para que no utilicen funciones de lenguaje integradas. El código de trabajo es el código de trabajo. Si el código no funciona, bien, deséchelo. Simplemente no creo que sea una justificación lo suficientemente buena como para no usar una función porque lo viste mal.

En realidad, soy nuevo en esta tendencia “OOP es malo, la herencia es mala, la encapsulación es mala”, ya que nunca me he encontrado con una situación en mi desarrollo que me haya hecho pensar en eso. Sin embargo, he visto bibliotecas de clases mal diseñadas donde el problema no tenía nada que ver con el hecho de que se usaban OOP o herencia. De hecho, en esos casos, fueron envolturas encapsuladas las que me ayudaron en el proceso de refactorizar ese código incorrecto. Y sí, ¡se usó la herencia! ¡JADEAR!

Está bien si a uno no le gusta la programación orientada a objetos. Hay muchos otros lenguajes utilizables que no están orientados a objetos. Sin embargo, decirle a la gente que no use características inherentes al diseño del lenguaje basado en sus propios prejuicios es una tontería … en mi opinión.

El problema con la herencia, creo, es que desalienta la reutilización del código. Si un objeto hereda de diferentes objetos, probablemente todos residen en diferentes paquetes, sería difícil aislar y reutilizar ese objeto en otro sistema. Más desalentador es la noción de herencia múltiple, donde un objeto hereda de una amplia gama de objetos aparentemente diferentes, probablemente objetos no relacionados que no tienen relación semántica alguna, todo con el pretexto de adornar el objeto con múltiples propósitos. Most people have suggested that whenever it is required for an object to perform different semantically different functions, Object composition should be preferred.

In any case, the correct thing to do will be to re-model your design to use Composition or any other pattern aside inheritance. Really, you can do without inheritance.

No, inheritance is a perfectly good way to structure some types of class.

Inheritance makes a lot of sense for a lot of jobs, it’s just got a bit of a bad rep because it’s been misused sometimes, and mud sticks.

It’s really about balance, use class inheritance where it makes logical sense and won’t overcomplicate things, if it starts to feel like you’re going too far, then you probably are.

If it feels like classes are similar enough to look the same, but not share implementation, interfaces make more sense. If it feels like classes are similar enough to share some parts of the implementation, then inheritance can make sense.

IMHO, without inheritance OOP is pretty useless. Inheritance is about the only reason I’d ever use OOP. When I don’t need inheritance, I simply write procedural code. So, no I don’t think it’s bad practice at all, it’s the best feature of OOP.

Technically no, but realistically yes. I’m not saying it can’t be done well, it’s just that it’s so easy to abuse and so frequently abused.

Just use an interface. You get the same structural inheritance without the code inheritance.

I’ve seen some really terrible, terrible fucking code that was so unnecessarily complex and messy all because someone wanted to use inheritance and didn’t want to not use it because it made them feel fancy or something.

Seriously complete bullshit, and over something that would have taken like 15 lines in Node.. Whenever I think of the horrors of Java and inheritance, and I think of this one dude and his absurd stubbornness in a space he didn’t fucking know, writing some of the most absurdly and totally unnecessarily complicated code.

And then he was a real dick to me on top of it.

Por supuesto no. You use inheritance where appropriate, just as you would use composition where appropriate. OOP does not require you to use inheritance. Moreover, you use OOP where appropriate, just as you would use FP where appropriate. It all comes down to how well you do architectural design. Remember the adage: “Always choose the right tool for the job.”

Newer languages are going for the “traits” and “mixins”…inheritance isn’t bad practice, but there’s a preoccupation with classical inheritance in academia and for this reason, we see in the workplace that there are too many contrived cases of inheritance.

The thing is….classical inheritance isn’t the only way to reuse code. In Academia, they talk as if it’s the only way to get code reuse. But FP, and newer things like “traits” and “mixins” are offering a fresh solutions to the same problems classical inheritance tried to solve.