我目前正在阅读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();
}
}
答案 0 :(得分:1)
将工厂与DI结合是一种常见的解决方案。在程序中动态创建和处理对象绝对没有错(要想尽早解决需要的每一点内存,要困难得多,而且要加以限制)。
我在这里找到了马克·西曼(Mark Seeman)关于注册,解决,释放模式(RRR)的帖子:http://blog.ploeh.dk/2010/09/29/TheRegisterResolveReleasepattern/
他说...
名称源自温莎城堡的术语,在这里我们:
在容器中注册组件
解决根组件
从容器中释放组件
因此,RRR模式仅限于DI容器。实际上,您确实在应用程序中一次在容器中注册和发布了组件。对于没有通过DI注入的对象(即那些在程序的正常执行中动态创建的对象),这什么也没说。
我已经看到许多文章针对您在程序中与DI相关的两种不同类型的事物使用不同的术语。有服务对象,即那些通过DI注入到您的应用程序中的全局对象。然后是数据或值对象。这些由您的程序根据需要动态创建,并且通常限于某些本地范围。两者都是完全有效的。
答案 1 :(得分:1)
听起来您希望既能够解析容器中的对象,然后释放它们,而又无需直接引用容器。
您可以通过在工厂界面中同时使用Create
和Release
方法来做到这一点。
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模式的讨论。这样做的主要原因是我们发现文本令人困惑(如您的问题所示)。但是,我们最终用光了时间,所以我们决定简单地删除详细的讨论。我们认为最重要的观点已经提出。