重构 C# 保存命令处理程序

Refactoring a C# Save Command Handler

我有以下命令处理程序。处理程序采用命令对象并使用其属性来创建或更新实体。

它由可为空的命令对象上的Id 属性 决定。如果为空,则创建,如果不为空,则更新。

public class SaveCategoryCommandHandler : ICommandHandler<SaveCategoryCommand>
{
    public SaveCategoryCommandHandler(
        ICategoryRepository<Category> categoryRepository,
        ITracker<User> tracker,
        IMapProcessor mapProcessor,
        IUnitOfWork unitOfWork,
        IPostCommitRegistrator registrator)
    {
         // Private fields are set up. The definitions for the fields have been removed for brevity.
    }

    public override void Handle(SaveCategoryCommand command)
    {
        // The only thing here that is important to the question is the below ternary operator.

            var category = command.Id.HasValue ? GetForUpdate(command) : Create(command);

 // Below code is not important to the question. It is common to both create and update operations though.

            MapProcessor.Map(command, category);

            UnitOfWork.Commit();

            Registrator.Committed += () =>
            {
                command.Id = category.Id;
            };

    }

    private Category GetForUpdate(SaveCategoryCommand command)
    {
        // Category is retrieved and tracking information added
    }

    private Category Create(SaveCategoryCommand command)
    {
        // Category is created via the ICategoryRepository and some other stuff happens too.
    }
}

我曾经有两个处理程序,一个用于创建,一个用于更新,以及两个用于创建和更新的命令。一切都使用 IoC 连接起来。

在重构为一个 class 以减少代码重复量后,我最终得到了上述处理程序 class。重构的另一个动机是避免使用两个命令(UpdateCategoryCommand 和 CreateCategoryCommand),这会导致更多的验证和类似重复。

这方面的一个例子是必须有两个验证装饰器来处理实际上相同的命令(因为它们的不同之处在于只有一个 Id 属性)。装饰器确实实现了继承,但是当有很多命令要处理时仍然很痛苦。

关于重构的处理程序,有几件事让我感到困扰。

一个是被注入的依赖项的数量。另一个是 class 上发生了很多事情。 if 三元让我很困扰——它似乎有点代码味道。

一种选择是将某种帮助程序 class 注入处理程序。这可以用具体的 CreateUpdate 实现实现某种 ICategoryHelper 接口。这意味着 ICategoryRepositoryITracker 依赖项可以替换为对 ICategoryHelper.

的单一依赖项

唯一的潜在问题是,这需要根据命令上的 Id 字段是否为空,从 IoC 容器进行某种条件注入。

我正在使用 SimpleInjector 并且不确定如何执行此操作的语法,甚至不确定是否可以完成。

这是通过 IoC 执行此操作也是一种气味,还是应该由处理程序负责执行此操作?

有没有其他模式或方法可以解决这个问题?我原以为可以使用装饰器,但我真的想不出该怎么做。

我认为,您可以将此命令分成两个定义明确的命令,例如CreateCategoryUpdateCategory(当然你应该选择最合适的名字)。此外,通过 Template Method design pattern 设计这两个命令。在基础 class 中,您可以为类别创建定义受保护的抽象方法,在 'Handle' 方法中,您应该调用此受保护的方法,之后您可以处理原始 'Handle' 方法的剩余逻辑:

public abstract class %YOUR_NAME%CategoryBaseCommandHandler<T> : ICommandHandler<T>
{
    public override void Handle(T command)
    {
        var category = LoadCategory(command);
        MapProcessor.Map(command, category);

        UnitOfWork.Commit();

        Registrator.Committed += () =>
        {
            command.Id = category.Id;
        };
    }

    protected abstract Category LoadCategory(T command);
} 

在派生的 classes 中,您只需覆盖 LoadCategory 方法。

我的经验是,将两个单独的命令(SaveCategoryCommandUpdateCategoryCommand)与一个命令处理程序一起使用会产生最佳结果(尽管两个单独的命令处理程序有时也可以)。

这些命令不应从 CategoryCommandBase 基础 class 继承,而是应该将两个命令共享的数据提取到 DTO class 中,该 DTO class 公开为 属性 在两个 classes 上(组合继承)。命令处理程序应实现这两个接口,这允许它包含共享功能。

[Permission(Permissions.CreateCategories)]
class SaveCategory {
    [Required, ValidateObject]
    public CategoryData Data;

    // Assuming name can't be changed after creation
    [Required, StringLength(50)]
    public string Name;
}

[Permission(Permissions.ManageCategories)]
class UpdateCategory {
    [NonEmptyGuid]
    public Guid CategoryId;

    [Required, ValidateObject]
    public CategoryData Data;
}

class CategoryData {
    [NonEmptyGuid]
    public Guid CategoryTypeId;
    [Required, StringLength(250)]
    public string Description;
}

有两个命令效果最好,因为当每个操作都有自己的命令时,可以更轻松地记录它们,并允许它们授予不同的权限(例如使用属性,如上所示)。共享数据对象效果最好,因为它允许您在命令处理程序中传递它并允许视图绑定到它。而且继承几乎总是丑陋的。

class CategoryCommandHandler :
    ICommandHandler<SaveCategory>,
    ICommandHandler<UpdateCategory> {
    public CategoryCommandHandler() { }

    public void Handle(SaveCategory command) {
        var c = new Category { Name = command.Name };
        UpdateCategory(c, command.Data);
    }

    public void Handle(UpdateCategory command) {
        var c = this.repository.GetById(command.CategoryId);
        UpdateCategory(c, command.Data);
    }

    private void UpdateCategory(Category cat, CategoryData data) {
        cat.CategoryTypeId = data.CategoryDataId;
        cat.Description = data.Description;
    }
}

请注意,CRUDy 操作总是会产生看起来不像基于任务的操作那样干净的解决方案。这是我推动开发人员和要求工程师考虑他们想要执行的任务的众多原因之一。这会带来更好的 UI、更好的用户体验、更具表现力的审计跟踪、更令人愉快的设计以及更好的整体软件。但是您的应用程序的某些部分将始终是 CRUDy;不管你做什么。