tipos simple relaciones programacion orientada objetos herencia entre ejercicios ejemplos composicion codigo clases agregacion java oop language-agnostic inheritance composition

java - simple - relaciones entre clases programacion orientada a objetos



¿Por qué usar herencia en absoluto? (13)

Sé que la pregunta ha sido discutida antes , pero parece siempre bajo el supuesto de que la herencia es, al menos, a veces preferible a la composición. Me gustaría desafiar esa suposición con la esperanza de obtener un poco de comprensión.

Mi pregunta es esta: ya que puedes lograr cualquier cosa con la composición de objetos que puedas con herencia clásica y dado que la herencia clásica se abusa muy a menudo [1] y dado que la composición del objeto te da flexibilidad para cambiar el tiempo de ejecución del objeto delegado, ¿por qué usarías alguna vez? herencia clásica?

Puedo entender por qué recomendaría la herencia en algunos lenguajes como Java y C ++ que no ofrecen una sintaxis conveniente para la delegación. En estos idiomas, puede guardar mucha escritura usando herencia siempre que no sea claramente incorrecto hacerlo. Pero otros lenguajes como Objective C y Ruby ofrecen herencia clásica y sintaxis muy conveniente para la delegación. El lenguaje de programación Go es el único idioma que, a mi conocimiento, ha decidido que la herencia clásica es más problemática de lo que vale y solo admite la delegación para la reutilización de código.

Otra forma de expresar mi pregunta es la siguiente: incluso si usted sabe que la herencia clásica no es incorrecta para implementar un determinado modelo, ¿es suficiente razón para usarla en lugar de la composición?

[1] Muchas personas usan la herencia clásica para lograr el polimorfismo en lugar de dejar que sus clases implementen una interfaz. El propósito de la herencia es la reutilización del código, no el polimorfismo. Además, algunas personas usan la herencia para modelar su comprensión intuitiva de una relación "es-a" que a menudo puede ser problemática .

Actualizar

Solo quiero aclarar lo que quiero decir exactamente cuando hablo de herencia:

Estoy hablando del tipo de herencia por el cual una clase hereda de una clase base implementada parcial o totalmente . No estoy hablando de heredar de una clase base puramente abstracta que equivale a la implementación de una interfaz, que para el registro no estoy discutiendo.

Actualización 2

Entiendo que la herencia es la única forma de lograr el polimorfismo i C ++. En ese caso, es obvio por qué debes usarlo. Por lo tanto, mi pregunta se limita a lenguajes como Java o Ruby que ofrecen distintas formas de lograr el polimorfismo (interfaces y tipado de pato, respectivamente).


El propósito de la herencia es la reutilización del código, no el polimorfismo.

Este es tu error fundamental. Casi exactamente lo opuesto es verdad. El propósito principal de la herencia (pública) es modelar las relaciones entre las clases en cuestión. El polimorfismo es una gran parte de eso.

Cuando se usa correctamente, la herencia no se trata de reutilizar el código existente. Más bien, se trata de ser utilizado por el código existente. Es decir, si tiene un código existente que puede funcionar con la clase base existente, cuando obtiene una nueva clase de esa clase base existente, ese otro código también puede funcionar automáticamente con su nueva clase derivada también.

Es posible utilizar la herencia para la reutilización del código, pero cuando lo haga, normalmente debería ser una herencia privada y no una herencia pública. Si el idioma que está utilizando respalda la delegación, es muy probable que rara vez tenga muchas razones para usar la herencia privada. OTOH, la herencia privada admite algunas cosas que la delegación (normalmente) no admite. En particular, aunque el polimorfismo es una preocupación decididamente secundaria en este caso, aún puede ser una preocupación, es decir, con la herencia privada puede comenzar desde una clase base que es casi lo que desea, y (suponiendo que lo permita) anular la partes que no son del todo correctas.

Con la delegación, su única opción real es utilizar la clase existente tal como está. Si no hace exactamente lo que quiere, su única opción real es ignorar por completo esa funcionalidad y volver a implementarla desde cero. En algunos casos, eso no es una pérdida, pero en otros es bastante importante. Si otras partes de la clase base usan la función polimórfica, la herencia privada le permite anular solo la función polimórfica, y las otras partes usarán su función anulada. Con la delegación, no puede conectar fácilmente su nueva funcionalidad para que otras partes de la clase base existente utilicen lo que ha anulado.


¿Qué pasa con el patrón de método de la plantilla? Supongamos que tiene una clase base con toneladas de puntos para políticas personalizables, pero un patrón de estrategia no tiene sentido por al menos uno de los siguientes motivos:

  1. Las políticas personalizables deben conocer la clase base, solo se pueden usar con la clase base y no tienen sentido en ningún otro contexto. En cambio, usar la estrategia es posible, pero un PITA porque tanto la clase base como la clase de política deben tener referencias entre sí.

  2. Las políticas están acopladas entre sí en el sentido de que no tendría sentido mezclarlas y combinarlas libremente. Solo tienen sentido en un subconjunto muy limitado de todas las combinaciones posibles.


Algo que no es totalmente OOP, sin embargo, la composición generalmente significa una falta de memoria caché adicional. Depende, pero tener los datos más cerca es un plus.

En general, me niego a participar en algunas peleas religiosas, usando tu propio juicio y estilo es lo mejor que puedes obtener.


Cuando preguntaste:

Incluso si sabe que la herencia clásica no es incorrecta para implementar un cierto modelo, ¿es suficiente razón para usarla en lugar de la composición?

La respuesta es no. Si el modelo es incorrecto (usando herencia), entonces es incorrecto usarlo sin importar qué.

Aquí hay algunos problemas con la herencia que he visto:

  1. Siempre teniendo que probar el tipo de tiempo de ejecución de punteros de clase derivados para ver si se pueden lanzar (o también hacia abajo).
  2. Esta ''prueba'' se puede lograr de varias maneras. Puede tener algún tipo de método virtual que devuelva un identificador de clase. O en su defecto, es posible que tenga que implementar RTTI (identificación del tipo de tiempo de ejecución) (al menos en c / c ++) que puede darle un golpe de rendimiento.
  3. los tipos de clase que no se pueden "lanzar" pueden ser potencialmente problemáticos.
  4. Hay muchas maneras de lanzar su tipo de clase hacia arriba y hacia abajo en el árbol de herencia.

La herencia es preferible si:

  1. Necesita exponer toda la API de la clase que amplía (con delegación, necesitará escribir muchos métodos de delegación) y su lenguaje no ofrece una manera simple de decir "delegar todos los métodos desconocidos".
  2. Necesita acceder a campos / métodos protegidos para idiomas que no tienen concepto de "amigos"
  3. Las ventajas de la delegación se reducen un poco si tu lenguaje permite una herencia múltiple
  4. Por lo general, no necesita ninguna delegación si su lenguaje permite heredar dinámicamente de una clase o incluso una instancia en tiempo de ejecución. No lo necesita en absoluto si puede controlar qué métodos están expuestos (y cómo están expuestos) al mismo tiempo.

Mi conclusión: la delegación es una solución para un error en un lenguaje de programación.


La principal utilidad de la herencia clásica es si tiene varias clases relacionadas que tendrán una lógica idéntica para los métodos que operan en variables / propiedades de instancia.

En realidad, hay 3 formas de manejar eso:

  1. Herencia.
  2. Duplicar el código ( código de olor "Código duplicado").
  3. Mueva la lógica a otra clase más (el código huele a "Lazy Class", "Middle Man", "Message Chains" y / o "Inappropriate Intimity").

Ahora bien, puede haber mal uso de la herencia. Por ejemplo, Java tiene las clases InputStream y OutputStream . Las subclases de estos se utilizan para leer / escribir archivos, sockets, arrays, cadenas y varios se utilizan para ajustar otras secuencias de entrada / salida. Según lo que hacen, deberían ser interfaces en lugar de clases.


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


Las interfaces solo definen qué puede hacer un objeto y no cómo. Entonces, en términos simples, las interfaces son solo contratos. Todos los objetos que implementan la interfaz deberán definir su propia implementación del contrato. En el mundo práctico, esto te da separation of concern . Imagínese a usted mismo escribiendo una aplicación que necesita tratar con varios objetos que no conoce de antemano, sin embargo, necesita lidiar con ellos, lo único que sabe es qué cosas diferentes se supone que deben hacer esos objetos. Entonces, definirá una interfaz y mencionará todas las operaciones en el contrato. Ahora escribirás tu aplicación contra esa interfaz. Más tarde, quien quiera aprovechar su código o aplicación tendrá que implementar la interfaz en el objeto para que funcione con su sistema. Su interfaz obligará a su objeto a definir cómo se supone que debe hacerse cada operación definida en el contrato. De esta forma, cualquier persona puede escribir objetos que implementen su interfaz, para que se adapten perfectamente a su sistema y todo lo que sabe es lo que debe hacerse y es el objeto el que necesita definir cómo se hace.

En el desarrollo del mundo real, esta práctica se conoce generalmente como Programming to Interface and not to Implementation .

Las interfaces son solo contratos o firmas y no saben nada sobre implementaciones.

Codificando contra medios de interfaz, el código del cliente siempre contiene un objeto de interfaz que es suministrado por una fábrica. Cualquier instancia devuelta por la fábrica sería de tipo Interfaz que cualquier clase de candidato de fábrica debe haber implementado. De esta forma, el programa del cliente no se preocupa por la implementación y la firma de la interfaz determina qué operaciones se pueden realizar. Esto se puede usar para cambiar el comportamiento de un programa en tiempo de ejecución. También te ayuda a escribir programas mucho mejores desde el punto de vista del mantenimiento.

Aquí hay un ejemplo básico para ti.

public enum Language { English, German, Spanish } public class SpeakerFactory { public static ISpeaker CreateSpeaker(Language language) { switch (language) { case Language.English: return new EnglishSpeaker(); case Language.German: return new GermanSpeaker(); case Language.Spanish: return new SpanishSpeaker(); default: throw new ApplicationException("No speaker can speak such language"); } } } [STAThread] static void Main() { //This is your client code. ISpeaker speaker = SpeakerFactory.CreateSpeaker(Language.English); speaker.Speak(); Console.ReadLine(); } public interface ISpeaker { void Speak(); } public class EnglishSpeaker : ISpeaker { public EnglishSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak English."); } #endregion } public class GermanSpeaker : ISpeaker { public GermanSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak German."); } #endregion } public class SpanishSpeaker : ISpeaker { public SpanishSpeaker() { } #region ISpeaker Members public void Speak() { Console.WriteLine("I speak Spanish."); } #endregion }

texto alternativo http://ruchitsurati.net/myfiles/interface.png


Siempre lo pienso dos veces antes de usar herencia, ya que puede ser complicado rápidamente. Dicho esto, hay muchos casos en los que simplemente produce el código más elegante.


Todos saben que el polimorfismo es una gran ventaja de la herencia. Otro beneficio que encuentro en herencia es que ayuda a crear una réplica del mundo real. Por ejemplo, en el sistema de nómina, tratamos a los gerentes, a los desarrolladores, a los muchachos de la oficina, etc. si heredamos todas estas clases con el empleado de la clase superior. hace que nuestro programa sea más comprensible en el contexto del mundo real que todas estas clases son básicamente empleados. Y una clase más de cosas no solo contiene métodos sino que también contienen atributos. Entonces, si incluimos atributos genéricos para el empleado en la clase Empleado como la edad del número de seguro social, etc., proporcionaría una mayor reutilización del código y claridad conceptual y, por supuesto, polimorfismo. Sin embargo, al usar cosas de herencia, debemos tener en cuenta el principio básico del diseño: "Identifique los aspectos de su aplicación que varían y sepárelos de los aspectos que cambian". Nunca debe implementar esos aspectos de la aplicación que cambian por herencia en lugar de usar la composición. Y para aquellos aspectos que no son modificables, debe usar la herencia del curso si se encuentra una relación evidente "es una".


Tu escribiste:

[1] Muchas personas usan la herencia clásica para lograr el polimorfismo en lugar de dejar que sus clases implementen una interfaz. El propósito de la herencia es la reutilización del código, no el polimorfismo. Además, algunas personas usan la herencia para modelar su comprensión intuitiva de una relación "es-a" que a menudo puede ser problemática.

En la mayoría de los idiomas, la línea entre ''implementar una interfaz'' y ''derivar una clase de otra'' es muy delgada. De hecho, en idiomas como C ++, si deriva una clase B de una clase A, y A es una clase que consiste únicamente en métodos virtuales puros, está implementando una interfaz.

La herencia se trata de la reutilización de la interfaz , no de la reutilización de la implementación . No se trata de la reutilización del código, como escribió anteriormente.

La herencia, como usted señala correctamente, pretende modelar una relación IS-A (el hecho de que muchas personas lo malinterpreten no tiene nada que ver con la herencia per se). También puede decir ''BEHAVES-LIKE-A''. Sin embargo, el hecho de que algo tenga una relación IS-A con otra cosa no significa que use el mismo código (o incluso similar) para cumplir esta relación.

Compare este ejemplo de C ++ que implementa diferentes formas de datos de salida; dos clases usan la herencia (pública) para que puedan acceder de forma polimórfica:

struct Output { virtual bool readyToWrite() const = 0; virtual void write(const char *data, size_t len) = 0; }; struct NetworkOutput : public Output { NetworkOutput(const char *host, unsigned short port); bool readyToWrite(); void write(const char *data, size_t len); }; struct FileOutput : public Output { FileOutput(const char *fileName); bool readyToWrite(); void write(const char *data, size_t len); };

Ahora imagina si esto fuera Java. ''Salida'' no era una estructura, sino una ''interfaz''. Podría llamarse ''Writeable''. En lugar de ''Salida pública'' dirías ''implementa Escribir''. ¿Cuál es la diferencia en lo que se refiere al diseño?

Ninguna.


Una de las formas más útiles que veo para usar la herencia es en los objetos de GUI.


Si delega todo lo que no ha reemplazado explícitamente a algún otro objeto que implementa la misma interfaz (el objeto "base"), entonces básicamente tiene herencia Greenspunned sobre la composición, pero (en la mayoría de los idiomas) con mucha más verborrea y repetitivo. El propósito de usar composición en lugar de herencia es para que solo pueda delegar los comportamientos que desea delegar.

Si desea que el objeto use todo el comportamiento de la clase base a menos que se anule explícitamente, entonces la herencia es la forma más simple, menos detallada y más directa de expresarlo.