oop - tutorial - referencia de clase en c#
Herencia vs. AgregaciĆ³n (12)
Hay dos escuelas de pensamiento sobre cómo extender, mejorar y reutilizar mejor el código en un sistema orientado a objetos:
Herencia: amplíe la funcionalidad de una clase creando una subclase. Anule los miembros de la superclase en las subclases para proporcionar una nueva funcionalidad. Cree métodos abstractos / virtuales para forzar a las subclases a "rellenar espacios en blanco" cuando la superclase desea una interfaz en particular, pero es independiente de su implementación.
Agregación: cree nuevas funcionalidades tomando otras clases y combinándolas en una nueva clase. Adjunte una interfaz común a esta nueva clase para la interoperabilidad con otro código.
¿Cuáles son los beneficios, los costos y las consecuencias de cada uno? ¿Hay otras alternativas?
Veo que este debate aparece de manera regular, pero no creo que se haya preguntado sobre el desbordamiento de la pila aún (aunque hay algunas discusiones relacionadas). También hay una sorprendente falta de buenos resultados de Google para ello.
Ambos enfoques se utilizan para resolver diferentes problemas. No siempre necesita agregar más de dos o más clases cuando hereda de una clase.
A veces tienes que agregar una única clase porque esa clase está sellada o tiene miembros no virtuales que necesitas interceptar para que crees una capa de proxy que obviamente no es válida en términos de herencia, pero siempre que la clase que estás examinando tiene una interfaz a la que puedes suscribirte que puede funcionar bastante bien.
Aquí está mi argumento más común:
En cualquier sistema orientado a objetos, hay dos partes en cualquier clase:
Su interfaz : la "cara pública" del objeto. Este es el conjunto de capacidades que anuncia para el resto del mundo. En muchos idiomas, el conjunto está bien definido en una "clase". Por lo general, estas son las firmas de método del objeto, aunque varía un poco según el idioma.
Su implementación : el trabajo "detrás de escena" que hace el objeto para satisfacer su interfaz y proporcionar funcionalidad. Este es típicamente el código y los datos de los miembros del objeto.
Uno de los principios fundamentales de OOP es que la implementación está encapsulada (es decir, oculta) dentro de la clase; lo único que deberían ver los de afuera es la interfaz.
Cuando una subclase hereda de una subclase, normalmente hereda tanto la implementación como la interfaz. Esto, a su vez, significa que estás obligado a aceptar ambos como restricciones en tu clase.
Con la agregación, puede elegir la implementación o la interfaz, o ambas cosas, pero no está obligado a hacerlo. La funcionalidad de un objeto se deja al objeto mismo. Puede diferir a otros objetos como quiera, pero en última instancia es responsable de sí mismo. En mi experiencia, esto conduce a un sistema más flexible: uno que es más fácil de modificar.
Entonces, cada vez que desarrollo software orientado a objetos, casi siempre prefiero la agregación sobre la herencia.
Creo que no se trata de un debate entre los dos. Es solo que:
- Las relaciones is-a (herencia) ocurren con menos frecuencia que las relaciones has-a (composición).
- La herencia es más difícil de corregir, incluso cuando es apropiado usarla, por lo que se debe tomar la diligencia debida porque puede romper la encapsulación, alentar un acoplamiento estricto al exponer la implementación y demás.
Ambos tienen su lugar, pero la herencia es más arriesgada.
Aunque, por supuesto, no tendría sentido tener una clase Shape ''teniendo-un'' Point y un Square classes. Aquí la herencia se debe.
La gente tiende a pensar en la herencia primero cuando intenta diseñar algo extensible, eso es lo que está mal.
Cubriré la parte donde pueden aplicar. Aquí hay un ejemplo de ambos, en un escenario de juego. Supongamos que hay un juego que tiene diferentes tipos de soldados. Cada soldado puede tener una mochila que puede contener cosas diferentes.
¿Herencia aquí? Hay una boina marina, verde y un francotirador. Estos son tipos de soldados. Entonces, hay una clase base Soldado con Marine, Green Beret y Sniper como clases derivadas
Agregación aquí? La mochila puede contener granadas, pistolas (diferentes tipos), cuchillo, botiquín, etc. Un soldado puede equiparse con cualquiera de estos en cualquier punto dado en el tiempo, además de que también puede tener un chaleco antibalas que actúa como armadura cuando es atacado y su la lesión disminuye a un cierto porcentaje. La clase de soldado contiene un objeto de clase de chaleco antibalas y la clase de mochila que contiene referencias a estos artículos.
Di una respuesta a "Is a" vs "Has a": ¿cuál es mejor? .
Básicamente, estoy de acuerdo con otras personas: use la herencia solo si su clase derivada es realmente del tipo que está extendiendo, no solo porque contiene la misma información. Recuerde que la herencia significa que la subclase gana tanto los métodos como los datos.
¿Tiene sentido para su clase derivada tener todos los métodos de la superclase? ¿O simplemente te prometes que esos métodos deberían ignorarse en la clase derivada? ¿O te encuentras reemplazando a los métodos de la superclase, haciendo que no funcionen para que nadie los llame inadvertidamente? ¿O dar sugerencias a su herramienta de generación de docs API para omitir el método del documento?
Esas son fuertes pistas de que la agregación es la mejor opción en ese caso.
El favor ocurre cuando ambos candidatos califican. A y B son opciones y usted favorece a A. La razón es que la composición ofrece más posibilidades de extensión / flexibilidad que la generalización. Esta extensión / flexibilidad se refiere principalmente al tiempo de ejecución / flexibilidad dinámica.
El beneficio no es visible inmediatamente. Para ver el beneficio que necesita para esperar la próxima solicitud de cambio inesperado. Entonces, en la mayoría de los casos, aquellos que se adhieren a la generlalización fracasan cuando se los compara con aquellos que adoptaron la composición (excepto un caso obvio que se menciona más adelante). De ahí la regla. Desde el punto de vista del aprendizaje, si puede implementar una inyección de dependencia con éxito, entonces debe saber a cuál favorecer y cuándo. La regla lo ayuda a tomar una decisión también; si no está seguro, seleccione la composición.
Resumen: Composición: el acoplamiento se reduce con solo conectar algunas cosas más pequeñas a algo más grande, y el objeto más grande simplemente llama al objeto más pequeño hacia atrás. Generlización: desde el punto de vista de la API, definir que un método puede ser anulado es un compromiso más fuerte que definir que se pueda llamar a un método. (muy pocas ocasiones cuando gana Generalización). Y nunca olvides que con la composición estás usando la herencia, desde una interfaz en lugar de una gran clase
La diferencia generalmente se expresa como la diferencia entre "es un" y "tiene un". Herencia, la relación "es una" se resume muy bien en el Principio de Sustitución de Liskov . La agregación, la relación "tiene una", es solo eso: muestra que el objeto agregado tiene uno de los objetos agregados.
También existen distinciones adicionales: la herencia privada en C ++ indica una relación "se implementa en términos de", que también se puede modelar mediante la agregación de objetos miembros (no expuestos) también.
La pregunta normalmente se expresa como Composición vs. Herencia , y se ha preguntado aquí antes.
No es una cuestión de cuál es el mejor, sino de cuándo usar qué.
En los casos "normales", una simple pregunta es suficiente para saber si necesitamos herencia o agregación.
- Si La nueva clase es más o menos como la clase original. Usa la herencia La nueva clase ahora es una subclase de la clase original.
- Si la nueva clase debe tener la clase original. Use la agregación. La nueva clase ahora tiene la clase original como miembro.
Sin embargo, hay una gran área gris. Entonces necesitamos muchos otros trucos.
- Si hemos usado la herencia (o planeamos usarla), pero solo usamos parte de la interfaz, o estamos forzados a anular una gran cantidad de funcionalidades para mantener la correlación lógica. Entonces tenemos un gran olor desagradable que indica que tuvimos que usar la agregación.
- Si usamos la agregación (o planeamos usarla), descubrimos que necesitamos copiar casi toda la funcionalidad. Entonces tenemos un olor que apunta en la dirección de la herencia.
Para abreviar. Deberíamos usar la agregación si parte de la interfaz no se usa o debe modificarse para evitar una situación ilógica. Solo necesitamos usar herencia, si necesitamos casi toda la funcionalidad sin grandes cambios. Y cuando tenga dudas, use Agregación.
Otra posibilidad para el caso de que tengamos una clase que necesita una parte de la funcionalidad de la clase original es dividir la clase original en una clase raíz y una subclase. Y deje que la nueva clase herede de la clase raíz. Pero deberías cuidarte con esto, no para crear una separación ilógica.
Vamos a agregar un ejemplo. Tenemos una clase ''Perro'' con métodos: ''comer'', ''caminar'', ''ladrar'', ''jugar''.
class Dog
Eat;
Walk;
Bark;
Play;
end;
Ahora necesitamos una clase ''Cat'', que necesita ''Eat'', ''Walk'', ''Purr'' y ''Play''. Entonces primero intenta extenderlo desde un perro.
class Cat is Dog
Purr;
end;
Parece, está bien, pero espera. Este gato puede ladrar (los amantes de los gatos me matarán por eso). Y un gato ladrando viola los principios del universo. Entonces debemos anular el método de Bark para que no haga nada.
class Cat is Dog
Purr;
Bark = null;
end;
Ok, esto funciona, pero huele mal. Entonces intentemos una agregación:
class Cat
has Dog;
Eat = Dog.Eat;
Walk = Dog.Walk;
Play = Dog.Play;
Purr;
end;
Ok, esto es bueno. Este gato ya no ladra, ni siquiera en silencio. Pero todavía tiene un perro interno que quiere salir. Intentemos la solución número tres:
class Pet
Eat;
Walk;
Play;
end;
class Dog is Pet
Bark;
end;
class Cat is Pet
Purr;
end;
Esto es mucho más limpio. Sin perros internos. Y los gatos y los perros están en el mismo nivel. Incluso podemos presentar otras mascotas para ampliar el modelo. A menos que sea un pez, o algo que no camine. En ese caso, nuevamente tenemos que refactorizar. Pero eso es algo para otro momento.
Quería hacer un comentario sobre la pregunta original, pero muerde 300 caracteres [; <).
Creo que debemos ser cuidadosos En primer lugar, hay más sabores que los dos ejemplos bastante específicos hechos en la pregunta.
Además, sugiero que es valioso no confundir el objetivo con el instrumento. Uno quiere asegurarse de que la técnica o metodología elegida apoye el logro del objetivo primario, pero no creo que sea fuera de contexto que la discusión técnica sea la mejor es muy útil. Ayuda a conocer las trampas de los diferentes enfoques junto con sus claros puntos dulces.
Por ejemplo, ¿qué vas a lograr, qué tienes disponible para empezar y cuáles son las limitaciones?
¿Estás creando un marco de componentes, incluso uno de propósito especial? ¿Las interfaces son separables de las implementaciones en el sistema de programación o se logra mediante una práctica que utiliza un tipo diferente de tecnología? ¿Se puede separar la estructura de herencia de las interfaces (si existe) de la estructura de herencia de las clases que las implementan? ¿Es importante ocultar la estructura de clases de una implementación del código que se basa en las interfaces que ofrece la implementación? ¿Hay varias implementaciones que se pueden usar al mismo tiempo o la variación es más a lo largo del tiempo como consecuencia del mantenimiento y la mejora? Esto y más deben considerarse antes de fijarse en una herramienta o una metodología.
Finalmente, ¿es tan importante bloquear distinciones en la abstracción y cómo se piensa (como en is-a versus has-a) con las diferentes características de la tecnología OO? Tal vez sea así, si mantiene la estructura conceptual consistente y manejable para usted y los demás. Pero es sabio no ser esclavizado por eso y las contorsiones que podrías terminar haciendo. Tal vez lo mejor sea retroceder un nivel y no ser tan rígido (pero dejar una buena narración para que otros puedan decir qué pasa). [Busco lo que hace que una porción particular de un programa sea explicable, pero algunas veces me decantan por la elegancia cuando hay una ganancia mayor. No siempre la mejor idea.]
Soy un fanático de la interfaz, y me atraen los tipos de problemas y enfoques en los que el purismo de interfaz es apropiado, ya sea para crear un marco de trabajo Java o para organizar algunas implementaciones de COM. Eso no lo hace apropiado para todo, ni siquiera cerca de todo, aunque lo juro. (Tengo un par de proyectos que parecen proporcionar contra-ejemplos serios contra el purismo de la interfaz, por lo que será interesante ver cómo me las arreglo).
Veo una gran cantidad de respuestas "es-a vs. tiene-una; son conceptualmente diferentes" sobre esta y las preguntas relacionadas.
Lo único que he encontrado en mi experiencia es que intentar determinar si una relación es "is-a" o "has-a" está destinada a fallar. Incluso si puede hacer correctamente esa determinación para los objetos ahora, el cambio de requisitos significa que probablemente se equivocará en algún momento en el futuro.
Otra cosa que he encontrado es que es muy difícil convertir de herencia a agregación una vez que hay una gran cantidad de código escrito en torno a una jerarquía de herencia. Simplemente cambiar de una superclase a una interfaz significa cambiar casi todas las subclases del sistema.
Y, como mencioné en otra parte de este post, la agregación tiende a ser menos flexible que la herencia.
Entonces, tienes una tormenta perfecta de argumentos en contra de la herencia cada vez que tienes que elegir una u otra:
- Su elección probablemente será la incorrecta en algún momento
- Cambiar esa elección es difícil una vez que la hayas logrado.
- La herencia tiende a ser una opción peor ya que es más restrictiva.
Por lo tanto, tiendo a elegir la agregación, incluso cuando parece haber una relación fuerte.