命令模式的实现DRY打破了单一责任原则;开闭原理

本文关键字:原则 责任 单一 模式 实现 DRY 命令 | 更新日期: 2023-09-27 17:59:38

我目前正在为我正在设计的服务实现命令处理程序模式,其中命令本质上是处理程序的.Handle()方法的DTO。当我开始实现各种具体的类时,我意识到为了满足开放/封闭原则和单一责任原则,我可能最终会创建数千个命令和处理程序类,这将严重违反Don't Repeat Yourself原则。

例如,我封装的部分过程需要通过ProjectId从60多个表中删除所有数据以重置它们。如果我将每一个实现为原子具体的Command对象和具体的CommandHandler对象,那么我将有120个类用于第一步他们都将完全遵循SRP&OCP,然而DRY需要一个严重的打击

public class DeleteProjectLogCommand : CommandBase
{
    public long? ProjectId { get; set; }
}
public class DeleteProjectLogCommandHandler : ICommandHandler<DeleteProjectLogCommand>
{
    public async Task<Feedback<DeleteProjectLogCommand>> Handle(DeleteProjectLogCommand command, CancellationToken token)
    {
        // ...
    }
}

或者,我可以实现一个单一的、多用途的命令和处理程序类,并且可以使用ProjectTables枚举来代替所有离散类。

public class DeleteTableByProjectIdCommand : CommandBase
{
    public DeleteTableByProjectIdCommand(ProjectTables table, long? projectId) {}
    public long? ProjectId { get; set; }        
    public ProjectTables Table { get; set; }
}
public class DeleteTableByProjectIdCommandHandler : ICommandHandler<DeleteTableByProjectIdCommand>
{
    public async Task<Feedback<DeleteTableByProjectIdCommand>> Handle(DeleteTableByProjectIdCommand command, CancellationToken token)
    {
        switch(command.Table)
        {
            case ProjectTables.ProjectLog:
                // x60 tables
                break;
        }
    }
}

然而,这将违反开放/关闭原则,因为如果添加新表,则枚举和使用它的每个地方都必须更新。更不用说你从60个案例的switch语句中得到的气味了。

Sooo。。。谁赢了?DRY或SRP&OCP?

命令模式的实现DRY打破了单一责任原则;开闭原理

不要太拘泥于缩写词。集中精力编写感觉正确的代码。原子命令是一个非常好的主意,但您需要正确的粒度级别——我通常认为命令是一种完整的(用户)操作。

你的枚举和God开关的设计没有通过基本的健全性测试,并且在不修改类本身的情况下是不可扩展的,所以它一定很糟糕,对吧?

考虑使用RelayCommand:http://msdn.microsoft.com/en-us/magazine/dn237302.aspx

这是一个实现ICommand的命令,但希望为实际工作注入一个委托。许多MVVM框架包括开箱即用的RelayCommand(或DelegateCommand)。

因此,您实现了您的命令界面,并要求Action或Action<T> 注射到ctor中。执行会触发操作。如果您需要向操作传递一些东西,您可以使用"T"版本,或者将其包含在您传递的委托中。

这允许您:

  • 有一个单一的Command实现(或者2,如果您支持泛型)
  • 将实际的命令逻辑放在其他地方(比如域对象中)
  • 如果有意义的话,命令逻辑实际上可以是域类的Private成员,因为您传递了委托而由命令公开

示例:

public class SomeViewModelOrDomainClass
{
  public ICommand DoItCommand {get; private set;}
  //ctor
  public SomeViewModelOrDomainClass()
  {
    // if your command includes a CanExecute bool, then also demand a Predicate to handle CanExecute
    this.DoItCommand = new RelayCommand(() => this.SomePrivateMethod(maybeEvenAnEnclosedParam), aCanExecutePredicate);
  }
}

数百个命令和处理程序既不违反DRY也不违反OCP,因为一个命令包含特定用例的顺序,即每个命令和处理过程都在实现一个业务用例。您有相同的业务用例吗?

举个例子,我开发了一个具有不同资源类型的应用程序。但我只有一个DeleteCommand,看起来像这个

public class DeleteResource:AbstractCommand
{
    public Guid ResourceId;
    public ResourceType Type;
}
 public class DeleteResourceHandler:IExecute<DeleteResource>
{        
    private IDispatchMessages _bus;
    private IStoreResources _repo;
    public DeleteResourceHandler(IStoreResources repo, IDispatchMessages bus)
    {
        _repo = repo;
        _bus = bus;
    }
    public void Execute(DeleteResource cmd)
    {
        _repo.Delete(cmd.ResourceId);
        var evnt = cmd.ToEvent();
        _bus.Publish(evnt); 
    }
}

当然,这并不是故事的全部,因为整个应用程序是为使用N资源类型而设计的,这意味着我的实体存储主要是一个键值存储,不关心实体结构。删除只需要一个id。

一旦删除了资源,就会发布一个事件,由读取模型更新程序处理,然后删除该资源类型的查询数据。

添加新资源时,我不需要触摸DeleteCommand或DeleteHandler,甚至不需要触摸实体存储。但您可以看到,命令和处理程序并不是单独工作的,它们使用其他组件来实现DRY和OCP。OCP是一个有点模糊的原理。