Herencia / Composición
Última actualización
¿Te fue útil?
Última actualización
¿Te fue útil?
Como se indicó previamente en Go no existe el concepto de herencia de implementación típico de lenguajes orientados a objetos como Java, C++ y Smalltalk entre otros.
En Go existe otro concepto complementario que es la composición.
Tal como menciona Steve Francia "Existen varios enfoques diferentes para definir relaciones entre objetos. Si bien difieren bastante entre sí, todos comparten un propósito común como mecanismo para la reutilización de código":
Herencia
Herencia múltiple
Polimorfismo
Composición de objetos
La herencia se puede expresar de dos maneras: herencia de clases y herencia de interfaces. "La herencia de clases define la implementación de un objeto en términos de la implementación de otro objeto. En resumen, es un mecanismo para compartir código y representación. Por el contrario, la herencia de interfaces (o subtipado) describe cuándo se puede usar un objeto en el lugar de otro." No todos los lenguajes de programación implementan la herencia de la misma manera. En algunos lenguajes la herencia de clases y la de interfaces existen como un mismo mecanismo (Eiffel por ejemplo), mientras que en otros están separados (Java por ejemplo). Algunos solamente permiten heredar de un único objeto, esto se denomina herencia simple; mientras otros permiten heredar de varios objetos y a esto se lo denomina herencia múltiple. Asimismo los comportamientos y datos heredados pueden estar limitados al acceso con el que el objeto padre los definió, esto se denomina visibilidad.
Se expresa a la herencia como una relación es-un/a.
La composición es una manera de definir objetos dentro de otros objetos. De esta forma un objeto puede adquirir los comportamientos y datos de los otros objetos por los que está compuesto.
Esto en cierta medida es más similar al concepto de herencia múltiple que al de simple.
Se expresa a la composición como una relación tiene-un/a.
"¿Por qué no hay herencia?: La programación orientada a objetos, al menos en los lenguajes más conocidos, implica demasiada discusión sobre las relaciones entre tipos, relaciones que a menudo podrían derivarse automáticamente. Go toma un enfoque diferente. En lugar de requerir que el programador declare de antemano que dos tipos están relacionados, en Go un tipo satisface automáticamente cualquier interfaz que especifique un subconjunto de sus métodos. Además de reducir la administración (la palabra original es bookkeeping), este enfoque tiene ventajas reales. Los tipos pueden satisfacer muchas interfaces a la vez, sin las complejidades de la herencia múltiple tradicional. Las interfaces pueden ser muy livianas - una interfaz con uno o incluso cero métodos puede expresar un concepto útil. Las interfaces se pueden agregar tardíamente si aparece una nueva idea o para probarla - sin anotar los tipos originales. Debido a que no existen relaciones explícitas entre los tipos y las interfaces, no hay una jerarquía de tipos para administrar o discutir. Es posible utilizar estas ideas para construir algo análogo a los type-safe de las pipes de Unix. Por ejemplo, vea cómo fmt.Fprintf permite la impresión formateada de cualquier salida, no solo de un archivo, o cómo el paquete bufio puede estar completamente separado de la E/S, o cómo el paquete image genera archivos de imágenes comprimidas. Todas estas ideas se derivan de una única interfaz (io.Writer) que representa un único método (Write). Y esto solo está arañando la superficie. Las interfaces en Go tienen una profunda influencia sobre cómo se estructuran los programas. Toma un tiempo acostumbrarse, pero este estilo implícito de dependencia de tipos es una de las cosas más productivas sobre Go."
"Una vez asistí a una reunión del grupo de usuarios de Java, donde James Gosling (el inventor de Java) fue el orador principal. Durante la memorable sesión de preguntas y respuestas, alguien le preguntó: 'Si pudieras volver a hacer Java, ¿qué cambiarías?' 'Suprimiría las clases', respondió. Después de que la risa se calmó, explicó que el verdadero problema no eran las clases en sí, sino la herencia de implementación (la relación extends). La herencia de interfaz (la relación implements) es preferible. Debe evitar la herencia de implementación siempre que sea posible.".
Pros:
Reutilización de código: todo código definido en la clase Fruta puede ser reutilizado por cualquiera de sus subclases.
Polimorfismo: una variable de tipo Fruta puede contener una referencia a un objeto de tipo superclase o cualquiera de sus subclases.
Enlace dinámico: en tiempo de ejecución se decidirá que método invocar en función del tipo de clase del objeto.
Facilidad para agregar una nueva subclase: simplemente se debe agregar una subclase que heredé de la superclase.
Contras:
Equipaje adicional: se heredan todos los métodos y propiedades de la superclase aunque no se deseen todos ellos.
Alto Acoplamiento: La subclase y superclase están fuertemente conectadas. Un cambio en la superclase puede impactar negativamente en la subclase.
Débil Encapsulamiento: si cambia la firma de un método en la superclase, hasta que la subclase no lo modifique el código no funcionará/compilará.
Pros:
Bajo Acoplamiento: Cualquier cambio en la clase Fruta no afecta a la clase Manzana. Incluso si se agrega un método en la clase Fruta con la misma firma de uno de la clase Manzana no afecta a esta última.
Reutilización de Código: Se puede lograr de igual forma que con la herencia, aunque hay que llamar explícitamente al código que necesita ser reutilizado (Nota: esto último no es necesario en Go).
Encapsulamiento más fuerte: en el ejemplo, si se cambia la firma del método pelar() en la clase Fruta, no hay necesidad de cambiar nada en la clase Manzana.
Contras:
Es más difícil agregar una nueva clase: sintácticamente requiere de más código.
Costo de rendimiento: el método explícito de reenvío de llamadas tiene un costo de rendimiento en comparación con la invocación directa de la herencia.
Como veremos la composición en Go se logra embebiendo tipos de datos unos dentro de otros:
Como se puede observar la función DarDeComer() espera una variable de tipo Primate pero se le pasa una de tipo Gorila. Como el tipo Gorila se compone de un Antropoide que implementa el método Alimentar() implícitamente también es un Primate.
Como se detallará más adelante, en la implementación de cada patrón de diseño, la falta de herencia será suplida de dos formas diferentes y/o complementarias:
la composición cuando exista un comportamiento que deba ser compartido entre tipos de datos
mediante interfaces cuando se deba asegurar que una estructura es parte de una relación es-un y/o requiera implementar ciertos comportamientos.
Estrictamente hablando de programación orientada a objetos, la mayor dificultad encontrada es cuando una clase abstracta implementa un método concreto con comportamiento que llama a métodos abstractos también definidos en dicha clase abstracta que luego serán implementados en las clases hijas. Para emular este comportamiento la estrategia utilizada en esta publicación será pasar como un argumento del método concreto de la clase abstracta una referencia de una interface que exponga cuáles serán los métodos abstractos que serán implementados por las clases hijas que implementen esa interface.
Veamos un ejemplo para entender la estrategia:
Go permite la composición y la herencia de interfaz. El uso de interfaces (es-un) y de la composición (tiene-un) posibilitan la reutilización de código en Go y la adopción de técnicas y de patrones orientados a objetos.
Seguramente no haya una respuesta única. No obstante en el faq de la documentación oficial de Go responden a esta pregunta de la siguiente forma :
En la comunidad de práctica de desarrollo de software existen miles de afirmaciones poco fundadas del estilo 'esto es mejor que aquello'. Al incursionar en Go y leer sobre por qué no existe la herencia me encontraba con frases del estilo "la herencia es mala", "la herencia simple no alcanza pero la herencia múltiple es aún peor", etc. En más de una oportunidad encontré una referencia a un artículo de JavaWorld donde el autor cuenta la siguiente anécdota:
Veamos un pequeño ejemplo en Java - ya que permite ambas formas - con algunos pros y contras - basado en el siguiente artículo :
Puede verse el uso de esta estrategia en el patrón .
Atención: Esta publicación se encuentra abandonada. Puede acceder a la versión vigente en