如何从容器中提取瞬态物体?我是否必须在容器中注册它们并注入需要类的构造函数?将所有内容注入构造函数中感觉不太好。同样只针对一个类,我不想创建TypedFactory
并将工厂注入需要的类。
我想到的另一个想法是根据需要“新”起来。但我也在我的所有类中注入一个Logger
组件(通过属性)。因此,如果我新建它们,我将不得不手动实例化这些类中的Logger
。如何继续为我的所有课程使用容器?
Logger注入: 我的大多数类都定义了Logger
属性,除非存在继承链(在这种情况下只有基类具有此属性)属性,以及所有派生类使用的)。当这些通过Windsor容器实例化时,它们会将ILogger
的实现注入其中。
//Install QueueMonitor as Singleton
Container.Register(Component.For<QueueMonitor>().LifestyleSingleton());
//Install DataProcessor as Trnsient
Container.Register(Component.For<DataProcessor>().LifestyleTransient());
Container.Register(Component.For<Data>().LifestyleScoped());
public class QueueMonitor
{
private dataProcessor;
public ILogger Logger { get; set; }
public void OnDataReceived(Data data)
{
//pull the dataProcessor from factory
dataProcessor.ProcessData(data);
}
}
public class DataProcessor
{
public ILogger Logger { get; set; }
public Record[] ProcessData(Data data)
{
//Data can have multiple Records
//Loop through the data and create new set of Records
//Is this the correct way to create new records?
//How do I use container here and avoid "new"
Record record = new Record(/*using the data */);
...
//return a list of Records
}
}
public class Record
{
public ILogger Logger { get; set; }
private _recordNumber;
private _recordOwner;
public string GetDescription()
{
Logger.LogDebug("log something");
// return the custom description
}
}
问题:
如何在不使用“新”的情况下创建新的Record
对象?
QueueMonitor
为Singleton
,而Data
为“Scoped”。如何将Data
注入OnDataReceived()
方法?
答案 0 :(得分:245)
从您提供的示例中很难非常具体,但一般来说,当您将ILogger
个实例注入大多数服务时,您应该问自己两件事:
<强> 1。我是否记录太多
当你有很多像这样的代码时,你的记录太多了:
try
{
// some operations here.
}
catch (Exception ex)
{
this.logger.Log(ex);
throw;
}
编写这样的代码来自丢失错误信息的担忧。然而,复制这些类型的try-catch块并不起作用。更糟糕的是,我经常看到开发人员记录并继续(他们删除了最后的throw
语句)。这非常糟糕(并且闻起来像旧的VB ON ERROR RESUME NEXT
行为),因为在大多数情况下,您根本没有足够的信息来确定它是否安全继续。通常,代码中存在错误或外部资源(如数据库)中的打嗝导致操作失败。继续意味着用户经常会认为操作成功,而它没有。问问自己:更糟糕的是,向用户显示一条错误消息,说明出现了问题并要求他再次尝试,或者默默地跳过错误并让用户认为他的请求已成功处理?考虑一下,如果他在两周后发现他的订单从未发货,用户将会感觉如何。你可能会失去一个客户。或者更糟糕的是,患者的MRSA注册无声地失败,导致患者不被护理隔离并导致其他患者的污染,导致高成本或甚至死亡。
应该删除大多数这类try-catch-log行,你应该简单地让异常冒泡到调用堆栈。
你不能记录吗?你绝对应该!但是,如果可以,请在应用程序的顶部定义一个try-catch块。使用ASP.NET,您可以实现Application_Error
事件,注册HttpModule
或定义执行日志记录的自定义错误页面。使用WinForms,解决方案是不同的,但概念保持不变:定义一个单一的最顶级。
但是,有时候,您仍然希望捕获并记录某种类型的异常。我过去使用的系统让业务层抛出ValidationExceptions,它将被表示层捕获。这些例外包含显示给用户的验证信息。由于这些异常会在表示层中被捕获和处理,因此它们不会冒泡到应用程序的最顶层部分,并且不会最终出现在应用程序的全部代码中。我仍然想记录这些信息,只是为了找出用户输入无效信息的频率,并找出是否因正确的原因触发了验证。所以这不是错误记录;只是记录。我编写了以下代码来执行此操作:
try
{
// some operations here.
}
catch (ValidationException ex)
{
this.logger.Log(ex);
throw;
}
看起来很熟悉?是的,看起来与前面的代码片段完全相同,区别在于我只捕获了ValidationException
个例外。但是,仅通过查看此片段就无法看到另一个差异。应用程序中只有一个地方包含该代码!这是一个装饰师,它让我想到你应该问自己的下一个问题:
<强> 2。我是否违反了SOLID原则?
记录,审核和安全等内容称为cross-cutting concerns(或方面)。它们被称为 cross cutting ,因为它们可以跨越应用程序的许多部分,并且通常必须应用于系统中的许多类。但是,当您发现您正在编写代码以供系统中的许多类使用时,您很可能违反了SOLID原则。以下面的例子为例:
public void MoveCustomer(int customerId, Address newAddress)
{
var watch = Stopwatch.StartNew();
// Real operation
this.logger.Log("MoveCustomer executed in " +
watch.ElapsedMiliseconds + " ms.");
}
您可以在此处测量执行MoveCustomer
操作所需的时间并记录该信息。系统中的其他操作很可能需要同样的跨领域问题。您开始为ShipOrder
,CancelOrder
,CancelShipping
和其他用例添加此类代码,这会导致大量代码重复并最终导致维护噩梦(I&#39;曾经去过那里。)
此代码存在的问题是它违反了SOLID原则。 SOLID原则是一组面向对象的设计原则,可帮助您定义灵活且可维护(面向对象)的软件。 MoveCustomer
示例违反了其中至少两项规则:
MoveCustomer
方法的类不仅包含核心业务逻辑,还会测量执行操作所需的时间。换句话说,它有多个职责。MoveCustomer
用例添加异常处理(第三项责任),您(再次)必须更改MoveCustomer
方法。但是你不仅需要改变MoveCustomer
方法,还要改变许多其他方法,这使得这个方法彻底改变。 OCP与DRY原则密切相关。此问题的解决方案是将日志记录提取到自己的类中,并允许该类包装原始类:
// The real thing
public class MoveCustomerService : IMoveCustomerService
{
public virtual void MoveCustomer(int customerId, Address newAddress)
{
// Real operation
}
}
// The decorator
public class MeasuringMoveCustomerDecorator : IMoveCustomerService
{
private readonly IMoveCustomerService decorated;
private readonly ILogger logger;
public MeasuringMoveCustomerDecorator(
IMoveCustomerService decorated, ILogger logger)
{
this.decorated = decorated;
this.logger = logger;
}
public void MoveCustomer(int customerId, Address newAddress)
{
var watch = Stopwatch.StartNew();
this.decorated.MoveCustomer(customerId, newAddress);
this.logger.Log("MoveCustomer executed in " +
watch.ElapsedMiliseconds + " ms.");
}
}
通过将装饰器包裹在真实实例周围,您现在可以将此测量行为添加到类中,而不需要更改系统的任何其他部分:
IMoveCustomerService command =
new MeasuringMoveCustomerDecorator(
new MoveCustomerService(),
new DatabaseLogger());
然而,之前的示例只解决了部分问题(仅限SRP部分)。在编写如上所示的代码时,您必须为系统中的所有操作定义单独的装饰器,并且您最终会得到装饰器,如
MeasuringShipOrderDecorator
,MeasuringCancelOrderDecorator
和MeasuringCancelShippingDecorator
。这导致了许多重复代码(违反OCP原则),并且仍然需要为系统中的每个操作编写代码。这里缺少的是系统中用例的常见抽象。
缺少的是ICommandHandler<TCommand>
界面。
让我们定义这个界面:
public interface ICommandHandler<TCommand>
{
void Execute(TCommand command);
}
让我们将MoveCustomer
方法的方法参数存储到名为MoveCustomerCommand
的自己的Parameter Object)类中:
public class MoveCustomerCommand
{
public int CustomerId { get; set; }
public Address NewAddress { get; set; }
}
让我们将MoveCustomer
方法的行为放在实现ICommandHandler<MoveCustomerCommand>
的类中:
public class MoveCustomerCommandHandler : ICommandHandler<MoveCustomerCommand>
{
public void Execute(MoveCustomerCommand command)
{
int customerId = command.CustomerId;
Address newAddress = command.NewAddress;
// Real operation
}
}
一开始可能看起来很奇怪,但是因为你现在有一个用例的通用抽象,你可以将装饰器重写为以下内容:
public class MeasuringCommandHandlerDecorator<TCommand>
: ICommandHandler<TCommand>
{
private ILogger logger;
private ICommandHandler<TCommand> decorated;
public MeasuringCommandHandlerDecorator(
ILogger logger,
ICommandHandler<TCommand> decorated)
{
this.decorated = decorated;
this.logger = logger;
}
public void Execute(TCommand command)
{
var watch = Stopwatch.StartNew();
this.decorated.Execute(command);
this.logger.Log(typeof(TCommand).Name + " executed in " +
watch.ElapsedMiliseconds + " ms.");
}
}
这个新的MeasuringCommandHandlerDecorator<T>
看起来很像MeasuringMoveCustomerDecorator
,但是这个类可以重复用于系统中的所有命令处理程序:
ICommandHandler<MoveCustomerCommand> handler1 =
new MeasuringCommandHandlerDecorator<MoveCustomerCommand>(
new MoveCustomerCommandHandler(),
new DatabaseLogger());
ICommandHandler<ShipOrderCommand> handler2 =
new MeasuringCommandHandlerDecorator<ShipOrderCommand>(
new ShipOrderCommandHandler(),
new DatabaseLogger());
通过这种方式,可以更加轻松地向系统添加横切关注点。在Composition Root中创建一个方便的方法很容易,它可以用系统中适用的命令处理程序包装任何创建的命令处理程序。例如:
private static ICommandHandler<T> Decorate<T>(ICommandHandler<T> decoratee)
{
return
new MeasuringCommandHandlerDecorator<T>(
new DatabaseLogger(),
new ValidationCommandHandlerDecorator<T>(
new ValidationProvider(),
new AuthorizationCommandHandlerDecorator<T>(
new AuthorizationChecker(
new AspNetUserProvider()),
new TransactionCommandHandlerDecorator<T>(
decoratee))));
}
此方法可按如下方式使用:
ICommandHandler<MoveCustomerCommand> handler1 =
Decorate(new MoveCustomerCommandHandler());
ICommandHandler<ShipOrderCommand> handler2 =
Decorate(new ShipOrderCommandHandler());
但是,如果您的应用程序开始增长,使用DI容器引导它会很有用,因为DI容器可以使用自动注册。这可以防止您必须为添加到系统的每个新命令/处理程序对更改组合根。特别是当您的装饰器具有泛型类型约束时,DI容器非常有用。
现代的大多数现代DI容器对装饰器都有相当不错的支持,尤其是Autofac(example)和Simple Injector(example)可以很容易地注册开放式通用装饰器。 Simple Injector甚至允许根据给定的谓词或复杂的泛型类型约束有条件地应用装饰器,允许装饰的类为injected as a factory并允许将contextual context注入装饰器,所有这些都可以非常有用。
另一方面,Unity和Castle具有动态拦截功能(正如Autofac对btw所做的那样)。动态拦截与装饰有很多共同点,但它使用动态代理生成。这可能比使用通用装饰器更灵活,但是在可维护性方面你需要付出代价,因为你经常会失去类型安全性,拦截器总是强迫你依赖拦截库,而装饰器是类型安全的,可以是在不依赖外部库的情况下编写。如果您想详细了解这种设计应用程序的方式,请阅读本文:Meanwhile... on the command side of my architecture。
更新:我还合着了一本名为Dependency Injection Principles, Practices, and Patterns的书,详细介绍了这种SOLID编程风格和上述设计(见第10章)。