简体   繁体   English

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

[英]Design Pattern: Setting up Controllers, Service, Repositories and UnitOfWork with IoC

Imagine I have a service for car rental shop. 想象一下,我有一个汽车租赁店的服务。

I have CarsController accepting ICarService in its only constructor, CarService accepting IUnitOfWork in its only constructor. 我让CarsController在其唯一的构造函数中接受ICarService,CarService在其唯一的构造函数中接受IUnitOfWork。 IUnitOfWork has 3 singleton read-only properties for ICarsRepository, IUsersRepository and ILogsRepository and a Commit method. IUnitOfWork为ICarsRepository,IUsersRepository和ILogsRepository提供了3个单独的只读属性和一个Commit方法。 Dependencies are being resolved via any decent dependency injection container (ninject, unity etc) and EF is the underlying ORM. 依赖关系通过任何体面的依赖注入容器(ninject,unity等)来解决,EF是底层的ORM。

I use this kind of architecture in almost all of my applications. 我几乎在所有应用程序中都使用这种架构。 And every now and then I come across a challenge: 我时不时地遇到挑战:

In my CarsController there is a method called RentCar(int carId, int userId) . 在我的CarsController中有一个名为RentCar(int carId, int userId)的方法RentCar(int carId, int userId) CarsController calls RentCar(int carId, int userId) on CarsService. CarsController在CarsService上调用RentCar(int carId, int userId) CarsService needs to perform some business logic before and after calling CarRepository methods. CarsService需要在调用CarRepository方法之前和之后执行一些业务逻辑。 This 'some business logic' could be for example verifying the user is valid, and saving some logs. 这种“一些业务逻辑”可以是例如验证用户是否有效,并保存一些日志。 As my IUnitOfWork gives me access to IUsersRepository and ILogsRepository I can interact with all repositories very easily and call commit on IUnitOfWork in the end when I am done. 由于我的IUnitOfWork允许我访问IUsersRepository和ILogsRepository,因此我可以非常轻松地与所有存储库进行交互,并在完成后最终调用IUnitOfWork上的commit。

However, the code that I will write to get the user and then validate it and log the event in DB, might already be there in IUserSerive and ILogsService. 但是,我将编写以获取用户然后验证它并在DB中记录事件的代码可能已经存在于IUserSerive和ILogsService中。 At this point I feel like I should be using those services within CarsService to avoid any duplication of logic. 在这一点上,我觉得我应该在CarsService中使用这些服务来避免任何重复的逻辑。

Questions: 问题:

Is it a good idea to access other services from within a service? 从服务中访问其他服务是一个好主意吗? If Yes: 如是:

Should all dependent services be passed into the service via constructor individually, or is there a pattern like UnitOfWork, where all service can be accessed via read-only singleton properties? 是否应该通过构造函数单独将所有相关服务传递到服务中,或者是否存在类似UnitOfWork的模式,其中所有服务都可以通过只读单例属性访问?

All service methods call Commit on IUnitOfWork in the end. 最后,所有服务方法都在IUnitOfWork上调用Commit。 So If I do access the service within a service, I might call Commit before my original calling service has finished its work. 因此,如果我在服务中访问服务,我可能会在我的原始呼叫服务完成其工作之前调用Commit。

If I should NOT be calling services within a service, then what about the duplication of logic scenario above? 如果我不应该在服务中调用服务,那么上面的逻辑方案重复怎么样?

What you are describing here is the use of cross-cutting concerns. 您在这里描述的是跨领域问题的使用。 Validation, authorization and logging are no business concerns, they care cross-cutting concerns. 验证,授权和日志记录不是业务问题,它们关注跨领域的问题。 So you don't want to pollute your business layer while adding this, and you want to prevent having to do lots of code duplication all over the place. 因此,您不希望在添加此业务层时污染您的业务层,并且您希望防止在整个地方进行大量代码重复。

The solution to this problem is to move the cross-cutting concerns to decorators and apply them to your business logic services. 此问题的解决方案是将横切关注点移至装饰器并将其应用于业务逻辑服务。 Problem now of course is that you don't want to have to define a decorator per service, since that would again lead to a lot of code duplication. 现在的问题当然是你不想为每个服务定义一个装饰器,因为这会再次导致大量的代码重复。

So the solution to that is to move to the command/handler pattern . 因此,解决方案是转移到命令/处理程序模式 In other words, define a single generic abstractions for any business transaction in the system, for instance: 换句话说,为系统中的任何业务事务定义单个通用抽象,例如:

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

And define a 'command' DTO/message object per operation, for instance: 并为每个操作定义一个“命令”DTO /消息对象,例如:

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

For each command you need to write a specific ICommandHandler<T> implementation. 对于每个命令,您需要编写特定的ICommandHandler<T>实现。 For instance: 例如:

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

This RentCarCommandHandler replaces the CarsService.RentCar method. 这个RentCarCommandHandler取代了CarsService.RentCar方法。 If CarsService has multiple methods, for each method there will be 1 command + 1 command handler. 如果CarsService有多个方法,则每个方法都有1个命令+ 1个命令处理程序。

Now your controller can depend on ICommandHandler<RentCarCommand> instead of ICarsService : 现在您的控制器可以依赖于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.
    }
}

By now you might be starting to think why we need all this extra 'complexity', but I would argue that we lowered the amount of complexity of the system, since we now only have one single abstraction ICommandHandler<T> left. 到现在为止,您可能已经开始考虑为什么我们需要所有这些额外的“复杂性”,但我认为我们降低了系统的复杂程度,因为我们现在只剩下一个单一的抽象ICommandHandler<T> Furthermore, think about the problem of adding cross-cutting concerns. 此外,考虑增加跨领域问题的问题。 That is now completely gone, since we can create decorators for that cross-cutting concerns such as 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);
    }
}

Now you can apply DataAnnotation's attributes to command properties and this validator will ensure that any command is verified. 现在,您可以将DataAnnotation的属性应用于命令属性,此验证程序将确保验证任何命令。

Or some decorator that does some audit trailing: 或者一些做一些审计跟踪的装饰器:

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

Since commands are simple data packages we an now serialize them to JSON and that will usually be enough for audit trailing. 由于命令是简单的数据包,我们现在将它们序列化为JSON,这通常足以进行审计跟踪。 You can of course do exactly the same with logging. 您当然可以使用日志记录完全相同。

And you can decorate your RentCarCommandHandler as follows: 您可以按如下方式装饰RentCarCommandHandler

ICommandHandler<RentCarCommand> handler = 
    new AuditTrailingCommandHandlerDecorator<RentCarCommand>(
        new AuditTrailRepository(uow),
            new ValidationCommandHandlerDecorator<RentCarCommand>(
                new Validator(),
                RentCarCommandHandler(uow)));

It would become quite cumbersome of course to apply this manually to each command handler in your system, but this is where DI libraries can come in handy. 当然,将此手动应用于系统中的每个命令处理程序会变得非常麻烦,但这就是DI库可以派上用场的地方。 How to do this depends on the library you use. 如何执行此操作取决于您使用的库。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 使用UnitOfWork到Service模式的EntityFramework - EntityFramework with UnitOfWork to Service pattern 如何在C#中设置DI / IoC和/或工厂模式设计 - How to set up DI / IoC and/or Factory pattern design in C# 这是围绕DDD使用仓库模式设计的一个很好的UnitOfWork - Is this a good UnitOfWork with Repository pattern Design around DDD 具有IOC容器的策略设计模式 - 具体为Ninject - Strategy Design pattern with IOC containers - Ninject specifically 工作单元,存储库和连接的处置 - unitofwork, repositories and disposing of connections DTO和UnitOfWork模式是一种为Web应用程序设计DAL的好方法吗? - Is DTO plus UnitOfWork pattern a good approach to design a DAL for a web application? 使用IoC注册通用UnitOfWork <>时出错 - Error registering generic UnitOfWork<> with IoC IoC,带有Ninject和实体框架的UnitOfWork - IoC, UnitOfWork with Ninject and Entity Framework IOC在preentation层中的服务层和服务中注册存储库 - IOC to register repositories in service layer and services in the preentation layer StructureMap错误202设置IOC容器 - StructureMap Error 202 Setting up IOC container
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM