设计模式:使用IoC设置控制器,服务,存储库和UnitOfWork

时间:2014-07-14 11:04:17

标签: c# design-patterns dependency-injection inversion-of-control unit-of-work

想象一下,我为汽车租赁店提供服务。

我让CarsController在其唯一的构造函数中接受ICarService,CarService在其唯一的构造函数中接受IUnitOfWork。 IUnitOfWork为ICarsRepository,IUsersRepository和ILogsRepository提供了3个单独的只读属性和一个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中记录事件的代码可能已经存在于IUserSerive和ILogsService中。在这一点上,我觉得我应该在CarsService中使用这些服务,以避免任何重复的逻辑。

问题:

从服务中访问其他服务是一个好主意吗?如果是:

是否应该通过构造函数单独将所有相关服务传递到服务中,或者是否存在类似UnitOfWork的模式,其中所有服务都可以通过只读单例属性进行访问?

所有服务方法最终都会调用IUnitOfWork上的Commit。因此,如果我在服务中访问服务,我可能会在我的原始呼叫服务完成其工作之前调用Commit。

如果我不应该在服务中调用服务,那么上面的逻辑方案重复怎么办?

1 个答案:

答案 0 :(得分:3)

您在这里描述的是跨领域问题的使用。验证,授权和日志记录不是业务问题,它们关注跨领域的问题。因此,您不希望在添加此内容时污染您的业务层,并且您希望防止在整个地方进行大量代码重复。

此问题的解决方案是将横切关注点移至装饰器并将其应用于业务逻辑服务。现在的问题当然是你不想为每个服务定义一个装饰器,因为这会再次导致大量的代码重复。

所以解决方法就是转移到command/handler pattern。换句话说,为系统中的任何业务事务定义单个通用抽象,例如:

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

并定义一个&#39;命令&#39;每个操作的DTO /消息对象,例如:

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.
    }
}

到目前为止,您可能已经开始考虑为什么我们需要所有这些额外的复杂性,但我认为我们降低了系统的复杂程度,因为我们现在只有一个抽象{{ {1}}离开。此外,考虑增加跨领域问题的问题。现在已经完全消失了,因为我们可以为交叉问题创建装饰器,例如验证:

ICommandHandler<T>

现在,您可以将DataAnnotation属性应用于命令属性,此验证程序将确保验证任何命令。

或者一些做一些审计跟踪的装饰器:

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

由于命令是简单的数据包,我们现在将它们序列化为JSON,这通常足以进行审计跟踪。您当然可以使用日志记录执行相同的操作。

您可以按如下方式装饰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); } }

RentCarCommandHandler

将手动应用到系统中的每个命令处理程序当然会变得非常麻烦,但这就是DI库可以派上用场的地方。如何执行此操作取决于您使用的库。