Percy

温莎-从容器中取出暂态对象

android

如何从容器中拉出本质上是瞬时的对象?我是否必须在容器中注册它们并注入需要类的构造函数中?将一切都注入到构造函数中感觉不好。同样,对于一个类,我也不想创建一个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
    }
}

问题:

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

  2. QueueMonitor是Singleton,而Data“范围”。我怎样才能注入Data到OnDataReceived()方法?


阅读 212

收藏
2020-11-26

共1个答案

一尘不染

从给出的样本中很难做到非常具体,但是通常,当将ILogger实例注入大多数服务时,应该问自己两件事:

我日志太多了吗?
我是否违反SOLID原则?
1.我日志太多了吗

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

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

编写这样的代码是因为担心丢失错误信息。但是,到处复制这些尝试捕获块都无济于事。更糟糕的是,我经常看到开发人员登录并继续(他们删除了最后一条throw语句)。这确实很糟糕(并且闻起来像旧的VBON ERROR RESUME NEXT行为),因为在大多数情况下,您只是没有足够的信息来确定它是否安全继续。通常,代码中有错误或数据库等外部资源中的故障会导致操作失败。继续意味着用户经常会想到操作成功了,但操作没有成功。问问自己:更糟的是,向用户显示一条通用错误消息,指出出现了问题并要求他们重试,或者静默跳过该错误并让用户认为他们的请求已成功处理?想一想用户如果两周后发现自己的订单从未发货,会感觉如何。您可能会失去客户。或更糟糕的是,患者的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;
}

看起来很熟悉?是的,看起来与先前的代码段完全相同,不同之处在于我只捕获了ValidationException异常。但是,仅查看此代码片段就无法看到另一个差异。应用程序中只有一个地方包含该代码!它是一个装饰器,这使我想到下一个您应该问自己的问题:

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

在这里,您可以测量执行MoveCustomer操作所需的时间,并记录该信息。系统中的其他操作很可能需要同样的跨领域关注。你开始增加这样的代码为您ShipOrder,CancelOrder,CancelShipping和其他用途的情况下,这导致了大量重复的代码,并最终维护的噩梦(我去过那里。)

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

该单一职责原则-班应该有一个单一的责任。MoveCustomer但是,持有该方法的类不仅包含核心业务逻辑,而且还测量了执行操作所需的时间。换句话说,它有多重职责。
在开闭原则(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.");
    }
}

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

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

但是,前面的示例仅解决了部分问题(仅SRP部分)。当编写的代码,如上图所示,你将不得不单独定义装饰器系统中的所有操作,而你最终会与像装饰MeasuringShipOrderDecorator,MeasuringCancelOrderDecorator和MeasuringCancelShippingDecorator。这又导致大量重复代码(违反了OCP原理),并且仍然需要为系统中的每个操作编写代码。这里缺少的是系统中用例的通用抽象。

缺少的是一个ICommandHandler接口。

让我们定义这个接口:

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

这样,向您的系统添加跨领域关注点将变得非常容易。在您的“合成根”中创建方便的方法非常容易,该方法可以将任何已创建的命令处理程序与系统中适用的命令处理程序进行包装。例如:

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容器将非常有用。

如今,大多数现代的.NET DI容器都对装饰器提供了相当不错的支持,尤其是Autofac(示例)和Simple Injector(示例)使注册开放式装饰器变得容易。这两个库都甚至允许基于给定的谓词有条件地应用装饰器,而Simple Injector甚至允许基于泛型类型约束有条件地应用装饰器,从而允许将装饰的类作为工厂注入,并允许注入上下文上下文进入装饰工。

另一方面,Unity和Castle具有动态拦截功能(就像Autofac所做的那样)。动态拦截与装饰有很多共同点,但是它在后台使用了动态代理生成。这可能比使用通用装饰器更灵活,但是在可维护性方面要付出代价,因为您经常松散类型安全性,并且拦截器总是强迫您依赖于侦听库,而装饰器是类型安全的并且可以编写时无需依赖外部库。

2020-11-26