Abstract factories when using dependency injection

2019-02-07 10:40发布

问题:

I'm wondering how to properly use abstract factories when using a DI framework and one of the parameters in that factory is a dependency that should be handled by the DI framework.

I am not sure whether to make my abstract factory omit the parameter completely then use my DI container to wire it up or whether I should pass the dependency to the object.

For example, I have a TcpServer and it uses a Session.Factory to create sockets. The Session object actually takes a Processor in its constructor. Should I pass the Processor to the TcpServer then have it pass it onto the Session.Factory or have my DI container do the wiring?

If I were to have the DI container do the wiring it would look like this:

class Session : ISession
{
    public delegate ISession Factory(string name);

    ...
    public Session(string name, Processor processor)
    {
      ...
    }
}

class TcpServer : ITcpServer
{
    private readonly Session.Factory _sessionFactory;

    public TcpServer(Session.Factory sessionFactory)
    {
        this._sessionFactory = socketFactory;
    }

    ...

    public void OnConnectionReceived()
    {
       ...
       var session= _sessionFactory(ip.LocalEndPoint());
       ...

    }
}

Then using a DI container like Ninject I'd be able to do this when configuring the container:

Bind<Session.Factory>().ToMethod(c =>
{
    var processor = Kernel.Get<Processor>();
    return (name) => new Session(name, processor);
}).InSingletonScope();

My main issue with this approach is that it assumes whoever creates the Session.Factory knows about the processor. In my case, since I am using a DI container, this is actually very convenient but it seems weird to have a factory have its own dependencies. I always imagined a factory not really ever having any members.

If I were to pass the dependency through

class Session : ISession
{
    public delegate ISession Factory(string name, Processor processor);

    ...
    public Session(string name, Processor processor)
    {
      ...
    }
}

class TcpServer : ITcpServer
{
    private readonly Session.Factory _sessionFactory;
    private readonly Processor _processor;

    public TcpServer(Session.Factory sessionFactory, Processor processor)
    {
        this._processor = processor;
    }

    ...

    public void OnConnectionReceived()
    {
       ...
       var session = _sessionFactory(ip.LocalEndPoint(), _processor);
       ...

    }
}     

I have two issues with the second approach:

  1. The TcpServer doesn't actually do anything with the Processor. It just passes it along. Seems like this is poor man's DI at work almost.
  2. In the real program behind this code, the Processor actually has a reference to the TcpServer. Therefore when using this approach, I get a circular reference. When I break it apart by using the first scenario then it's not an issue.

What do you think is the best approach? I am open to new ideas as well.

Thanks!

回答1:

Many containers support factories in one or another way and this is the way you should go.

E.g. Taking your example define a ISessionFactory interface like this

public interface ISessionFactory
{
    ISession CreateSession(string name);
}

For Ninject 2.3 see https://github.com/ninject/ninject.extensions.factory and let it be implemented by Ninject

Bind<ISessionFactory>().AsFactory();

For 2.2 do the implementation yourself

public class SessionFactory : ISessionFactory
{
    private IKernel kernel;
    public SessionFactory(IKernel kernel)
    {
        this.kernel = kernel;
    }

    public ISession CreateSession(string name)
    {
        return this.kernel.Get<ISession>(new ConstructorArgument("name", name));
    }
}


回答2:

The pattern I use for an abstract factory pattern is a little different from yours. I use something like setter injection on a generic singleton, but wrap the configurable delegate "property" in a more intuitive interface.

I would prefer not to have to register each implementation individually, so I would prefer to use some convention that can be tested at application start up. I'm not sure about the Ninject syntax for autoregistering custom conventions, but the logic would come down to scanning the relevant assemblies for reference types, T, that have static readonly fields of type AbstractFactory<T>, then calling Configure(Func<T>) on that static member using reflection.

An example of the generic abstract factory singleton and how it would be declared on a Session is below.

 public class Session { 
      public static readonly AbstractFactory<Session> Factory = AbstractFactory<Session>.GetInstance();

 }

 public sealed class AbstractFactory<T> 
     where T: class{

     static AbstractFactory(){
          Bolt = new object();
     }
     private static readonly object Bolt;
     private static AbstractFactory<T> Instance;
     public static AbstractFactory<T> GetInstance(){
          if(Instance == null){
              lock(Bolt){
                  if(Instance == null)
                      Instance = new AbstractFactory<T>();
              }
          }
          return Instance;
     }

     private AbstractFactory(){}

     private Func<T> m_FactoryMethod;

     public void Configure(Func<T> factoryMethod){
              m_FactoryMethod = factoryMethod;
     }

     public T Create() { 
              if(m_FactoryMethod == null) {
                       throw new NotImplementedException();
              }
              return m_FactoryMethod.Invoke();
     } 

 } 

Update

If you need to pass parameters into your factory method, then you can alter the class such as:

 public sealed class AbstractFactory<TDataContract,T> 
      where T: class{
     static AbstractFactory(){
          Bolt = new object();
     }
     private static readonly object Bolt;
     private static AbstractFactory<TDataContract,T> Instance;
     public static AbstractFactory<TDataContract,T> GetInstance(){
          if(Instance == null){
              lock(Bolt){
                  if(Instance == null)
                      Instance = new AbstractFactory<T>();
              }
          }
          return Instance;
     }

     private AbstractFactory(){}

     private Func<TDataContract,T> m_FactoryMethod;

     public void Configure(Func<TDataContract,T> factoryMethod){
              m_FactoryMethod = factoryMethod;
     }

     public T Create(TDataContract data) { 
              if(m_FactoryMethod == null) {
                       throw new NotImplementedException();
              }
              return m_FactoryMethod.Invoke(data);
     } 

 } 

Your SessionData, Session and TcpServer might look like

 public class SessionData{
      public DateTime Start { get; set; }
      public string IpAddress { get; set; }
 }

 public class Session { 
      public static readonly AbstractFactory<SessionData,Session> Factory = AbstractFactory<Session>.GetInstance();

      private readonly string _ip;
      private readonly DateTime _start;

      public Session(SessionData data) { 
           _ip = data.IpAddress;
           _start = DateTime.Now;

      }
      public event EventHandler<RequestReceivedEventEventArgs> RequestAdded;

 }

 public class RequestReceivedEventArgs:  EventArgs { 

     public SessionRequest Request { get; set; }
 }

public class TcpServer : ITcpServer
{
    private readonly Processor _processor;

    public TcpServer(Processor processor)
    {
        this._processor = processor;
    }

    public void OnConnectionReceived()
    {
       var sessionData = new SessionData { 
                                            IpAddress = ip.LocalEndPoint(),
                                            Start = DateTime.Now
                                          };
       var session = Session.Factory.Create(sessionData);

       //...do other stuff
    }

    public void ServeResponse(SessionRequest request){
            _processor.Process(request);
    }
}  

When configuring your DI container, you can set up the factory such as:

Session.Factory.Configure(sessionData => { 
       // instead of injecting the processor into the Session, configure events 
       // that allow the TcpServer to process the data.  
       // (After all, it is more logical for a servers to serve a request than 
       // it is for a Session to do the Processing.  Session's tend to store data
       // and state, not invoke processes
       session.RequestAdded += (sender,e) => { 
              Kernel.Get<ITcpServer>.ServeResponse(e.Request);
       };
});