Windsor - 从容器中拉出瞬态对象

Posted

技术标签:

【中文标题】Windsor - 从容器中拉出瞬态对象【英文标题】:Windsor - pulling Transient objects from the container 【发布时间】:2012-04-11 03:18:27 【问题描述】:

如何从容器中提取本质上是瞬态的对象?我是否必须将它们注册到容器并注入所需类的构造函数?将所有内容注入构造函数感觉并不好。也只是为了一个类,我不想创建一个TypedFactory 并将工厂注入到需要的类中。

我想到的另一个想法是根据需要“新建”它们。但我也在向我的所有类中注入一个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
    

问题:

    如何在不使用“new”的情况下创建新的Record 对象?

    QueueMonitorSingleton,而 Data 是“作用域”。如何将Data 注入OnDataReceived() 方法?

【问题讨论】:

@Steven 我添加了一个代码示例,显示了 Logger 的使用情况。你认为这是一个糟糕的设计吗? 你能用具体的代码,甚至更好的测试来说明你想要实现的目标吗? 抱歉,我的问题构建得不好。我添加了更多代码来解释我的情况。 【参考方案1】:

从您提供的示例中很难说得很具体,但总的来说,当您将ILogger 实例注入大多数服务时,您应该问自己两件事:

    我是否记录太多? 我是否违反了 SOLID 原则?

1.我是不是记录太多了

当你有很多这样的代码时,你记录的太多了:

try

   // some operations here.

catch (Exception ex)

    this.logger.Log(ex);
    throw;

编写这样的代码是出于对丢失错误信息的担忧。然而,在所有地方复制这些类型的 try-catch 块并没有帮助。更糟糕的是,我经常看到开发人员通过删除最后的 throw 语句来登录并继续:

try

   // some operations here.

catch (Exception ex)

    this.logger.Log(ex); // <!-- No more throw. Execution will continue.

在大多数情况下这是一个坏主意(并且闻起来像旧的 VB ON ERROR RESUME NEXT 行为),因为在大多数情况下,您根本没有足够的信息来确定它是否安全继续。通常代码中存在错误或外部资源(如数据库)中的故障导致操作失败。继续意味着用户经常得到操作成功的想法,而实际上并没有。问问自己:更糟糕的是,向用户显示一条通用错误消息,指出出现问题并要求他们重试,或者默默地跳过错误并让用户认为他们的请求已成功处理? p>

想想如果用户在两周后发现他们的订单从未发货,他们会有什么感受。你可能会失去一个客户。或者更糟糕的是,一个病人的MRSA注册默默失败,导致病人没有被护士隔离,导致其他病人被污染,造成高成本甚至死亡。

大多数这类 try-catch-log 行应该被删除,你应该简单地让异常在调用堆栈中冒泡。

你不应该登录吗?你绝对应该!但如果可以,请在应用程序顶部定义一个 try-catch 块。使用 ASP.NET,您可以实现 Application_Error 事件、注册 HttpModule 或定义执行日志记录的自定义错误页面。使用 Win Forms 的解决方案有所不同,但概念保持不变:定义一个单一的***包罗万象。

但是,有时您仍想捕获并记录某种类型的异常。我过去工作的一个系统让业务层抛出ValidationExceptions,这将被表示层捕获。这些异常包含向用户显示的验证信息。由于这些异常会在表示层中被捕获和处理,它们不会冒泡到应用程序的最顶层,也不会最终出现在应用程序的包罗万象的代码中。我仍然想记录这些信息,只是为了了解用户输入无效信息的频率,并了解是否出于正确的原因触发了验证。所以这不是错误记录;只是记录。为此,我编写了以下代码:

try

   // some operations here.

catch (ValidationException ex)

    this.logger.Log(ex);
    throw;

看起来很眼熟?是的,看起来和前面的代码 sn-p 完全一样,不同的是我只捕获了ValidationException 异常。但是,仅看这个sn-p是看不到另一个区别的。应用程序中只有一个地方包含该代码!这是一个装饰器,这让我想到了你应该问自己的下一个问题:

2。我是否违反了 SOLID 原则?

日志记录、审计和安全等内容称为cross-cutting concerns(或方面)。它们被称为横切,因为它们可以横切应用程序的许多部分,并且必须经常应用于系统中的许多类。但是,当您发现您正在编写代码供系统中的许多类使用时,您很可能违反了 SOLID 原则。举个例子:

public void MoveCustomer(int customerId, Address newAddress)

    var watch = Stopwatch.StartNew();

    // Real operation
    
    this.logger.Log("MoveCustomer executed in " +
        watch.ElapsedMiliseconds + " ms.");

在这里您测量执行MoveCustomer 操作所需的时间并记录该信息。系统中的其他操作很可能需要同样的横切关注点。您开始为您的ShipOrderCancelOrderCancelShipping 和其他用例添加这样的代码,这会导致大量代码重复并最终成为维护噩梦(我曾经经历过。)

此代码的问题可以追溯到违反SOLID 原则。 SOLID 原则是一组面向对象的设计原则,可帮助您定义灵活且可维护(面向对象)的软件。 MoveCustomer 示例至少违反了其中两个规则:

    Single Responsibility Principle (SRP)——类应该有一个单一的职责。然而,持有MoveCustomer 方法的类不仅包含核心业务逻辑,而且还测量执行操作所需的时间。换句话说,它有多个职责。 Open-Closed principle (OCP) — 它规定了一种应用程序设计,可以防止您对整个代码库进行彻底的更改;或者,在 OCP 的词汇表中,一个类应该对扩展开放,但对修改关闭。如果您需要向MoveCustomer 用例添加异常处理(第三项职责),您(再次)必须更改MoveCustomer 方法。但是,您不仅需要更改 MoveCustomer 方法,还需要更改许多其他方法,因为它们通常需要相同的异常处理,因此这是一个彻底的更改。

解决这个问题的方法是将日志提取到自己的类中,并允许该类包装原始类:

// 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.");
    

通过将decorator 包裹在真实实例周围,您现在可以将此测量行为添加到类中,而无需更改系统的任何其他部分:

IMoveCustomerService service =
    new MeasuringMoveCustomerDecorator(
        new MoveCustomerService(),
        new DatabaseLogger());

但是,前面的示例确实解决了部分问题(仅解决了 SRP 部分)。在编写如上所示的代码时,您必须为系统中的所有操作定义单独的装饰器,最终会得到MeasuringShipOrderDecoratorMeasuringCancelOrderDecoratorMeasuringCancelShippingDecorator 等装饰器。这再次导致大量重复代码(违反 OCP 原则),并且仍然需要为系统中的每个操作编写代码。这里缺少的是对系统中用例的通用抽象。

缺少的是ICommandHandler&lt;TCommand&gt; 接口。

让我们定义这个接口:

public interface ICommandHandler<TCommand>

    void Execute(TCommand command);

让我们将 MoveCustomer 方法的方法参数存储到它自己的 (Parameter Object) 类中,称为 MoveCustomerCommand

public class MoveCustomerCommand

    public int CustomerId  get; set; 
    public Address NewAddress  get; set; 

让我们将MoveCustomer 方法的行为放在实现ICommandHandler&lt;MoveCustomerCommand&gt; 的类中:

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&lt;T&gt; 看起来很像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 容器可以支持自动注册。这使您不必为添加到系统的每个新命令/处理程序对更改组合根。

大多数现代、成熟的 .NET DI 容器都对装饰器提供了相当不错的支持,尤其是 Autofac (example) 和 Simple Injector (example) 可以轻松注册开放通用装饰器。

另一方面,Unity 和 Castle 具有动态拦截功能(就像 Autofac 对 btw 所做的那样)。动态拦截与装饰有很多共同之处,但它在幕后使用动态代理生成。这可能比使用泛型装饰器更灵活,但在可维护性方面你付出了代价,因为你经常失去类型安全性并且拦截器总是迫使你依赖于拦截库,而装饰器是类型安全的并且可以编写时不依赖外部库。

我已经使用这些类型的设计十多年了,如果没有它,我无法设计我的应用程序。我已经written extensively 讨论了这些设计,最近,我与人合着了一本名为Dependency Injection Principles, Practices, and Patterns 的书,其中更详细地介绍了这种 SOLID 编程风格和上述设计(参见第 10 章)。

【讨论】:

很好的答案史蒂文。我同意你在这里所说的一切,我也不同意捕获异常只是为了记录和重新抛出,因为我认为应用程序域中应该有一个中心点来执行此操作,但我确实觉得有时你想要捕获特定异常(例如 SQLException)以重试操作。 SimpleInjector 看起来很棒,并且在我的使用列表中排名第一,继续努力。以下是 Windsor mikehadlow.blogspot.com/2010/01/… 的装饰器示例,供任何感兴趣的人参考。 @OutOFTouch:重试操作非常适合 AOP,但是您不想在每个数据库操作中都包装这样的东西,而是在事务边界处包装。事实上,this blog post of mine 显示了这一点(看看DeadlockRetryCommandHandlerDecorator)。 如果有什么方法可以多宣传一下,那就去做吧。我用一百万种语言(包括 Haskell、Racket、Forth)编写代码已经 20 年了,这些语言都声称可以改变你的思维方式,并且认为我很擅长。这是 20 年来(自从 4 horsemen 的书)以来的第一件事,它真正改变了我的思维方式,让我后悔我曾经写过的每一个应用程序。这应该是必读的。 只是一个观察——使用 Simple Injector 的装饰器示例现在位于 simpleinjector.readthedocs.io/en/latest/aop.html#decoration

以上是关于Windsor - 从容器中拉出瞬态对象的主要内容,如果未能解决你的问题,请参考以下文章

castle windsor学习-------Container Events 容器的事件

Castle Windsor 学习-----Installer的几种安装方式

csharp 用于ASP.NET MVC的Castle Windsor IoC容器设置

MEF 和 IoC 容器(如 Unity、Autofac、SMap、Ninject、Windsor.Spring.net 等)之间的区别

castle windsor学习-----Inline dependencies 依赖

在共享主机上使用 Castle Windsor 和 NHibernate 设施