根据您提供的样本,很难做出非常具体的说明。但是通常情况下,在将ILogger
实例注入到大多数服务中时,您应该考虑以下两个问题:
- 我是否记录了过多日志?
- 我是否违反了SOLID原则?
1. 我是否记录了过多日志
当您有很多这样的代码时,您就会记录太多日志:
try
{
}
catch (Exception ex)
{
this.logger.Log(ex);
throw;
}
写出这样的代码是因为担心丢失错误信息。但是在各个地方重复这些try-catch块并没有帮助。更糟糕的是,我经常看到开发人员通过删除最后一个throw语句来记录和继续执行:
try
{
}
catch (Exception ex)
{
this.logger.Log(ex);
}
这往往是个坏主意(并且闻起来像旧的VB
ON ERROR RESUME NEXT
行为),因为在大多数情况下,你没有足够的信息来确定是否安全继续。通常代码中存在错误或外部资源(如数据库)出现故障导致操作失败。继续执行意味着用户经常会认为操作成功了,而实际上并没有。问问自己:哪个更糟糕,向用户显示一个通用错误消息,告诉他们再试一次,还是默默地跳过错误,让用户
认为他们的请求已被成功处理?
想象一下,如果两周后用户发现他们的订单从未发货,他们会感觉如何。你可能会失去一个客户。或者更糟糕的是,患者的
MRSA注册默默失败,导致护理人员无法对其进行隔离,导致其他患者受到污染,引起高昂的费用或甚至死亡。
大多数这种try-catch-log行应该被删除,你应该让异常沿着调用堆栈向上抛出。
你应该记录日志吗?当然应该!但如果可以的话,最好在应用程序的顶部定义一个try-catch块。对于ASP.NET,您可以实现Application_Error
事件、注册HttpModule
或定义自定义错误页面来记录日志。对于Win Forms,解决方案是不同的,但概念是相同的:定义一个单一的顶级catch-all。
然而,有时您仍然希望捕获和记录特定类型的异常。我曾经使用过的一个系统允许业务层抛出ValidationExceptions
,这些异常将被表示层捕获。这些异常包含用于向用户显示的验证信息。由于这些异常会在表示层中被捕获和处理,因此它们不会冒泡到应用程序的最顶部,并且不会出现在应用程序的catch-all代码中。尽管如此,我仍然想记录这些信息,只是为了找出用户输入无效信息的频率,并找出验证是否出于正确的原因触发。因此,这不是错误日志记录;只是日志记录。我编写了以下代码来实现:
try
{
}
catch (ValidationException ex)
{
this.logger.Log(ex);
throw;
}
看起来很熟悉?是的,与先前的代码片段完全相同,不同之处在于我仅捕获了ValidationException
异常。但是,有另一个区别,仅从这个片段中无法看到。整个应用程序中只有一个地方包含了那段代码!它是一个装饰器,这让我想到了下一个你应该问自己的问题:
2. 我违反了SOLID原则吗?
像日志记录、审计和安全等内容被称为cross-cutting concerns(或方面)。它们被称为横切关注点,因为它们可以穿过应用程序的许多部分,并且通常必须应用于系统中的许多类。但是,当您发现为其在系统中的许多类中编写代码时,您很可能正在违反SOLID原则。例如,考虑以下示例:
public void MoveCustomer(int customerId, Address newAddress)
{
var watch = Stopwatch.StartNew();
this.logger.Log("MoveCustomer executed in " +
watch.ElapsedMiliseconds + " ms.");
}
在这里,您可以测量执行
MoveCustomer
操作所需的时间,并记录该信息。很可能系统中的其他操作需要相同的横切关注点。您开始为
ShipOrder
、
CancelOrder
、
CancelShipping
和其他用例添加类似于此的代码,这导致了大量的代码重复,最终成为一个维护噩梦(我也曾经历过)。
这段代码的问题可以追溯到违反
SOLID原则。SOLID原则是一组面向对象设计原则,可帮助您定义灵活且易于维护的(面向对象)软件。
MoveCustomer
示例违反了至少两个规则:
- 单一职责原则 (SRP) - 类应该只有一个职责。然而,持有
MoveCustomer
方法的类不仅包含核心业务逻辑,还测量执行操作所需的时间。换句话说,它有多个职责。
- 开闭原则 (OCP) - 它规定了一种应用程序设计,可以防止您不得不在整个代码库中进行大规模更改;或者换句话说,在OCP的词汇表中,一个类应该是可扩展的,但应该对修改关闭。如果您需要将异常处理(第三个职责)添加到
MoveCustomer
用例中,则必须(再次)修改MoveCustomer
方法。但是,您不仅需要修改MoveCustomer
方法,还需要修改许多其他方法,因为它们通常需要相同的异常处理,从而使其成为大规模更改。
解决此问题的方法是将日志记录提取到自己的类中,并允许该类包装原始类:
public class MoveCustomerService : IMoveCustomerService
{
public virtual void MoveCustomer(int customerId, Address newAddress)
{
}
}
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 部分)。当按照上述代码编写时,您将不得不为系统中的所有操作定义单独的装饰器,并且您最终将拥有像
MeasuringShipOrderDecorator
、
MeasuringCancelOrderDecorator
和
MeasuringCancelShippingDecorator
这样的装饰器。这又导致了大量重复代码(违反了 OCP 原则),并且仍然需要为系统中的每个操作编写代码。缺少的是系统用例上的公共抽象。
缺少的是一个 ICommandHandler<TCommand>
接口。
让我们来定义这个接口:
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; }
}
提示:这个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;
}
}
一开始可能看起来有些奇怪,但由于您现在拥有了用例的通用抽象,因此可以将装饰器重写为以下内容:
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 容器可以支持自动注册。这样一来,你就不必为每个新的命令/处理程序对添加到系统中而更改组合根。
大多数现代成熟的 .NET DI 容器都具有相当不错的装饰器支持,特别是 Autofac(示例)和 Simple Injector(示例),它们使注册开放式通用装饰器变得容易。
另一方面,Unity 和 Castle 则拥有动态拦截功能(就像 Autofac 一样)。动态拦截与装饰器有很多相似之处,但它在底层使用了动态代理生成。这可能比使用通用装饰器更灵活,但在可维护性方面则需要付出代价,因为你经常会失去类型安全性,并且拦截器总是强制让你依赖于拦截库,而装饰器是类型安全的,并且可以编写而不必依赖外部库。
我已经使用这些类型的设计十多年了,无法想象在没有它的情况下设计我的应用程序。我已经广泛撰写关于这些设计的文章,并且最近,我合著了一本名为Dependency Injection Principles, Practices, and Patterns的书,其中详细介绍了这种SOLID编程风格和上面描述的设计(见第10章)。