我如何有效地设计我的应用程序,大多数类依赖于ILogger



我将Logger组件注入所有我的类。我的大多数类都定义了Logger属性,除非存在继承链(在这种情况下,只有基类具有此属性,所有派生类都使用该属性)。当它们通过Windsor容器实例化时,它们会将我的ILogger实现注入其中。我目前正在使用属性注入,因为将所有东西注入构造函数感觉不太好。

我可以将它们从容器中取出吗?我必须在容器中注册它们并注入需要的类的构造函数吗?也只是对于一个类,我不想创建一个TypedFactory并将工厂注入到需要的类中。

另一个想到我是new他们需要的基础上。因此,如果我新建它们,我将不得不在这些类中手动实例化Logger。我怎样才能继续使用容器为我所有的类?

示例Windsor注册:

//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对象而不使用"new"?

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

从您提供的示例中很难非常具体,但一般来说,当您将ILogger实例注入大多数服务时,您应该问自己两个问题:

  1. 我是否登录太多了?
  2. 我是否违反了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.
}

在大多数情况下,这是一个坏主意(并且闻起来像旧的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操作所需的时间,并记录该信息。系统中的其他操作很可能需要同样的横切关注点。您开始为ShipOrderCancelOrderCancelShipping和其他用例添加这样的代码,这会导致大量的代码重复,并最终导致维护噩梦(我曾经遇到过这种情况)

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

  1. 单一职责原则(SRP) -类应该有单一职责。但是,包含MoveCustomer方法的类不仅包含核心业务逻辑,而且还度量执行操作所需的时间。换句话说,它有多个职责
  2. 开闭原则(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 service =
new MeasuringMoveCustomerDecorator(
new MoveCustomerService(),
new DatabaseLogger());
然而,前面的示例只解决了部分问题(仅是SRP部分)。在编写如上所示的代码时,您必须为系统中的所有操作定义单独的装饰器,并且最终会得到像MeasuringShipOrderDecoratorMeasuringCancelOrderDecoratorMeasuringCancelShippingDecorator这样的装饰器。这又导致了大量的重复代码(违反了OCP原则),并且仍然需要为系统中的每个操作编写代码。这里缺少的是对系统中用例的通用抽象。

缺少的是ICommandHandler<TCommand>接口。

让我们定义这个接口:

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

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

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

提示:这个MoveCustomerCommand对象变成一条消息。这就是为什么有些人用'Message'后缀此类型,称之为MoveCustomerMessage。其他人倾向于将其称为MoveCustomerRequest,而其他人则完全删除后修复并简单地将此参数对象称为MoveCustomer。当我最初写这个答案时,我习惯使用"Command"后缀,但现在,我倾向于简单地使用MoveCustomer。但是无论您选择什么,这里的强大之处在于数据(命令/消息)和行为(处理程序)之间的分离,我们将在下面看到。

让我们把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根中创建一个方便的方法非常容易,该方法可以将任何创建的命令处理程序与系统中适用的命令处理程序包装在一起。例如:

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(示例)和Simple Injector(示例)使得注册开放泛型装饰器变得很容易。

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

我已经使用这些类型的设计超过十年了,无法想象没有它我的应用程序设计。我已经写了大量关于这些设计的文章,最近,我与人合著了一本名为《依赖注入原则、实践和模式》的书,其中详细介绍了这种SOLID编程风格和上面描述的设计(参见第10章)。

相关内容

最新更新