¿Hay una alternativa a la inyección bastarda? (Inyección de AKA pobre a través del constructor predeterminado)

Con mucha frecuencia me siento tentado a usar “inyección bastarda” en algunos casos. Cuando tengo un constructor de dependency injections “adecuado”:

public class ThingMaker { ... public ThingMaker(IThingSource source){ _source = source; } 

Pero entonces, para las clases que pretendo como API públicas (clases que otros equipos de desarrollo consumirán), nunca puedo encontrar una mejor opción que escribir un constructor “bastardo” predeterminado con la dependencia necesaria más probable:

  public ThingMaker() : this(new DefaultThingSource()) {} ... } 

El inconveniente obvio aquí es que esto crea una dependencia estática en DefaultThingSource; idealmente, no existiría tal dependencia, y el consumidor siempre inyectaría cualquier IThingSource que quisiera. Sin embargo, esto es demasiado difícil de usar; los consumidores quieren actualizar un ThingMaker y ponerse a trabajar haciendo cosas, y luego meses más tarde inyectar algo más cuando surja la necesidad. Esto deja solo algunas opciones en mi opinión:

  1. Omita al constructor bastardo; obligar al consumidor de ThingMaker a entender IThingSource, comprender cómo ThingMaker interactúa con IThingSource, buscar o escribir una clase concreta y luego inyectar una instancia en su llamada de constructor.
  2. Omita al constructor bastardo y proporcione una clase, un contenedor u otra clase / método de arranque por separado; de alguna manera hacer que el consumidor entienda que no necesita escribir su propio IThingSource; obligue al consumidor de ThingMaker a encontrar y comprender la fábrica o el agente de arranque y usarlo.
  3. Mantenga el constructor bastardo, permitiendo al consumidor “actualizar” un objeto y ejecutarlo, y hacer frente a la dependencia estática opcional de DefaultThingSource.

Chico, # 3 seguro parece atractivo. ¿Hay otra mejor opción? # 1 o # 2 simplemente no parecen valer la pena.

Por lo que yo entiendo, esta pregunta se relaciona con cómo exponer una API débilmente acoplada con algunos valores predeterminados apropiados. En este caso, puede tener un buen valor predeterminado local , en cuyo caso la dependencia se puede considerar como opcional. Una forma de tratar con dependencias opcionales es usar Inyección de propiedad en lugar de Inyección de constructor ; de hecho, esta es una especie de escenario de póster para Inyección de propiedad.

Sin embargo, el peligro real de la inyección de bastard es cuando el valor predeterminado es un valor predeterminado en el extranjero , porque eso significaría que el constructor predeterminado arrastra un acoplamiento indeseable al ensamblado que implementa el valor predeterminado. Sin embargo, como entiendo esta pregunta, el incumplimiento previsto se originaría en la misma asamblea, en cuyo caso no veo ningún peligro particular.

En cualquier caso, también podría considerar una fachada como se describe en una de mis respuestas anteriores: Dependency Inject (DI) biblioteca “amigable”

Por cierto, la terminología utilizada aquí se basa en el lenguaje de patrones de mi libro .

Mi compensación es un giro en @BrokenGlass:

1) El único constructor es el constructor parametrizado

2) Use el método de fábrica para crear un ThingMaker y pasar esa fuente predeterminada.

 public class ThingMaker { public ThingMaker(IThingSource source){ _source = source; } public static ThingMaker CreateDefault() { return new ThingMaker(new DefaultThingSource()); } } 

Obviamente, esto no elimina su dependencia, pero me deja más en claro que este objeto tiene dependencias que la persona que llama puede profundizar si así lo desea. Puede hacer que el método de fábrica sea incluso más explícito si lo desea (CreateThingMakerWithDefaultThingSource) si eso ayuda a comprenderlo. Prefiero esto a anular el método de fábrica IThingSource ya que continúa favoreciendo la composición. También puede agregar un nuevo método de fábrica cuando DefaultThingSource está obsoleto y tiene una forma clara de encontrar todo el código utilizando DefaultThingSource y marcarlo para actualizarlo.

Cubriste las posibilidades en tu pregunta. Clase de fábrica en otro lugar por conveniencia o por alguna conveniencia dentro de la misma clase. La única otra opción poco atractiva estaría basada en la reflexión, ocultando aún más la dependencia.

Una alternativa es tener un método de fábrica CreateThingSource() en su clase ThingMaker que cree la dependencia para usted.

Para realizar pruebas o si necesita otro tipo de IThingSource , deberá crear una subclase de ThingMaker y reemplazar CreateThingSource() para devolver el tipo concreto que desee. Obviamente, este enfoque solo vale la pena si es necesario principalmente poder inyectar la dependencia para las pruebas, pero para la mayoría / todos los demás propósitos no necesitan otro IThingSource

Yo voto por el # 3. Harás que tu vida, y la vida de otros desarrolladores, sea más fácil.

Si tiene que tener una dependencia “predeterminada”, también conocida como Inyección de Dependencia de Pobre, entonces debe inicializar y “conectar” la dependencia en algún lugar.

Mantendré los dos constructores pero tengo una fábrica solo para la inicialización.

 public class ThingMaker { private IThingSource _source; public ThingMaker(IThingSource source) { _source = source; } public ThingMaker() : this(ThingFactory.Current.CreateThingSource()) { } } 

Ahora en la fábrica crea la instancia predeterminada y permite que el método se sobrescriba:

 public class ThingFactory { public virtual IThingSource CreateThingSource() { return new DefaultThingSource(); } } 

Actualizar:

Por qué usar dos constructores: dos constructores muestran claramente cómo se pretende usar la clase. Los estados de constructor sin parámetros: simplemente crea una instancia y la clase realizará todas sus responsabilidades. Ahora, el segundo constructor afirma que la clase depende de IThingSource y proporciona una forma de utilizar una implementación diferente a la predeterminada.

Por qué usar una fábrica: 1- Disciplina: Crear nuevas instancias no debe ser parte de las responsabilidades de esta clase, una clase de fábrica es más apropiada. 2- SECO: imagina que en la misma API otras clases también dependen de IThingSource y hacen lo mismo. Anule una vez que el método de fábrica que devuelve IThingSource y todas las clases de su API comiencen a usar automáticamente la nueva instancia.

No veo ningún problema en el acoplamiento de ThingMaker a una implementación predeterminada de IThingSource, siempre que esta implementación tenga sentido para la API como un todo y también proporcione formas de anular esta dependencia para fines de prueba y extensión.

No está contento con la impureza OO de esta dependencia, pero realmente no dice qué problema causa finalmente.

  • ¿ThingMaker utiliza DefaultThingSource de alguna manera que no se ajuste a IThingSource? No.
  • ¿Podría llegar un momento en el que te verías obligado a retirar el constructor sin parámetros? Dado que puede proporcionar una implementación predeterminada en este momento, es poco probable.

Creo que el mayor problema aquí es la elección del nombre, no si se debe usar la técnica.

Los ejemplos usualmente relacionados con este estilo de inyección son a menudo extremadamente simplistas: “en el constructor predeterminado para la clase B , llame a un constructor sobrecargado con la new A() y ¡esté en camino!”

La realidad es que las dependencias a menudo son extremadamente complejas de construir. Por ejemplo, ¿qué B si B necesita una dependencia que no sea de clase, como una conexión de base de datos o una configuración de aplicación? A continuación, vincula la clase B al espacio de nombres System.Configuration , aumentando su complejidad y acoplamiento mientras reduce su coherencia, todo para codificar detalles que podrían simplemente externalizarse omitiendo el constructor predeterminado.

Este estilo de inyección le comunica al lector que ha reconocido los beneficios del diseño desacoplado pero no está dispuesto a comprometerse con él. Todos sabemos que cuando alguien ve ese constructor por defecto jugoso, fácil y de baja fricción, lo llamará por riguroso que sea su progtwig a partir de ese momento. No pueden entender la estructura de su progtwig sin leer el código fuente de ese constructor predeterminado, que no es una opción cuando solo distribuye los ensamblajes. Puede documentar las convenciones del nombre de la cadena de conexión y la clave de configuración de la aplicación, pero en ese punto el código no se sostiene por sí mismo y le pone la responsabilidad al desarrollador para buscar el hechizo correcto.

Optimizando el código para que quienes lo escriben puedan pasar sin entender lo que están diciendo es una canción de sirena, un anti-patrón que finalmente lleva a perder más tiempo en desentrañar la magia que el tiempo ahorrado en el esfuerzo inicial. O desacopla o no; mantener un pie en cada patrón disminuye el enfoque de ambos.

Por lo que vale, todo el código estándar que he visto en Java lo hace así:

 public class ThingMaker { private IThingSource iThingSource; public ThingMaker() { iThingSource = createIThingSource(); } public virtual IThingSource createIThingSource() { return new DefaultThingSource(); } } 

Cualquiera que no quiera un objeto DefaultThingSource puede anular createIThingSource . (Si es posible, la llamada a createIThingSource sería en algún lugar que no sea el constructor.) C # no fomenta la anulación como lo hace Java, y podría no ser tan obvio como lo sería en Java que los usuarios pueden y quizás deberían proporcionar su propio IThingSource implementación. (Tampoco es obvio cómo proporcionarlo). Creo que el # 3 es el camino a seguir, pero pensé que mencionaría esto.

Solo una idea, tal vez un poco más elegante, pero lamentablemente no se deshace de la dependencia:

  • eliminar el “constructor bastardo”
  • en el constructor estándar, usted hace que el parámetro fuente predeterminado sea nulo
  • luego verifica si la fuente es nula y si este es el caso, se le asigna “nueva DefaultThingSource ()” de otra manera cualquier cosa que el consumidor inyecte

Tener una fábrica interna (interna a su biblioteca) que mapee DefaultThingSource a IThingSource, que se llama desde el constructor predeterminado.

Esto le permite “actualizar” la clase ThingMaker sin parámetros o conocimiento de IThingSource y sin una dependencia directa de DefaultThingSource.

Para API verdaderamente públicas, generalmente manejo esto usando un enfoque de dos partes:

  1. Cree un ayudante dentro de la API para permitir que un consumidor API registre implementaciones de interfaz “predeterminadas” desde la API con su contenedor IoC de elección.
  2. Si es deseable permitir que el consumidor API use la API sin su propio contenedor IoC, aloje un contenedor opcional dentro de la API que tenga las mismas implementaciones “predeterminadas”.

La parte realmente difícil aquí es decidir cuándo activar el contenedor n. ° 2, y el mejor enfoque de elección dependerá en gran medida de los consumidores de la API.

Apoyo la opción n. ° 1, con una extensión: hacer que DefaultThingSource una clase pública . Su redacción anterior implica que DefaultThingSource se DefaultThingSource a los consumidores públicos de la API, pero según entiendo su situación, no hay razón para no exponer el valor predeterminado. Además, puede documentar fácilmente el hecho de que, fuera de circunstancias especiales, un new DefaultThingSource() siempre se puede pasar al ThingMaker .