代码之家  ›  专栏  ›  技术社区  ›  Graham

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

  •  4
  • Graham  · 技术社区  · 10 年前

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

    这是由 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连接起来的。

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

    其中一个例子是,对于实际上是同一个命令的命令,必须有两个验证修饰符(因为它们的区别在于只有一个Id属性)。装饰器确实实现了继承,但当有很多命令需要处理时,这仍然是一个难题。

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

    一个是注入的依赖项的数量。另一个原因是课堂上有很多内容。这个 if 三元让我很烦恼,它似乎有点代码味。

    一个选项是将某种帮助类注入处理程序。这可以实现某种 ICategoryHelper 与混凝土的界面 Create 和 Update 实现。这意味着 ICategoryRepository 和 ITracker 依赖项可以替换为对 ICategoryHelper(类别助手) .

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

    我使用的是SimpleInjector,我不确定如何实现这一点的语法,甚至不确定是否可以实现。

    这是通过IoC来做这件事也是一种气味吗,还是应该由处理人员负责做这件事情?

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

    2 回复  |  直到 10 年前
        1
  •  6
  •   Steven    10 年前

    我的经验是,有两个独立的命令( SaveCategoryCommand 和 UpdateCategoryCommand )使用一个命令处理程序可以得到最好的结果(尽管有时两个单独的命令处理程序也可以)。

    命令不应继承自 CategoryCommandBase 基类,但两个命令共享的数据应该提取到一个DTO类,该类作为两个类的属性公开(继承上的组合)。命令处理程序应该实现两个接口,这允许它包含共享功能。

    [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、更高的用户体验、更具表现力的审计跟踪、更令人愉悦的设计和更好的整体软件。但你的应用程序的某些部分永远是粗制滥造的;不管你做什么。

        2
  •  0
  •   Sattar Imamov    10 年前

    我认为,您可以将此命令拆分为两个定义良好的命令,例如。 CreateCategory 和 UpdateCategory (当然你应该选择最合适的名字)。此外,通过 Template Method design pattern 。在基类中,您可以为类别创建定义受保护的抽象方法,在“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);
    } 
    

    在派生类中,您只需重写 LoadCategory 方法