我有以下命令处理程序。处理程序接受命令对象并使用其属性来创建或更新实体。
它由可以为空的命令对象上的Id
属性决定。如果为null,则创建,如果不是,则更新。
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
上的单个依赖项。
唯一可能的问题是,根据Command上的Id字段是否为空,这需要从IoC容器中进行某种条件注入。
我正在使用SimpleInjector,并且不确定如何执行此操作的语法,或者即使它可以完成。
这是通过IoC这样做也是一种气味,还是处理者有责任这样做?
是否还有其他模式或方法可以解决此问题?我原以为可能会使用一个装饰器,但我真的不能想到如何这样做。
答案 0 :(得分:6)
我的经验是,使用两个单独的命令(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操作将始终导致解决方案看起来不像基于任务的操作那样干净。这是推动开发人员并要求工程师考虑他们想要执行的任务的众多原因之一。这样可以获得更好的用户界面,更高的用户体验,更具表现力的审计线索,更舒适的设计以及更好的整体软件但是你的应用程序的某些部分将始终是CRUDy;无论你做什么。
答案 1 :(得分:0)
我认为,你可以将这个命令分成两个定义明确的命令,例如: CreateCategory
和UpdateCategory
(当然您应该选择最合适的名称)。另外,通过Template Method design pattern设计两个命令。在基类中,您可以为类别创建定义受保护的抽象方法,并在&#39; Handle&#39;您应该调用此受保护方法的方法,之后您可以处理原始&#39; Handle&#39;的剩余逻辑。方法:
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
方法。