Windsor - 从容器中拉出瞬态对象 [英] Windsor - pulling Transient objects from the container

查看:28
本文介绍了Windsor - 从容器中拉出瞬态对象的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

我想到的另一个想法是根据需要新"它们.但我也在我的所有类中注入了一个 Logger 组件(通过属性).因此,如果我新建它们,则必须手动实例化这些类中的 Logger.我怎样才能继续在我的所有课程中使用该容器?

Logger 注入: 我的大部分类都定义了 Logger 属性,除非有继承链(在这种情况下只有基类具有此属性,并且所有派生类都使用该属性).当这些通过 Windsor 容器实例化时,它们会将我的 ILogger 实现注入其中.

//将QueueMonitor安装为单例Container.Register(Component.For<QueueMonitor>().LifestyleSingleton());//将DataProcessor安装为TrnsientContainer.Register(Component.For<DataProcessor>().LifestyleTransient());Container.Register(Component.For<Data>().LifestyleScoped());公共类队列监视器{私有数据处理器;公共 ILogger 记录器 { 获取;放;}public void OnDataReceived(数据数据){//从工厂拉取dataProcessordataProcessor.ProcessData(data);}}公共类数据处理器{公共 ILogger 记录器 { 获取;放;}public Record[] ProcessData(数据数据){//数据可以有多个记录//遍历数据并创建新的记录集//这是创建新记录的正确方法吗?//我如何在这里使用容器并避免新"Record record = new Record(/*使用数据*/);...//返回记录列表}}公开课记录{公共 ILogger 记录器 { 获取;放;}私人_recordNumber;私人_recordOwner;公共字符串 GetDescription(){Logger.LogDebug("记录一些东西");//返回自定义描述}}

问题:

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

  2. QueueMonitorSingleton,而 Data 是Scoped".如何将 Data 注入 OnDataReceived() 方法?

解决方案

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

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

1.我是否记录太多

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

试试{//这里有一些操作.}捕获(异常前){this.logger.Log(ex);扔;}

写这样的代码来自于担心丢失错误信息.然而,到处复制这些类型的 try-catch 块也无济于事.更糟糕的是,我经常看到开发人员记录并继续删除最后一个 throw 语句:

试试{//这里有一些操作.}捕获(异常前){this.logger.Log(ex);//<!-- 不再抛出.执行将继续.}

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

想想如果用户两周后发现他们的订单从未发货,他们会有什么感受.您可能会失去一位客户.或者更糟的是,患者的 MRSA 注册默默地失败了,导致患者得不到护理隔离,造成其他患者的污染,造成高额费用甚至死亡.

大多数此类 try-catch-log 行都应该删除,您应该简单地让异常在调用堆栈中冒泡.

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

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

试试{//这里有一些操作.}捕获(验证异常前){this.logger.Log(ex);扔;}

看起来很眼熟?是的,看起来和前面的代码片段完全一样,不同的是我只捕获了 ValidationException 异常.但是,仅通过查看此代码段无法看出另一个差异.应用程序中只有一处包含该代码!这是一个装饰器,这让我想到了下一个你应该问自己的问题:

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

诸如日志记录、审计和安全之类的事情被称为横切关注点(或方面).它们被称为横切,因为它们可以横切应用程序的许多部分,并且必须经常应用于系统中的许多类.然而,当您发现您正在编写用于系统中许多类的代码时,您很可能违反了 SOLID 原则.以下面的例子为例:

public void MoveCustomer(int customerId, Address newAddress){var watch = Stopwatch.StartNew();//实际操作this.logger.Log("MoveCustomer 在" +watch.ElapsedMiliseconds + "女士");}

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

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

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

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

//真正的东西公共类 MoveCustomerService : IMoveCustomerService{public virtual void MoveCustomer(int customerId, Address newAddress){//实际操作}}//装饰器公共类MeasurementMoveCustomerDecorator : IMoveCustomerService{私人只读 IMoveCustomerService 装饰;私有只读 ILogger 记录器;公共测量移动客户装饰器(IMoveCustomerService 修饰,ILogger 记录器){this.decorated = 装饰;this.logger = 记录器;}public void MoveCustomer(int customerId, Address newAddress){var watch = Stopwatch.StartNew();this.decorated.MoveCustomer(customerId, newAddress);this.logger.Log("MoveCustomer 在" +watch.ElapsedMiliseconds + "女士");}}

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

IMoveCustomerService 服务 =新的测量移动客户装饰器(新的移动客户服务(),新的数据库记录器());

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

缺少的是 ICommandHandler 接口.

让我们定义这个接口:

公共接口 ICommandHandler{无效执行(TCommand命令);}

让我们将 MoveCustomer 方法的方法参数存储到它自己的 (参数对象) 名为MoveCustomerCommand 的类:

public class MoveCustomerCommand{公共 int CustomerId { 获取;放;}公共地址 NewAddress { get;放;}}

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

public class MoveCustomerCommandHandler : ICommandHandler{公共无效执行(MoveCustomerCommand 命令){int customerId = command.CustomerId;地址 newAddress = command.NewAddress;//实际操作}}

这乍一看可能很奇怪,但是因为您现在对用例有了一般的抽象,您可以将装饰器重写为以下内容:

公共类MeasurementCommandHandlerDecorator: ICommandHandler{私人 ILogger 记录器;私有 ICommandHandler装饰;公共测量CommandHandlerDecorator(ILogger 记录器,ICommandHandler装饰){this.decorated = 装饰;this.logger = 记录器;}公共无效执行(TCommand 命令){var watch = Stopwatch.StartNew();this.decorated.Execute(command);this.logger.Log(typeof(TCommand).Name + "在 " +watch.ElapsedMiliseconds + "女士");}}

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

ICommandHandler处理程序 1 =新的MeasurementCommandHandlerDecorator(新的 MoveCustomerCommandHandler(),新的数据库记录器());ICommandHandler处理程序 2 =新的MeasurementCommandHandlerDecorator(新 ShipOrderCommandHandler(),新的数据库记录器());

通过这种方式,向您的系统添加横切关注点会容易得多.在您的 Composition Root 可以用系统中适用的命令处理程序包装任何创建的命令处理程序.例如:

私有静态 ICommandHandler;装饰(ICommandHandler被装饰者){返回新的MeasurementCommandHandlerDecorator(新的数据库记录器(),新的 ValidationCommandHandlerDecorator(新的验证提供者(),新的 AuthorizationCommandHandlerDecorator(新的授权检查器(新的 AspNetUserProvider()),new TransactionCommandHandlerDecorator(装饰者))));}

这个方法可以如下使用:

ICommandHandler处理程序 1 =装饰(新的 MoveCustomerCommandHandler());ICommandHandler处理程序 2 =装饰(新 ShipOrderCommandHandler());

但是,如果您的应用程序开始增长,使用 DI 容器进行引导会很有用,因为 DI 容器可以支持自动注册.这可以防止您为添加到系统中的每个新命令/处理程序对更改组合根.

大多数现代、成熟的 .NET DI 容器都对装饰器有相当不错的支持,尤其是 Autofac (example) 和 Simple Injector (example) 使注册变得容易开放式通用装饰器.

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

十多年来我一直在使用这些类型的设计,没有它我无法设计我的应用程序.我已经大量写了这些设计,最近,我与人合着了一本书称为依赖注入原则、实践和模式,其中详细介绍了这种 SOLID 编程风格和上述设计(见第 10 章).

How can I pull objects from the container that are transient in nature? Do I have to register them with the container and inject in the constructor of the needing class? Injecting everything into the constructor doesn't feel good. Also just for one class I don't want to create a TypedFactory and inject the factory into the needing class.

Another thought that came to me was "new" them up on need basis. But I am also injecting a Logger component (through property) into all my classes. So if I new them up, I will have to manually instantiate the Logger in those classes. How can I continue to use the container for ALL of my classes?

Logger injection: Most of my classes have the Logger property defined, except where there is inheritance chain (in that case only the base class has this property, and all the deriving classes use that). When these are instantiated through Windsor container, they would get my implementation of ILogger injected into them.

//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
    }
}

Questions:

  1. How do I create new Record object without using "new"?

  2. QueueMonitor is Singleton, whereas Data is "Scoped". How can I inject Data into OnDataReceived() method?

解决方案

From the samples you give it is hard to be very specific, but in general, when you inject ILogger instances into most services, you should ask yourself two things:

  1. Do I log too much?
  2. Do I violate the SOLID principles?

1. Do I log too much

You are logging too much, when you have a lot of code like this:

try
{
   // some operations here.
}
catch (Exception ex)
{
    this.logger.Log(ex);
    throw;
}

Writing code like this comes from the concern of losing error information. Duplicating these kinds of try-catch blocks all over the place, however, doesn't help. Even worse, I often see developers log and continue by removing the last throw statement:

try
{
   // some operations here.
}
catch (Exception ex)
{
    this.logger.Log(ex); // <!-- No more throw. Execution will continue.
}

This is most of the cases a bad idea (and smells like the old VB ON ERROR RESUME NEXT behavior), because in most situations you simply have not enough information to determine whether it is safe continue. Often there is a bug in the code or a hiccup in an external resource like a database that caused the operation to fail. To continue means that the user often gets the idea that the operation succeeded, while it hasn't. Ask yourself: what is worse, showing users a generic error message saying that there something gone wrong and ask them to try again, or silently skipping the error and letting users think their request was successfully processed?

Think about how users will feel if they found out two weeks later that their order was never shipped. You’d probably lose a customer. Or worse, a patient’s MRSA registration silently fails, causing the patient not to be quarantined by nursing and resulting in the contamination of other patients, causing high costs or perhaps even death.

Most of these kinds of try-catch-log lines should be removed and you should simply let the exception bubble up the call stack.

Shouldn't you log? You absolutely should! But if you can, define one try-catch block at the top of the application. With ASP.NET, you can implement the Application_Error event, register an HttpModule or define a custom error page that does the logging. With Win Forms the solution is different, but the concept stays the same: Define one single top most catch-all.

Sometimes, however, you still want to catch and log a certain type of exception. A system I worked on in the past let the business layer throw ValidationExceptions, which would be caught by the presentation layer. Those exceptions contained validation information for display to the user. Since those exceptions would get caught and processed in the presentation layer, they would not bubble up to the top most part of the application and didn't end up in the application's catch-all code. Still I wanted to log this information, just to find out how often the user entered invalid information and to find out whether the validations were triggered for the right reason. So this was no error logging; just logging. I wrote the following code to do this:

try
{
   // some operations here.
}
catch (ValidationException ex)
{
    this.logger.Log(ex);
    throw;
}

Looks familiar? Yes, looks exactly the same as the previous code snippet, with the difference that I only caught ValidationException exceptions. However, there was another difference that can't be seen by just looking at this snippet. There was only one place in the application that contained that code! It was a decorator, which brings me to the next question you should ask yourself:

2. Do I violate the SOLID principles?

Things like logging, auditing, and security, are called cross-cutting concerns (or aspects). They are called cross cutting, because they can cut across many parts of your application and must often be applied to many classes in the system. However, when you find you're writing code for their use in many classes in the system, you are most likely violating the SOLID principles. Take for instance the following example:

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

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

Here you measure the time it takes to execute the MoveCustomer operation and you log that information. It is very likely that other operations in the system need this same cross-cutting concern. You start adding code like this for your ShipOrder, CancelOrder, CancelShipping, and other use cases, and this leads to a lot of code duplication and eventually a maintenance nightmare (I've been there.)

The problem with this code can be traced back to a violation of the SOLID principles. The SOLID principles are a set of object-oriented design principles that help you in defining flexible and maintainable (object-oriented) software. The MoveCustomer example violated at least two of those rules:

  1. The Single Responsibility Principle (SRP)—classes should have a single responsibility. The class holding the MoveCustomer method, however, does not only contain the core business logic, but also measures the time it takes to do the operation. In other words, it has multiple responsibilities.
  2. The Open-Closed principle (OCP)—it prescribes an application design that prevents you from having to make sweeping changes throughout the code base; or, in the vocabulary of the OCP, a class should be open for extension, but closed for modification. In case you need to add exception handling (a third responsibility) to the MoveCustomer use case, you (again) have to alter the MoveCustomer method. But not only do you have to alter the MoveCustomer method, but many other methods as well, as they will typically require that same exception handling, making this a sweeping change.

The solution to this problem is to extract the logging into its own class and allow that class to wrap the original class:

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

By wrapping the decorator around the real instance, you can now add this measuring behavior to the class, without any other part of the system to change:

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

The previous example did, however, just solve part of the problem (only the SRP part). When writing the code as shown above, you will have to define separate decorators for all operations in the system, and you'll end up with decorators like MeasuringShipOrderDecorator, MeasuringCancelOrderDecorator, and MeasuringCancelShippingDecorator. This lead again to a lot of duplicate code (a violation of the OCP principle), and still needing to write code for every operation in the system. What's missing here is a common abstraction over use cases in the system.

What's missing is an ICommandHandler<TCommand> interface.

Let's define this interface:

public interface ICommandHandler<TCommand>
{
    void Execute(TCommand command);
}

And let's store the method arguments of the MoveCustomer method into its own (Parameter Object) class called MoveCustomerCommand:

public class MoveCustomerCommand
{
    public int CustomerId { get; set; }
    public Address NewAddress { get; set; }
}

And let's put the behavior of the MoveCustomer method in a class that implements ICommandHandler<MoveCustomerCommand>:

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

This might look weird at first, but because you now have a general abstraction for use cases, you can rewrite your decorator to the following:

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

This new MeasuringCommandHandlerDecorator<T> looks much like the MeasuringMoveCustomerDecorator, but this class can be reused for all command handlers in the system:

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

ICommandHandler<ShipOrderCommand> handler2 =
    new MeasuringCommandHandlerDecorator<ShipOrderCommand>(
        new ShipOrderCommandHandler(),
        new DatabaseLogger());

This way it will be much, much easier to add cross-cutting concerns to your system. It's quite easy to create a convenient method in your Composition Root that can wrap any created command handler with the applicable command handlers in the system. For instance:

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))));
}

This method can be used as follows:

ICommandHandler<MoveCustomerCommand> handler1 = 
    Decorate(new MoveCustomerCommandHandler());

ICommandHandler<ShipOrderCommand> handler2 =
    Decorate(new ShipOrderCommandHandler());

If your application starts to grow, however, it can get useful to bootstrap this with a DI Container, because a DI Container can support Auto-Registration. This prevents you from having to make changes to your Composition Root for every new command/handler pair you add to the system.

Most modern, mature DI Containers for .NET have fairly decent support for decorators, and especially Autofac (example) and Simple Injector (example) make it easy to register open-generic decorators.

Unity and Castle, on the other hand, have Dynamic Interception facilities (as Autofac does to btw). Dynamic Interception has a lot in common with decoration, but it uses dynamic-proxy generation under the covers. This can be more flexible than working with generic decorators, but you pay the price when it comes to maintainability, because you often loose type safety and interceptors always force you to take a dependency on the interception library, while decorators are type-safe and can be written without taking a dependency on an external library.

I've been using these types of designs for over a decade now and can't think of designing my applications without it. I've written extensively about these designs, and more recently, I coauthored a book called Dependency Injection Principles, Practices, and Patterns, which goes into much more detail on this SOLID programming style and the design described above (see chapter 10).

这篇关于Windsor - 从容器中拉出瞬态对象的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆