¿Existe un patrón para inicializar objetos creados a través de un contenedor DI?

147

Estoy intentando que Unity gestione la creación de mis objetos y quiero tener algunos parámetros de inicialización que no se conocen hasta el tiempo de ejecución:

Por el momento, la única forma en que puedo pensar en hacerlo es tener un método Init en la interfaz.

interface IMyIntf {
  void Initialize(string runTimeParam);
  string RunTimeParam { get; }
}

Luego, para usarlo (en Unity), haría esto:

var IMyIntf = unityContainer.Resolve<IMyIntf>();
IMyIntf.Initialize("somevalue");

En este escenario, el runTimeParamparámetro se determina en tiempo de ejecución según la entrada del usuario. El caso trivial aquí simplemente devuelve el valor de runTimeParampero en realidad el parámetro será algo así como el nombre del archivo y el método de inicialización hará algo con el archivo.

Esto crea una serie de problemas, a saber, que el Initializemétodo está disponible en la interfaz y se puede llamar varias veces. Establecer una marca en la implementación y lanzar una excepción en llamadas repetidas Initializeparece torpe.

En el punto donde resuelvo mi interfaz, no quiero saber nada sobre la implementación de IMyIntf. Sin embargo, lo que sí quiero es saber que esta interfaz necesita ciertos parámetros de inicialización únicos. ¿Hay alguna manera de anotar de alguna manera (atributos) la interfaz con esta información y pasarlos al marco cuando se crea el objeto?

Editar: describió la interfaz un poco más.

Igor Zevaka
fuente
9
Te estás perdiendo el punto de usar un contenedor DI. Se supone que las dependencias deben resolverse por usted.
Pierreten
¿De dónde obtiene los parámetros necesarios? (archivo de configuración, db, ??)
Jaime
runTimeParames una dependencia que se determina en tiempo de ejecución en función de una entrada del usuario. ¿La alternativa para esto debería ser dividirlo en dos interfaces: una para la inicialización y otra para almacenar valores?
Igor Zevaka
dependencia en IoC, generalmente se refiere a la dependencia a otras clases u objetos de tipo de referencia que se pueden determinar en la fase de inicialización de IoC. Si su clase necesita solo algunos valores para funcionar, ahí es donde el método Initialize () en su clase se vuelve útil.
La luz
Quiero decir, imagine que hay 100 clases en su aplicación en las que se puede aplicar este enfoque; entonces tendrá que crear 100 clases de fábrica adicionales + 100 interfaces para sus clases y podría salirse con la suya si solo estuviera usando el método Initialize ().
The Light

Respuestas:

276

En cualquier lugar donde necesite un valor de tiempo de ejecución para construir una dependencia particular, Abstract Factory es la solución.

Tener los métodos Initialize en las interfaces huele a una abstracción permeable .

En su caso, diría que debe modelar la IMyIntfinterfaz según cómo necesite usarla , no cómo intenta crear implementaciones de la misma. Ese es un detalle de implementación.

Por lo tanto, la interfaz debería ser simplemente:

public interface IMyIntf
{
    string RunTimeParam { get; }
}

Ahora defina la Fábrica abstracta:

public interface IMyIntfFactory
{
    IMyIntf Create(string runTimeParam);
}

Ahora puede crear una implementación concreta IMyIntfFactoryque crea instancias concretas IMyIntfcomo esta:

public class MyIntf : IMyIntf
{
    private readonly string runTimeParam;

    public MyIntf(string runTimeParam)
    {
        if(runTimeParam == null)
        {
            throw new ArgumentNullException("runTimeParam");
        }

        this.runTimeParam = runTimeParam;
    }

    public string RunTimeParam
    {
        get { return this.runTimeParam; }
    }
}

Observe cómo esto nos permite proteger las invariantes de la clase mediante el uso de la readonlypalabra clave. No son necesarios métodos de inicialización malolientes.

Una IMyIntfFactoryimplementación puede ser tan simple como esto:

public class MyIntfFactory : IMyIntfFactory
{
    public IMyIntf Create(string runTimeParam)
    {
        return new MyIntf(runTimeParam);
    }
}

En todos sus consumidores donde necesita una IMyIntfinstancia, simplemente depende IMyIntfFactoryde ella solicitándola a través de la inyección de constructor .

Cualquier contenedor DI que valga la pena será capaz de conectar automáticamente una IMyIntfFactoryinstancia por usted si la registra correctamente.

Mark Seemann
fuente
13
El problema es que un método (como Initialize) es parte de su API, mientras que el constructor no lo es. blog.ploeh.dk/2011/02/28/InterfacesAreAccessModifiers.aspx
Mark Seemann
12
Además, un método Initialize indica Acoplamiento temporal: blog.ploeh.dk/2011/05/24/DesignSmellTemporalCoupling.aspx
Mark Seemann
2
@Darlene Es posible que pueda usar un Decorador vagamente inicializado, como se describe en la sección 8.3.6 de mi libro . También proporciono un ejemplo de algo similar en mi presentación Big Object Graphs Up Front .
Mark Seemann
2
@Mark Si la creación de la MyIntfimplementación por parte de la fábrica requiere más de runTimeParam(lea: otros servicios que uno desearía que se resolviera mediante un IoC), entonces todavía tiene que resolver esas dependencias en su fábrica. Me gusta la respuesta de @PhilSandler de pasar esas dependencias al constructor de la fábrica para resolver esto, ¿es esa tu opinión también?
Jeff
2
También cosas geniales, pero tu respuesta a esta otra pregunta realmente llegó a mi punto.
Jeff
15

Por lo general, cuando se encuentra con esta situación, debe volver a visitar su diseño y determinar si está mezclando sus objetos de estado / datos con sus servicios puros. En la mayoría de los casos (no en todos), querrá mantener estos dos tipos de objetos separados.

Si necesita un parámetro específico de contexto pasado en el constructor, una opción es crear una fábrica que resuelva sus dependencias de servicio a través del constructor, y tome su parámetro de tiempo de ejecución como un parámetro del método Create () (o Generate ( ), Build () o como quieras nombrar tus métodos de fábrica).

Tener un método initialize () definidores o están por lo general piensa que es un mal diseño, ya que es necesario "recordar" a llamarlos y asegurarse de que no se abren demasiado de estado de su aplicación (es decir, lo que es dejar a alguien de re -invocando initialize o el setter?).

Phil Sandler
fuente
5

También me he encontrado con esta situación varias veces en entornos en los que estoy creando dinámicamente objetos ViewModel basados ​​en objetos Model (descritos muy bien en esta otra publicación de Stackoverflow ).

Me gustó cómo la extensión Ninject que te permite crear dinámicamente fábricas basadas en interfaces:

Bind<IMyFactory>().ToFactory();

No pude encontrar ninguna funcionalidad similar directamente en Unity ; así que escribí mi propia extensión al IUnityContainer, que le permite registrar fábricas que crearán nuevos objetos basados ​​en datos de objetos existentes, esencialmente asignando desde una jerarquía de tipos a una jerarquía de tipos diferente: UnityMappingFactory @ GitHub

Con un objetivo de simplicidad y legibilidad, terminé con una extensión que le permite especificar directamente las asignaciones sin declarar clases o interfaces de fábrica individuales (un ahorro de tiempo real). Simplemente agregue las asignaciones justo donde registra las clases durante el proceso normal de arranque ...

//make sure to register the output...
container.RegisterType<IImageWidgetViewModel, ImageWidgetViewModel>();
container.RegisterType<ITextWidgetViewModel, TextWidgetViewModel>();

//define the mapping between different class hierarchies...
container.RegisterFactory<IWidget, IWidgetViewModel>()
.AddMap<IImageWidget, IImageWidgetViewModel>()
.AddMap<ITextWidget, ITextWidgetViewModel>();

Luego simplemente declara la interfaz de fábrica de mapeo en el constructor para CI y usa su método Create () ...

public ImageWidgetViewModel(IImageWidget widget, IAnotherDependency d) { }

public TextWidgetViewModel(ITextWidget widget) { }

public ContainerViewModel(object data, IFactory<IWidget, IWidgetViewModel> factory)
{
    IList<IWidgetViewModel> children = new List<IWidgetViewModel>();
    foreach (IWidget w in data.Widgets)
        children.Add(factory.Create(w));
}

Como beneficio adicional, cualquier dependencia adicional en el constructor de las clases asignadas también se resolverá durante la creación del objeto.

Obviamente, esto no resolverá todos los problemas, pero hasta ahora me ha servido bastante bien, así que pensé que debería compartirlo. Hay más documentación en el sitio del proyecto en GitHub.

jigamiller
fuente
1

No puedo responder con la terminología específica de Unity, pero parece que estás aprendiendo acerca de la inyección de dependencia. Si es así, le insto a que lea la guía del usuario breve, clara y llena de información para Ninject .

Esto lo guiará a través de las diversas opciones que tiene cuando usa DI, y cómo explicar los problemas específicos que enfrentará en el camino. En su caso, lo más probable es que desee utilizar el contenedor DI para crear instancias de sus objetos y hacer que ese objeto obtenga una referencia a cada una de sus dependencias a través del constructor.

El tutorial también detalla cómo anotar métodos, propiedades e incluso parámetros utilizando atributos para distinguirlos en tiempo de ejecución.

Incluso si no usa Ninject, el tutorial le dará los conceptos y la terminología de la funcionalidad que se adapte a su propósito, y debería poder asignar ese conocimiento a Unity u otros marcos de DI (o convencerlo de que pruebe Ninject) .

Antonio
fuente
Gracias por eso. En realidad estoy evaluando los marcos DI y NInject iba a ser mi próximo.
Igor Zevaka
@johann: proveedores? github.com/ninject/ninject/wiki/…
anthony
1

Creo que lo resolví y se siente bastante saludable, por lo que debe ser medio correcto :))

Me dividí IMyIntfen interfaces "getter" y "setter". Entonces:

interface IMyIntf {
  string RunTimeParam { get; }
}


interface IMyIntfSetter {
  void Initialize(string runTimeParam);
  IMyIntf MyIntf {get; }
}

Luego la implementación:

class MyIntfImpl : IMyIntf, IMyIntfSetter {
  string _runTimeParam;

  void Initialize(string runTimeParam) {
    _runTimeParam = runTimeParam;
  }

  string RunTimeParam { get; }

  IMyIntf MyIntf {get {return this;} }
}

//Unity configuration:
//Only the setter is mapped to the implementation.
container.RegisterType<IMyIntfSetter, MyIntfImpl>();
//To retrieve an instance of IMyIntf:
//1. create the setter
IMyIntfSetter setter = container.Resolve<IMyIntfSetter>();
//2. Init it
setter.Initialize("someparam");
//3. Use the IMyIntf accessor
IMyIntf intf = setter.MyIntf;

IMyIntfSetter.Initialize()todavía se puede llamar varias veces, pero usando bits del paradigma del Localizador de servicios podemos resumirlo bastante bien, de modo que IMyIntfSetteres casi una interfaz interna que es distinta IMyIntf.

Igor Zevaka
fuente
13
Esta no es una solución particularmente buena, ya que se basa en un método Initialize, que es una abstracción permeable. Por cierto, esto no se parece al Localizador de servicios, sino más bien a la inyección de interfaz. En cualquier caso, vea mi respuesta para una mejor solución.
Mark Seemann