¿Podemos reemplazar completamente la herencia usando el patrón de estrategia y la inyección de dependencia?

10

Por ejemplo:

var duckBehaviors = new Duckbehavior();
duckBehaviors.quackBehavior = new Quack();
duckBehaviors.flyBehavior = new FlyWithWings();
Duck mallardDuck = new Duck(DuckTypes.MallardDuck, duckBehaviors)

Como la clase Duck contiene todos los comportamientos (abstractos), no parece necesario crear una nueva clase MallardDuck(que se extienda Duck).

Referencia: Head First Design Pattern, Capítulo 1.

Deepak Mishra
fuente
¿Cuáles son los tipos Duckbehavior.quackBehaviory otros campos en su código?
max630
No hay inyección de dependencia en su ejemplo.
David Conrad
11
La herencia es increíble cuando eres un desarrollador de nivel junior a medio porque hay una larga historia de refactorización de trucos y patrones de diseño a su alrededor. Pero los desarrolladores más experimentados con los que he hablado prefieren jerarquías de herencia realmente poco profundas basadas en la composición siempre que sea posible. La herencia puede causar un acoplamiento más apretado de lo necesario y puede dificultar los cambios.
Mark Rogers
55
@DavidConrad: El OP no está haciendo la actualización dentro de la clase . DI / IoC se trata de inyectar dependencias, no de contenedores o de nunca usar "nuevo"; Esa es una preocupación completamente ortogonal. Además, siempre debe cambiar el código en algún lugar, ya sea ​​la raíz de composición o algún archivo de configuración. El control se invierte aquí dentro del tipo Duck, ya que lo que controla la creación de las dependencias no es el Duck ctor, sino algún contexto externo; este es un ejemplo de juguete dado por claridad, está perfectamente bien que el contexto externo esté representado solo por el código de llamada.
Filip Milovanović

Respuestas:

21

Claro, pero llamamos a eso composición y delegación . El patrón de estrategia y la inyección de dependencia pueden parecer estructuralmente similares, pero sus intenciones son diferentes.

El patrón de estrategia permite la modificación del comportamiento en tiempo de ejecución bajo la misma interfaz. Podría decirle a un pato silvestre que vuele y verlo volar con alas. Luego cámbielo por un pato piloto y véalo volar con Delta Airlines. Hacer eso mientras el programa se está ejecutando es una cuestión de Patrón de estrategia.

La inyección de dependencias es una técnica para evitar dependencias de codificación dura para que puedan cambiar de forma independiente sin requerir que los clientes sean modificados cuando cambian. Los clientes simplemente expresan sus necesidades sin saber cómo se satisfarán. Por lo tanto, cómo se cumplen se decide en otro lugar (generalmente en main). No necesitas dos patos para hacer uso de esta técnica. Simplemente algo que usa un pato sin saber o importar qué pato. Algo que no construye el pato o lo busca, pero está feliz de usar cualquier pato que le des.

Si tengo una clase de pato concreta, puedo hacer que implemente su comportamiento de vuelo. Incluso podría hacer que cambie los comportamientos de volar con alas a volar con Delta en función de una variable de estado. Esa variable podría ser un booleano, un int, o podría tener FlyBehaviorun flymétodo que haga cualquier estilo de vuelo sin que tenga que probarlo con un if. Ahora puedo cambiar los estilos de vuelo sin cambiar los tipos de pato. Ahora los ánades reales pueden convertirse en pilotos. Esto es composición y delegación . El pato está compuesto por un FlyBehavior y puede delegarle solicitudes de vuelo. Puede reemplazar todos sus comportamientos de pato de una vez de esta manera o mantener algo para cada comportamiento o cualquier combinación intermedia.

Esto le da a todos los mismos poderes que tiene la herencia, excepto uno. La herencia le permite expresar qué métodos de Duck está anulando en los subtipos de Duck. La composición y la delegación requieren que Duck delegue explícitamente los subtipos desde el principio. Esto es mucho más flexible, pero implica más tipeo del teclado y Duck tiene que saber que está sucediendo.

Sin embargo, muchas personas creen que la herencia debe diseñarse explícitamente desde el principio. Y que si no lo ha sido, debe marcar sus clases como selladas / finales para no permitir la herencia. Si toma esa opinión, la herencia realmente no tiene ventaja sobre la composición y la delegación. Porque entonces, de cualquier manera, debe diseñar la extensibilidad desde el principio o estar dispuesto a derribar las cosas más adelante.

Derribar cosas es en realidad una opción popular. Solo tenga en cuenta que hay casos en los que es un problema. Si ha implementado de forma independiente bibliotecas o módulos de código que no tiene la intención de actualizar con la próxima versión, puede terminar atrapado con versiones de clases que no saben nada sobre lo que está haciendo ahora.

Si bien estar dispuesto a derribar las cosas más tarde puede liberarlo de un diseño excesivo, hay algo muy poderoso en poder diseñar algo que use un pato sin tener que saber qué hará realmente el pato cuando se use. Que no saber es algo poderoso. Te permite dejar de pensar en patos por un tiempo y pensar en el resto de tu código.

"Podemos" y "deberíamos" son preguntas diferentes. Favorecer la composición sobre la herencia no dice nunca usar la herencia. Todavía hay casos en los que la herencia tiene más sentido. Te mostraré mi ejemplo favorito :

public class LoginFailure : System.ApplicationException {}

La herencia le permite crear excepciones con nombres descriptivos más específicos en una sola línea.

Intenta hacerlo con la composición y obtendrás un desastre. Además, no hay riesgo del problema de herencia yo-yo porque no hay datos o métodos aquí para reutilizar y fomentar el encadenamiento de herencia. Todo esto agrega es un buen nombre. Nunca subestimes el valor de un buen nombre.

naranja confitada
fuente
1
" Nunca subestimes el valor de un buen nombre ". Tiempo de ropa nueva del emperador: LoginExceptionno es un buen nombre. Es el clásico "nombre de pitufo" y, si lo quito Exception, todo lo que tengo es Loginque no me dice nada de lo que salió mal.
David Arno
Lamentablemente, estamos atascados con la ridícula convención de poner Exceptionfin a cada clase de excepción, pero por favor no confunda "atascado" con "bueno".
David Arno
@DavidArno Si puedes sugerir un nombre mejor, soy todo oídos. Aquí tenemos el beneficio de no quedar atrapados en las convenciones de una base de código existente. Si tuvieras el poder de cambiar el mundo con un chasquido de dedos, ¿cómo lo llamarías?
candied_orange
Yo los nombro, StackOverflow, OutOfMemory, NullReferenceAccess, LoginFailureetc Básicamente, toman "excepción" en el nombre. Y, si es necesario, corríjalos para que describa lo que salió mal.
David Arno
@DavidArno por su comando.
candied_orange
7

Puede reemplazar casi cualquier metodología con cualquier otra metodología y aún así producir software de trabajo. Sin embargo, algunos se ajustan mejor a un problema particular que otros.

Depende de muchas cosas cuál es preferible. Técnica anterior en la aplicación, experiencia en el equipo, desarrollos futuros esperados, preferencia personal y cuán difícil será para un recién llegado entenderlo, por nombrar algunos.

A medida que adquieras más experiencia y luches con más frecuencia con las creaciones de otras personas, es probable que pongas más énfasis en la última contemplación.

La herencia sigue siendo una herramienta de modelado válida y sólida que no siempre es necesariamente la más flexible, pero ofrece una guía sólida a las personas nuevas que pueden estar agradecidas por la asignación clara al dominio del problema.

Martin Maat
fuente
-1

La herencia no es tan importante como se pensaba. Todavía es importante , y eliminarlo sería un grave error.

Un ejemplo extremo es Objective-C, donde todo es una subclase de NSObject (el compilador en realidad no le permite declarar una clase que no tiene una clase base, por lo que todo lo que no está integrado en el compilador debe ser una subclase de algo integrado en el compilador). Hay muchas cosas útiles integradas en NSObject que no debería perderse.

Otro ejemplo interesante es UIView, la clase básica de "vista" en UIKit para el desarrollo de iOS. Esta es una clase que, por un lado, es un poco como una clase abstracta que declara la funcionalidad que las subclases deben completar, pero también es útil por sí sola. Tiene subclases suministradas por UIKit, que a menudo se usan tal cual. Existe una composición del desarrollador que instala subvistas en una vista. Y a menudo hay subclases definidas por el desarrollador, que a menudo usan composición. No hay una sola regla estricta o incluso reglas, usted usa lo que mejor se adapte a sus necesidades.

gnasher729
fuente
Su "caso extremo" es más o menos la forma más modernos lenguajes orientados a objetos función: subclases de Python type, todos los no-primitivo en hereda de Java Object, JavaScript todo en un prototipo ha terminando con Object, etc
Delioth
-1

[Arriesgaré una respuesta descarada a la pregunta titular.]

¿Podemos reemplazar completamente la herencia usando el patrón de estrategia y la inyección de dependencia?

Sí ... excepto que el patrón de estrategia en sí usa herencia. El patrón de estrategia funciona en la herencia de la interfaz. Reemplazar la herencia con composición + estrategia mueve la herencia a un lugar diferente. No obstante, a menudo vale la pena realizar dicho reemplazo, ya que permite separar las jerarquías .

Nick Alexeev
fuente
2
" El patrón de estrategia funciona en la herencia de la interfaz ". Recuerde, el patrón de estrategia es un patrón de diseño; No es un patrón de implementación. Por lo tanto, se puede implementar usando interfaces, pero también se puede implementar usando, por ejemplo, un hash / diccionario de funciones.
David Arno
3
La herencia de la interfaz no es en absoluto una herencia, es solo un tipo de contrato o clasificador. Puede usar delegados también para el patrón de estrategia (prefiero usarlos).
Deepak Mishra
Más uno, amigo: ... trabaja en la herencia de la interfaz , felicitaciones por el uso adecuado del término general "interfaz". Creo que el diseño de clase pobre o incompetente es la razón básica por la que se plantea esta pregunta. Para explicar por qué / cómo tomaría un libro; El diablo está en los detalles. He trabajado con diseños de herencia que fueron una alegría para la vista y al mismo tiempo una herencia profunda que fue un infierno. He visto un interfacediseño craptastic arreglado con herencia. El problema oculto aquí es la implementación inconsistente que transforma el comportamiento dependiente. PD. herencia e interfaz no son mutuamente excluyentes,
radarbob
Comentario de @DavidArno : este comentario estaría bien si se adjunta a la pregunta de OP. La pregunta de OP es técnicamente errónea, pero aún así la entendemos. El problema no es esta respuesta particular.
radarbob
@DeepakMishra comentario: . Una "interfaz" es todos los miembros públicos de una clase. Lamentablemente, el significado de "interfaz" está sobrecargado. Debemos tener cuidado de distinguir el significado general con la palabra clave de un lenguaje de programacióninterface
radarbob
-2

No. Si un pato silvestre requiere parámetros diferentes para crear una instancia que otro tipo de pato, sería una pesadilla cambiarlo. ¿También deberías poder crear una instancia de un pato? Es más un pato real o un pato mandarín o cualquier otro tipo de pato que tengas.

También es posible que desee algo de lógica en torno a los tipos para que una jerarquía de tipos sea mejor.

Ahora, si la reutilización del código se vuelve problemática para algunas de las funciones, podríamos componerla pasando las funciones a través del constructor.

Nuevamente, realmente depende de su caso de uso. Si solo tiene un pato y un pato silvestre, una jerarquía de clases es una solución mucho más simple.

Pero aquí hay un ejemplo en el que le gustaría usar el patrón de estrategia: si tiene clases de clientes y desea pasar una estrategia de facturación (interfaz) que podría ser una estrategia de facturación de hora feliz o una estrategia de facturación regular, puede pasarla en el constructor De esta manera, no tiene que hacer dos clases diferentes de clientes y no necesita una jerarquía. Solo tienes una clase de cliente.

Benjamin Erichsen
fuente