注册,解决,发布模式的使用

时间:2018-11-12 18:48:45

标签: c# design-patterns dependency-injection

我目前正在阅读Mark Seeman的书Dependency Injection in .NET。在本书中,他建议使用 Register,Resolve,Release 模式,并建议在您的应用程序代码中这些操作中的每个操作应仅出现一次。

我的情况如下:我正在创建一个使用专有通信协议与PLC(一种工业嵌入式计算机)进行通信的应用程序,PLC制造商为其提供了一个库。库的文档建议创建与PLC的连接并保持打开状态;然后使用计时器或while循环,应定期发送一个请求以读取PLC存储器的内容,该内容随时间而变化。

从PLC内存中读取的值应用于对数据库进行操作,而我打算使用该数据库使用实体框架。据我了解,最好的选择是在每次执行循环时都创建一个新的dbContext,以避免出现停顿缓存或并发问题(循环可能每隔几毫秒执行一次,而连接始终保持打开状态)。

我的第一个选择是在应用程序构造上调用Resolve来创建一个长期对象,该对象将与PLC通信对象一起注入并处理循环执行并使连接保持活动状态。然后,在每个循环执行的开始,我打算再次调用Resolve来创建一个短期对象,该对象将被注入新的dbContext并将在数据库上执行操作。但是,在阅读了那本书的建议后,我怀疑我是否走上了正确的道路。

我的第一个想法是在构造它时将一个委托传递给长寿命的对象,这将使它能够构建该短寿命对象的新实例(我相信这是工厂模式),从而消除了对持久对象的依赖。我的长寿对象的DI容器。但是,此构造仍违反上述模式。

在这种情况下,哪种方法是正确的处理依赖注入的方法?

我第一次没有DI的尝试:

class NaiveAttempt
{
    private PlcCommunicationObject plcCommunicationObject;
    private Timer repeatedExecutionTimer;

    public NaiveAttempt()
    {
        plcCommunicationObject = new PlcCommunicationObject("192.168.0.10");
        plcCommunicationObject.Connect();

        repeatedExecutionTimer = new Timer(100); //Read values from PLC every 100ms
        repeatedExecutionTimer.Elapsed += (_, __) =>
        {
            var memoryContents = plcCommunicationObject.ReadMemoryContents();
            using (var ctx = new DbContext())
            {
                // Operate upon database
                ctx.SaveChanges();
            }
        }
    }
}

第二次尝试使用可怜人的DI。

class OneLoopObject
{
    private PlcCommunicationObject plcCommunicationObject;
    private Func<DbContext> dbContextFactory;

    public OneLoopObject(PlcCommunicationObject plcCommunicationObject, DbContext dbContext
    {
        this.plcCommunicationObject = plcCommunicationObject;
        this.dbContext = dbContext;
    }

    public void Execute()
    {
        var memoryContents = plcCommunicationObject.ReadMemoryContents();
        // Operate upon database    
    }
}

class LongLivedObject
{
    private PlcCommunicationObject plcCommunicationObject;
    private Timer repeatedExecutionTimer;   
    private Func<OneLoopObject> oneLoopObjectFactory;

    public LongLivedObject(PlcCommunicationObject plcCommunicationObject, Func<PlcCommunicationObject, OneLoopObject> oneLoopObjectFactory)
    {
        this.plcCommunicationObject = plcCommunicationObject;
        this.dbContextFactory = dbContextFactory;
        this repeatedExecutionTimer = new Timer(100);
        this.repeatedExecutionTimer.Elapsed += (_, __) =>
        {
            var loopObject = oneLoopObjectFactory(plcCommunicationObject);
            loopObject.Execute();
        }
    }
}

static class Program
{
    static void Main()
    {
        Func<PlcCommunicationObject, OneLoopObject> oneLoopObjectFactory = plc => new OneLoopObject(plc, new DbContext());
        var myObject = LongLivedObject(new PlcCommunicationObject("192.168.1.1"),  oneLoopObjectFactory)

        Console.ReadLine();
    }
}

4 个答案:

答案 0 :(得分:1)

将工厂与DI结合是一种常见的解决方案。在程序中动态创建和处理对象绝对没有错(要想尽早解决需要的每一点内存,要困难得多,而且要加以限制)。

我在这里找到了马克·西曼(Mark Seeman)关于注册,解决,释放模式(RRR)的帖子:http://blog.ploeh.dk/2010/09/29/TheRegisterResolveReleasepattern/

他说...

  

名称源自温莎城堡的术语,在这里我们:

     

在容器中注册组件

     

解决根组件

     

从容器中释放组件

因此,RRR模式仅限于DI容器。实际上,您确实在应用程序中一次在容器中注册和发布了组件。对于没有通过DI注入的对象(即那些在程序的正常执行中动态创建的对象),这什么也没说。

我已经看到许多文章针对您在程序中与DI相关的两种不同类型的事物使用不同的术语。有服务对象,即那些通过DI注入到您的应用程序中的全局对象。然后是数据或值对象。这些由您的程序根据需要动态创建,并且通常限于某些本地范围。两者都是完全有效的。

答案 1 :(得分:1)

听起来您希望既能够解析容器中的对象,然后释放它们,而又无需直接引用容器。

您可以通过在工厂界面中同时使用CreateRelease方法来做到这一点。

public interface IFooFactory
{
   Foo Create();
   void Release(Foo created);
}

这允许您在IFooFactory的实现中隐藏对容器的引用。

您可以创建自己的工厂实现,但为方便起见,某些容器(例如温莎)将create the factory implementation for you

var container = new WindsorContainer();
container.AddFacility<TypedFactoryFacility>();
container.Register(Component.For<Foo>());
container.Register(
    Component.For<IFooFactory>()
        .AsFactory()
);

您可以注入工厂,调用Create以获得工厂创建的任何实例,完成后,将该实例传递给Release方法。

温莎按照惯例进行此操作。方法名称无关紧要。如果您调用接口的返回值的方法,它将尝试解决该问题。如果某个方法返回void并接受一个参数,则它将尝试从容器中释放该参数。

在幕后与您编写的内容大致相同:

public class WindsorFooFactory : IFooFactory
{
    private readonly IWindsorContainer _container;

    public WindsorFooFactory(IWindsorContainer container)
    {
        _container = container;
    }

    public Foo Create()
    {
        return _container.Resolve<Foo>();
    }

    public void Release(Foo created)
    {
        _container.Release(created);
    }
}

工厂实现“知道”容器,但这没关系。它的工作是创建对象。工厂的 interface 没有提到容器,因此依赖于接口的类不会耦合到容器。您可以创建不使用容器的工厂的完全不同的实现。如果不需要释放对象,则可以使用一个Release方法,该方法什么也不做。

因此,简而言之,工厂界面使您可以遵循模式的解析/发布部分,而无需直接依赖于容器。

这里的another example显示了您可以使用这些抽象工厂做更多的事情。

答案 2 :(得分:1)

Autofac uses Func<> as the factory pattern,因此您可以始终这样做:

public class Foo()
{
  private readonly Func<Bar> _barFactory;

  public Foo(Func<Bar> barFactory)
  {
    _barFactory = barFactory;
  }
}

为工厂添加工厂接口并不是大多数人应该做的事情,这是一项额外的工作,几乎没有回报。

然后,您只需要跟踪版本中的externally owned or DI owned实体(在C#中处理)即可。

答案 3 :(得分:1)

第一版说明(第3章,第82页):

  

以纯粹的形式,“注册解决发布”模式指出,您应仅在每个阶段调用单个方法,应用程序只应包含一个单个。 em>调用Resolve方法。

此描述源于您的应用程序仅包含一个根对象(通常在编写简单的控制台应用程序时)或一个根类型的逻辑组的想法,例如MVC控制器。例如,对于MVC控制器,您将拥有一个自定义的Controller Factory,该工厂由MVC框架提供,并带有要构建的控制器类型。在这种情况下,该工厂将仅在提供类型时调用Resolve

但是,在某些情况下,您的应用程序具有多组根类型。例如,一个Web应用程序可能包含API控制器,MVC控制器和View组件的混合。对于每个逻辑组,您可能会在应用程序中对Resolve进行一次调用,从而对Resolve进行多次调用(通常是因为每种根类型都有自己的工厂)。

还有其他有效的原因可以回调到容器中。例如,您可能希望推迟构建对象图的一部分,以解决Captive Dependencies的问题。这似乎是您的情况。拥有额外解决方案的另一个原因是,当您使用Mediator模式将消息分派到可以处理该消息的某个实现(或多个实现)时。在这种情况下,您的Mediator实现通常会包装容器并调用Resolve。介体的抽象可能会在您的域库中定义,而介体的实现及其容器知识应在组成根内部中定义。

因此,不应从字面上接受仅调用Resolve的建议。与让类本身回调到容器中以解决其依赖关系(即服务定位器反模式)相比,此处的实际目标是在一次调用中尽可能地构建单个对象图。

这本书的second edition的另一个重点是

  

即使依赖性不佳,即使通过DI容器查询依赖关系也将成为服务定位器。当应用程序代码(而不是基础结构代码)主动查询服务以提供所需的依赖关系时,则它已成为服务定位器。

     

封装在“合成根”中的DI容器不是服务定位器,而是基础结构组件。

(注意:此引文来自second edition;尽管第一版也包含此信息,但其表达方式可能有所不同)。

因此,RRR模式的目标是促进成分根内DI容器的封装,这就是为什么它坚持只调用Resolve的原因。

请注意,在编写second edition时,Mark和我想重写关于RRR模式的讨论。这样做的主要原因是我们发现文本令人困惑(如您的问题所示)。但是,我们最终用光了时间,所以我们决定简单地删除详细的讨论。我们认为最重要的观点已经提出。