设计模式:使用IoC设置控制器、服务、存储库和工作单元



假设我有一个汽车租赁店的服务。

我让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库可以派上用场的地方。这取决于你使用的库。

最新更新