温莎 - 从容器中拉出瞬态物体

use*_*376 57 .net dependency-injection castle-windsor ioc-container

如何从容器中拉出瞬态物体?我是否必须在容器中注册它们并注入需要类的构造函数?将所有内容注入构造函数中感觉不太好.也只是为了一个类,我不想创建一个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
    }
}
Run Code Online (Sandbox Code Playgroud)

问题:

  1. 如何在Record不使用"new"的情况下创建新对象?

  2. QueueMonitorSingleton,而是Data"Scoped".我怎样才能注入DataOnDataReceived()方法?

Ste*_*ven 253

从你给出的样本很难非常具体,但一般来说,当你将ILogger实例注入大多数服务时,你应该问自己两件事:

  1. 我记得太多了吗?
  2. 我违反了SOLID原则吗?

我记得太多了吗?

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

try
{
   // some operations here.
}
catch (Exception ex)
{
    this.logger.Log(ex);
    throw;
}
Run Code Online (Sandbox Code Playgroud)

编写这样的代码来自丢失错误信息的担忧.然而,在整个地方复制这些类型的try-catch块并没有帮助.更糟糕的是,我经常看到开发人员记录并继续(他们删除了最后一个throw语句).这真的很糟糕(并且闻起来像旧的VB ON ERROR RESUME NEXT),因为在大多数情况下,你根本没有足够的信息来确定它是否安全继续.通常,代码中存在导致操作失败的错误.继续意味着用户经常会认为操作成功,而实际上却没有.问问自己:更糟糕的是,向用户显示一条错误消息,说明出现了问题,或者默默地跳过错误并让用户认为他的请求已成功处理?考虑一下,如果他在两周后发现他的订单从未发货,用户将会感觉如何.你可能会失去一个客户.或者更糟糕的是,患者的MRSA注册无声地失败,导致患者不被护理隔离并导致其他患者的污染,导致高成本甚至死亡.

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

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

但有时,您仍然希望捕获并记录某种类型的异常.我过去使用的一个系统,让业务层抛出ValidationExceptionss,这将由表示层捕获.这些例外包含显示给用户的验证信息.由于这些异常会在表示层中被捕获和处理,因此它们不会冒泡到应用程序的最顶层部分,并且不会最终出现在应用程序的全部代码中.我仍然想记录这些信息,只是为了找出用户输入无效信息的频率,并找出是否因正确原因触发了验证.所以这不是错误记录; 只是记录.我编写了以下代码来执行此操作:

try
{
   // some operations here.
}
catch (ValidationException ex)
{
    this.logger.Log(ex);
    throw;
}
Run Code Online (Sandbox Code Playgroud)

看起来很熟悉?是的,看起来与之前的代码片段完全相同,区别在于我只捕获了ValidationExceptions.然而,还有另一个区别,只能通过查看代码段看不到.应用程序中只有一个地方包含该代码!这是一个装饰师,它让我想到你应该问自己的下一个问题:

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

记录,审计和安全等事件被称为跨领域问题(或方面).它们被称为交叉切割,因为它们可以跨越应用程序的许多部分,并且通常必须应用于系统中的许多类.但是,当您发现编写代码以供系统中的许多类使用时,您很可能违反了SOLID原则.以下面的例子为例:

public void MoveCustomer(int customerId, Address newAddress)
{
    var watch = Stopwatch.StartNew();

    // Real operation

    this.logger.Log("MoveCustomer executed in " +
        watch.ElapsedMiliseconds + " ms.");
}
Run Code Online (Sandbox Code Playgroud)

在这里,我们测量执行MoveCustomer操作所需的时间,并记录该信息.系统中的其他操作很可能需要同样的跨领域问题.你会开始添加如下代码为您ShipOrder,CancelOrder,CancelShipping等方法结束这导致很多重复的代码,并最终维护的噩梦.

这里的问题违反了SOLID原则.SOLID原则是一组面向对象的设计原则,可帮助您定义灵活且可维护的软件.该MoveCustomer示例违反了至少两个规则:

  1. 单一职责原则.持有该MoveCustomer方法的类不仅移动客户,还测量执行操作所花费的时间.换句话说,它有多重责任.您应该将测量提取到自己的类中.
  2. 开闭原则(OCP).应该能够在不改变任何现有代码行的情况下改变系统的行为.当您还需要异常处理(第三个责任)时,您(再次)必须更改MoveCustomer方法,这违反了OCP.

除了违反SOLID原则外,我们在这里肯定违反了DRY原则,基本上说代码重复是坏的,mkay.

此问题的解决方案是将日志记录提取到其自己的类中,并允许该类包装原始类:

// 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.");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

IMoveCustomerService command =
    new MeasuringMoveCustomerDecorator(
        new MoveCustomerService(),
        new DatabaseLogger());
Run Code Online (Sandbox Code Playgroud)

然而,前面的示例只解决了部分问题(仅限SOLID部分).当编写的代码,如上图所示,你必须定义装饰器系统中的所有操作,而你最终会与像装饰 MoveCustomer,MoveCustomerMeasuringShipOrderDecorator.这导致了许多重复代码(违反DRY原则),并且仍然需要为系统中的每个操作编写代码.这里缺少的是对系统中用例的常见抽象.缺少的是MeasuringCancelOrderDecorator界面.

我们来定义这个界面:

public interface ICommandHandler<TCommand>
{
    void Execute(TCommand command);
}
Run Code Online (Sandbox Code Playgroud)

让我们将方法的方法参数存储MeasuringCancelShippingDecorator到自己的(Parameter Object)类中,该类称为ICommandHandler<TCommand>:

public class MoveCustomerCommand
{
    public int CustomerId { get; set; }
    public Address NewAddress { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

让我们将MoveCustomer方法的行为放在一个实现的类中MoveCustomerCommand:

public class MoveCustomerCommandHandler : ICommandHandler<MoveCustomerCommand>
{
    public void Execute(MoveCustomerCommand command)
    {
        int customerId = command.CustomerId;
        Address newAddress = command.NewAddress;
        // Real operation
    }
}
Run Code Online (Sandbox Code Playgroud)

这可能看起来很奇怪,但因为我们现在有一个用例的通用抽象,我们可以重写我们的装饰器,如下所示:

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.");
    }
}
Run Code Online (Sandbox Code Playgroud)

这个新的MoveCustomer看起来很像ICommandHandler<MoveCustomerCommand>,但是这个类可以重用于系统中的所有命令处理程序:

ICommandHandler<MoveCustomerCommand> handler1 =
    new MeasuringCommandHandlerDecorator<MoveCustomerCommand>(
        new MoveCustomerCommandHandler(),
        new DatabaseLogger());

ICommandHandler<ShipOrderCommand> handler2 =
    new MeasuringCommandHandlerDecorator<ShipOrderCommand>(
        new ShipOrderCommandHandler(),
        new DatabaseLogger());
Run Code Online (Sandbox Code Playgroud)

通过这种方式,可以更加轻松地向系统添加横切关注点.在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))));
}
Run Code Online (Sandbox Code Playgroud)

但是,如果你的应用程序开始增长,那么在没有容器的情况下引导这一切都会很痛苦.特别是当您的装饰器具有泛型类型约束时.

现在大多数现代的DI Containers for .NET都对装饰器提供了相当不错的支持,尤其是Autofac(示例)和Simple Injector(示例)使得注册开放式通用装饰器变得容易.Simple Injector甚至允许根据给定的谓词或复杂的泛型类型约束有条件地应用装饰器,允许将装饰类作为工厂注入并允许将上下文上下文注入装饰器,所有这些都可以在时间到时间.

另一方面,Unity和Castle有拦截设施(正如Autofac对btw所做的那样).拦截与装饰有很多共同点,但它使用动态代理生成.这比使用通用装饰器更灵活,但是在可维护性方面你会付出代价,因为你经常会失去类型安全性,拦截器总是强迫你依赖拦截库,而装饰器类型安全,可以在不依赖外部库的情况下编写.

如果您想了解更多关于这种设计应用程序的方法,请阅读本文:同时......在我的架构的命令端.

我希望这有帮助.

  • 如果有任何方法可以更多地宣传这一点,那就去做吧.我已经用百万种语言编写了20年的代码(包括Haskell,Racket,Forth),他们都声称改变了你的思维方式,并且认为我很擅长它.这是二十年来的第一件事(自4马书以来),这实际上改变了我的思维方式,让我后悔每一个我写过的应用程序.这应该是必读的. (29认同)
  • 伟大的答案史蒂文.我同意你在这里说的所有内容,我也不同意捕获异常只是为了记录和重新抛出,因为我觉得应用域中应该有一个中心点来做这个,但是我觉得有时候你想要捕获一个特定的异常,例如SQLException,以重试该操作. (8认同)
  • @OutOFTouch:重新执行操作非常适合AOP,但是您不希望在每个数据库操作中包含此类内容,而是在事务边界处.事实上,[我的博客文章](http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91)显示了这一点(看看`DeadlockRetryCommandHandlerDecorator`) . (3认同)