【问题标题】:Design Pattern: Setting up Controllers, Service, Repositories and UnitOfWork with IoC设计模式:使用 IoC 设置控制器、服务、存储库和 UnitOfWork
【发布时间】:2014-07-14 11:04:16
【问题描述】:

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

我让 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 中的代码可能已经存在于 IUserServe 和 ILogsService 中。在这一点上,我觉得我应该在 CarsService 中使用这些服务以避免任何重复的逻辑。

问题:

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

是否应该通过构造函数将所有依赖服务单独传递给服务,或者是否有像 UnitOfWork 这样的模式,其中所有服务都可以通过只读单例属性访问?

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

如果我不应该在服务中调用服务,那么上面的重复逻辑场景呢?

【问题讨论】:

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


    【解决方案1】:

    您在这里描述的是横切关注点的使用。验证、授权和日志记录不是业务问题,它们关心横切问题。所以你不想在添加这个的时候污染你的业务层,并且你想避免在整个地方做大量的代码重复。

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

    因此,解决方案是移至command/handler pattern。换句话说,为系统中的任何业务事务定义一个通用抽象,例如:

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

    并为每个操作定义一个“命令”DTO/消息对象,例如:

    public class RentCarCommand
    {
        public int CarId { get; set; }
        public int UserId { get; set; }
    }
    

    对于每个命令,您需要编写特定的ICommandHandler&lt;T&gt; 实现。例如:

    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&lt;RentCarCommand&gt; 而不是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&lt;T&gt;。此外,考虑添加横切关注点的问题。现在完全没有了,因为我们可以为诸如验证之类的横切关注点创建装饰器:

    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 库可以派上用场的地方。如何执行此操作取决于您使用的库。

    【讨论】:

    • 感谢 Steven,非常感谢您的详细回复。我想我今天学到了一些非常有用的东西,这会伴随我很长时间。
    • 一个简单的问题,如果你真的想从另一个服务访问一个方法。我应该永远不要这样做,并且总是找到替代方法吗?我的意思是我给出的例子是关于日志记录和验证的,但它可能是别的东西。或者,如果我遇到这种情况,那是不是我做错了什么?
    • @aDev:命令处理程序实现可能仍会将工作委派给系统的其他部分,所以这很好。您可以将服务注入到处理程序的构造函数中。我阻止做的是让处理程序直接或间接依赖于其他命令处理程序,因为这会使系统复杂化。在这种情况下,您应该简单地从处理程序中提取公共逻辑到服务中,然后将该服务注入到所有需要它的处理程序中。
    • 有趣。谢谢。我现在会尝试这整个事情。我仍然认为我会被每个服务方法结束时调用的 commit 方法所困扰。例如,如果 ServiceA 有 MethodA,它在方法 B 完成时调用 ServiceB 的 MethodB,它将在 uow 上执行 Commit()。但这不应该发生,因为 MethodA 仍然有几件事要完成,一旦完成,它将尝试再次在 uow 上调用 Commit()。 (如果你能解决这个问题,那就太好了,否则我会再单独问这个问题:))。非常感谢您的帮助。
    • @aDev:你应该做的是有一个装饰器,你可以在其中调用 Submit()。这释放了您的服务调用它的需求,并允许整个操作成为单个事务。
    猜你喜欢
    • 1970-01-01
    • 2019-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-12-14
    • 1970-01-01
    • 2020-08-01
    • 1970-01-01
    相关资源
    最近更新 更多