2009-05-01 6 views
15

hay una regla general de diseño orientado a objetos que se debe modelar is-a relaciones utilizando la herencia y tiene unas relaciones usando-contención/agregación y expedición/delegación. Esto se reduce aún más por la advertencia del GoF de que generalmente se debe favorecer la contención sobre la herencia, sugiriendo, tal vez, que si se puede defender fuertemente a uno de ellos en una situación particular, esa contención generalmente debe recibir el visto bueno debido al mantenimiento problemas que la herencia a veces puede causar.vs seco "prefieren contención sobre la herencia"

entiendo el razonamiento detrás de esta forma de pensar, y no necesariamente están de acuerdo con ella. Sin embargo, cuando veo una clase con decenas de métodos, cada uno reenviando a una variable de instancia, veo una forma de duplicación de código. La duplicación de código es, en mi opinión, el último olor del código. Reimplementar un enorme protocolo de métodos simplemente porque la relación entre dos clases no es estrictamente-a parece una exageración. Es un código adicional e innecesario agregado al sistema, código que ahora necesita ser probado y documentado como cualquier otra parte del sistema, código que probablemente no habría tenido que escribir si hubiera heredado.

¿Los costes de la adhesión a este principio de contención-sobre-herencia vez mayores que sus beneficios?

+0

¿Podría dar un ejemplo más concreto? Es demasiado abstracto para comentar sin asumir lo que quieres decir. –

+0

Por ejemplo, desea un comportamiento tipo HashTable en una clase, pero la clase no es lo suficientemente estricta como una HashTable para justificar heredar de ella bajo la regla is-a. Pero que contenga una HashTable daría lugar a la reimplementación de una gran parte de los métodos de HashTable, la mayoría de los cuales invocaría el mismo método en la variable de instancia que contiene la HashTable. –

+0

Mi propia opinión sobre los problemas de la herencia es que hay efectos secundarios involuntarios al heredar de las clases. Al heredar, se crean dependencias y eso afectará la mantenibilidad general del sistema. Obviamente, esto no es gran cosa si la jerarquía de herencia es poco profunda, pero si es muy profunda, puede causar problemas. – Min

Respuesta

19

El costo de prácticamente cualquier cosa puede superar sus beneficios. Tener reglas duras y rápidas, SIN EXCEPCIONES siempre te meterá en problemas con el desarrollo. En general, es una mala idea usar (si su idioma/tiempo de ejecución lo admite) reflexión para obtener acceso a variables que no están destinadas a ser visibles para su código. ¿Es esto una mala práctica? Como con todo,

it depends. 

¿Puede la composición a veces ser más flexible o más fácil de mantener que la herencia directa? Por supuesto. Es por eso que existe. De la misma manera, la herencia puede ser más flexible o más fácil de mantener que una arquitectura de composición pura. Luego tienes interfaces o (dependiendo del idioma) herencia múltiple.

Ninguno de estos conceptos es malo, y la gente debe ser consciente de que a veces nuestra propia falta de comprensión o resistencia al cambio puede hacernos crear reglas arbitrarias que definen algo como "malo" sin ninguna razón real para hacerlo asi que.

+0

No podría estar más de acuerdo. Si bien parece ser la tendencia natural de los ingenieros a querer encontrar el "único camino verdadero", la realidad es que todo depende de su contexto, simplemente no hay una respuesta correcta para todo. Y * que *, es la respuesta correcta para todo. – airportyh

+0

La verdadera realidad "sin bala de plata". Aunque sí estoy a favor del principio KISS (Keep it simple, stupid) siempre que sea posible. – RobS

+0

quisiera que más personas tuvieran su actitud: el fenómeno de "mala arbitrariedad" solo ha crecido desde que escribió esto ... –

1

Sí, lo que está viendo es una horrenda colisión de paradigmas de diseño de diferentes rincones del universo: la agregación/composición del GoF aprovechando la colisión con la "Ley de Demeter".

estoy en el registro como la creencia de que, en el contexto de la agregación y la utilización composición, the Law of Demeter is an anti-pattern.

Contrariamente a ello, yo creo que las construcciones como person->brain->performThought() son absolutamente correcto y apropiado.

1

Estoy de acuerdo con su análisis, y favorecería la herencia en esos casos. Me parece que esto es un poco lo mismo que implementar ciegamente accesorios estúpidos en un esfuerzo ingenuo para proporcionar encapsulación. Creo que la lección aquí es que simplemente no hay reglas universales que siempre se apliquen.

0

Esto podría no responder a su pregunta, pero hay algo que siempre me ha molestado acerca de Java y su Stack. Stack, en cuanto a mis conocimientos, debe ser una estructura de datos de contenedor muy simple (o probablemente la más simple) con tres operaciones públicas básicas: pop, push y peek. ¿Por qué querrías en una pila insertAt, removeAt, et al. Tipo de funcionalidad? (en Java, Stack hereda de Vector).

Uno puede decir, bueno, al menos no tiene que documentar esos métodos, pero ¿por qué tener métodos que no deberían estar allí desde el principio?

+0

Sí, esto es realmente del tema. Pero estoy de acuerdo en que la interfaz inflada de java.util.Stack es molesta. –

+1

Sin embargo, esta es una observación bastante interesante sobre algunos de los peligros de la herencia. Si su respuesta fue reformulada, esto sería bastante sobre el tema. La pila de Java podría haber contenido un Vector y solo tiene pop, push y peek. En otra nota, creo que tener estructuras de datos más flexibles en el BCL es algo bueno. – Min

-1

GoF ya es texto antiguo .. Depende del entorno de OO que mires (y aun así es obvio que puedes encontrarlo en OO-piezas poco amistosas como C-with-classes).

Para entornos de tiempo de ejecución, prácticamente no tiene elección, herencia única. Y cualquier solución intentaba eludir sus limitaciones es solo eso, no importa cuán sofisticado o 'genial' pueda parecer.

Nuevamente verá esto manifestado en todas partes, incluyendo C++ (el más capaz) donde interactúa con devoluciones de llamadas en C (que está lo suficientemente extendido como para hacer que cualquiera preste atención). Sin embargo, C++ le ofrece mezclas y diseños basados ​​en políticas con características de plantilla, por lo que ocasionalmente puede ayudar con la ingeniería rigurosa.

Mientras que la contención puede darle beneficios de la indirección, la herencia puede darle una composición de fácil acceso. Elija veneno ... puertos de agregación mejor, pero siempre se ve como una violación de DRY, mientras que la herencia puede conducir a una reutilización más fácil con diferentes dolores de cabeza de mantenimiento potenciales.

El problema real es que el lenguaje de la computadora o la herramienta de modelado debe darle una opción de lo que debería hacer independientemente de la elección y, como tal, hacerlo menos propenso a errores humanos; pero no mucha gente modela antes de dejar que las computadoras escriban programas para ellos + no hay buenas herramientas (Osla ciertamente no es una) o su entorno está empujando algo tan tonto como la reflexión, los IoC y lo que no ... que es muy popular y que en en sí mismo dice mucho.

Hubo una vez una pieza hecha por un antiguo técnico COM que fue llamado Delegador Universal por uno de los mejores reproductores Doom de los alrededores, pero no es el tipo de desarrollo que alguien adoptaría en estos días ... Requiere interfaces seguras (y no es un requisito realmente difícil para el caso general). La idea es simple, es anterior a todo para interrumpir el manejo. Solo enfoques similares le brindan lo mejor de ambos mundos y son algo evidentes en los juegos de secuencias de comandos como JavaScript y programación funcional (aunque mucho menos legible o con mejor rendimiento).

3

Tienes que elegir la solución adecuada para el problema en cuestión. A veces, la herencia es mejor que la contención, a veces no. Tienes que usar tu criterio, y cuando no puedes decidir qué camino tomar, escribe un pequeño código y observa qué tan malo es. Algunas veces, escribir un código puede ayudarte a tomar una decisión que, de lo contrario, no sería obvia.

Como siempre: la respuesta correcta depende de tantos factores que no se pueden establecer reglas estrictas.

+0

Entiendo eso, pero a menudo veo que la gente llega tan lejos como para sugerir que nunca se debe usar la herencia (implementación). Esto, para mí, suena loco. ¿Estas personas simplemente disfrutan de escribir el código repetitivo/andamio? –

+3

Algunas personas lo hacen. Puedo verlo un poco Es muy relajante en comparación con el trabajo real. – chaos

+1

He visto lo peor de ambas maneras. Una aplicación con la que trato tiene una clase principal que hereda de 20 (no, no estoy bromeando) la mayoría (pero no completamente) clases abstractas. El 80-90% de las funciones de esa clase solo llaman a otra función. ¡Gran cantidad de herencia Y muchas cosas repetitivas! Intento MUCHO trabajo no duro con esa aplicación ... –

0

Hasta cierto punto, esta es una cuestión de soporte de idiomas. Por ejemplo, en Ruby podríamos implementar una pila simple que utiliza una matriz interna de este modo:

class Stack 
    extend Forwardable 
    def_delegators :@internal_array, :<<, :push, :pop 
    def initialize() @internal_array = [] end 
end 

Todo lo que estamos haciendo aquí es declarar qué subconjunto de la funcionalidad de la otra clase que queremos usar. Realmente no hay mucha repetición. Heck, si en realidad quería volver a utilizar toda la funcionalidad de la otra clase, que incluso podría especificar que sin llegar a repetir nada:

class ArrayClone 
    extend Forwardable 
    def_delegators(:@internal_array, 
        *(Array.instance_methods - Object.instance_methods)) 
    def initialize() @internal_array = [] end 
end 

Obviamente (espero), que no es código general que iba a escribir, pero creo que es muestra que se puede hacer. En idiomas sin metaprogramación fácil, puede ser algo más difícil en general mantener SECO.

1

Aunque estoy de acuerdo con todos los que han dicho "depende" - y que también depende del idioma hasta cierto punto - me sorprende que nadie haya mencionado eso (en las famosas palabras de Allen Holub) "extends is evil ". Cuando leí por primera vez ese artículo, tengo que admitir que me desilusioné un poco, pero tiene razón: independientemente del idioma, , la relación es una relación más estrecha de acoplamiento. Hay. Las cadenas de herencia altas son un antipatrón distinto. Por lo tanto, aunque no es correcto decir que siempre debe evitar la herencia, se debe usar con moderación (para las clases, se recomienda la herencia de la interfaz). Mi tendencia a la orientación de objetos novatos era modelar todo como una cadena de herencia, y sí, reduce la duplicación de código, pero a un costo muy real de acoplamiento ajustado, lo que significa dolores de cabeza de mantenimiento inevitables en algún momento del camino.

Su artículo es mucho mejor en explicar por qué herencia es estrecho acoplamiento, pero la idea básica es que es -una requiere que cada clase hija (y el nieto, etc.) dependiendo de la aplicación de las clases de los antepasados. "Programación a la interfaz" es una estrategia bien conocida para reducir la complejidad y ayudar con el desarrollo ágil. Realmente no puede programar a la interfaz de una clase principal porque la instancia es esa clase.

Por otro lado, el uso de agregación/composición fuerza una buena encapsulación, lo que hace que el sistema sea mucho menos rígido. Agrupe ese código reutilizable en una clase de utilidad, vincúlelo con una relación has-a, y su clase de cliente ahora está consumiendo un servicio proporcionado según un contrato. Ahora puede refactorizar la clase de utilidad al contenido de su corazón; siempre y cuando se ajuste a la interfaz, su clase de cliente puede permanecer felizmente inconsciente del cambio, y (lo que es más importante) no debería tener que ser recompilado.

No propongo esto como una religión, solo una buena práctica. Están destinados a romperse cuando sea necesario, por supuesto, pero generalmente hay una buena razón por la cual "lo mejor" es ese término.

0

Podría considerar la implementación del código "decorador" en una clase base abstracta que (de forma predeterminada) reenvía todas las llamadas al objeto contenido. Luego, subclase el decorador abstracto y anule/agregue métodos según sea necesario.

abstract class AbstractFooDecorator implements Foo { 
    protected Foo foo; 

    public void bar() { 
     foo.bar(); 
    } 
} 

class MyFoo extends AbstractFooDecorator { 
    public void bar() { 
     super.bar(); 
     baz(); 
    } 
} 

Esto, al menos, elimina la repetición del código "reenvío", si tienes muchas clases de envolver un tipo específico.

En cuanto a si la guía es útil o no, supongo que se debe hacer hincapié en la palabra "preferir". Obviamente, habrá casos en los que tenga mucho sentido usar la herencia. He aquí un example of when inheritance should not have been used:

La clase Hashtable se mejoró en JDK 1.2 para incluir un nuevo método, entrySet, que apoya la eliminación de las entradas de la tabla hash. La clase Provider no se actualizó para anular este nuevo método. Esta supervisión permitió a un atacante eludir la comprobación de SecurityManager aplicada en Provider.remove y eliminar las asignaciones de Proveedor simplemente invocando el método Hashtable.entrySet.

El ejemplo pone de manifiesto que la prueba sigue siendo necesaria para las clases en una relación de herencia, en contra de la implicación de que uno sólo tiene que mantener/test "encapsular" código de estilo - el costo de mantenimiento de una clase que hereda de otro podría no ser tan barato como parece a primera vista.

+0

Lástima que no hay forma de que vb.net o C# declaren que una clase es decoradora. Al menos con las interfaces, el lenguaje podría permitir que tal cosa se aplicara incluso genéricamente sin ambigüedad, siempre que hubiera un medio para especificar que un parámetro de tipo era una interfaz. – supercat