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.
- ¿Cuáles son algunos consejos para un probador manual con poco interés en la codificación que ha estado atrapado en este trabajo durante los últimos 3 años?
- ¿Cómo es ser un desarrollador independiente de iPhone?
- ¿Cuáles son las ventajas y desventajas de usar Java sobre PHP en el desarrollo de aplicaciones web?
- ¿Cuál debería ser el siguiente paso en mi carrera de Ingeniería de software?
- Estoy muy interesado en las pruebas de software de investigación. ¿Cómo y en qué temas de las pruebas de software debo investigar?
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:
- Debes escribir un código de andamio adicional para implementar la delegación
- ¿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.