假设我有一个汽车租赁店的服务。
我让CarsController在它唯一的构造函数中接受ICarService, CarService在它唯一的构造函数中接受IUnitOfWork。IUnitOfWork有3个单例只读属性ICarsRepository, IUsersRepository和ILogsRepository,还有一个Commit方法。依赖是通过任何合适的依赖注入容器(ninject, unity等)来解决的,EF是底层的ORM。
我在几乎所有的应用程序中都使用这种架构。每隔一段时间,我就会遇到挑战:
在我的CarsController中有一个叫做RentCar(int carId, int userId)
的方法。CarsController调用CarsService上的RentCar(int carId, int userId)
。CarsService需要在调用CarRepository方法之前和之后执行一些业务逻辑。例如,这个"某些业务逻辑"可以是验证用户是否有效,并保存一些日志。由于我的IUnitOfWork让我访问IUsersRepository和ILogsRepository,我可以很容易地与所有存储库进行交互,并在完成后调用IUnitOfWork上的commit。
然而,我将编写的代码来获取用户,然后验证它并在DB中记录事件,可能已经存在于iuserserve和ILogsService中。在这一点上,我觉得我应该在CarsService中使用这些服务,以避免任何重复的逻辑。
问题:
从一个服务内部访问其他服务是个好主意吗?如果是:
所有依赖的服务都应该通过构造函数单独传递到服务中,还是存在像UnitOfWork这样的模式,其中所有服务都可以通过只读的单例属性访问?
所有的服务方法最后都会在IUnitOfWork上调用Commit。因此,如果我访问服务中的服务,我可能会在原始调用服务完成工作之前调用Commit。
如果我不应该调用服务中的服务,那么上面的重复逻辑场景怎么办?
您在这里描述的是横切关注点的使用。验证、授权和日志记录不是业务问题,它们关心的是横切问题。所以你不想在添加这个时污染你的业务层,并且你想要避免在所有地方做大量的代码复制。
这个问题的解决方案是将横切关注点移动到装饰器,并将它们应用到您的业务逻辑服务中。现在的问题当然是你不想为每个服务定义一个装饰器,因为那样会再次导致大量的代码重复。因此,解决方案是移动到命令/处理程序模式。换句话说,为系统中的任何业务事务定义单个泛型抽象,例如:
public interface ICommandHandler<TCommand>
{
void Handle(TCommand command);
}
每个操作定义一个'command' DTO/message对象,例如:
public class RentCarCommand
{
public int CarId { get; set; }
public int UserId { get; set; }
}
对于每个命令,您需要编写一个特定的ICommandHandler<T>
实现。例如:
public class RentCarCommandHandler : ICommandHandler<RentCarCommand>
{
private readonly IUnitOfWork uow;
public RentCarCommandHandler(IUnitOfWork uow)
{
this.uow = uow;
}
public void Handle(RentCarCommand command)
{
// Business logic of your old CarsService.RentCar method here.
}
}
这个RentCarCommandHandler
取代了CarsService.RentCar
方法。如果CarsService
有多个方法,每个方法将有1个命令+ 1个命令处理程序。
现在你的控制器可以依赖于ICommandHandler<RentCarCommand>
而不是ICarsService
:
public class CarsController : Controller
{
private readonly ICommandHandler<RentCarCommand> rentCarHandler;
public CarsController(ICommandHandler<RentCarCommand> rentCarHandler)
{
this.rentCarHandler = rentCarHandler;
}
public ActionResult Index(int carId, int userId)
{
if (this.ModelState.IsValid)
{
var command = new RentCarCommand { CarId = carId, UserId = userId };
this.rentCarHandler.Handle(command);
}
// etc.
}
}
现在你可能开始思考为什么我们需要这些额外的"复杂性",但我认为我们降低了系统的复杂性,因为我们现在只剩下一个抽象ICommandHandler<T>
。此外,考虑添加横切关注点的问题。现在完全没有了,因为我们可以为横切关注点创建装饰器,比如validation:
public class ValidationCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand>
{
private readonly IValidator validator;
private readonly ICommandHandler<TCommand> handler;
public ValidationCommandHandlerDecorator(IValidator validator,
ICommandHandler<TCommand> handler)
{
this.validator = validator;
this.handler = handler;
}
void ICommandHandler<TCommand>.Handle(TCommand command)
{
// validate the supplied command (throws when invalid).
this.validator.ValidateObject(command);
// forward the (valid) command to the real
// command handler.
this.handler.Handle(command);
}
}
现在您可以将DataAnnotation的属性应用于命令属性,此验证器将确保验证任何命令。
或者做一些审计的修饰符:
public class AuditTrailingCommandHandlerDecorator<TCommand>: ICommandHandler<TCommand>
{
private readonly IAuditTrailRepository repository;
private readonly ICommandHandler<TCommand> handler;
public LoggingCommandHandlerDecorator(
IAuditTrailRepository repository,
ICommandHandler<TCommand> handler)
{
this.logger = logger;
this.handler = handler;
}
void ICommandHandler<TCommand>.Handle(TCommand command)
{
string json = JsonConverter.Serialize(command);
this.repository.AppendToTrail(typeof(TCommand), json);
this.handler.Handle(command);
}
}
由于命令是简单的数据包,我们现在可以将它们序列化为JSON,这通常足以用于审计跟踪。当然,您也可以对日志执行完全相同的操作。
你可以这样装饰你的RentCarCommandHandler
:
ICommandHandler<RentCarCommand> handler =
new AuditTrailingCommandHandlerDecorator<RentCarCommand>(
new AuditTrailRepository(uow),
new ValidationCommandHandlerDecorator<RentCarCommand>(
new Validator(),
RentCarCommandHandler(uow)));
当然,对系统中的每个命令处理程序手动应用它会变得相当麻烦,但这正是DI库可以派上用场的地方。这取决于你使用的库。