【问题标题】:Design pattern - Client Server - Command pattern设计模式 - 客户端服务器 - 命令模式
【发布时间】:2017-04-30 16:23:37
【问题描述】:

我有一堆命令需要从客户端批处理并在服务器上执行。这些命令具有不同的类型,命令的契约和相应的返回类型通过库在客户端和服务器之间共享。

客户端代码如下-

var client = new ClientSDK();
client.Add(new Command1());
client.Add(new Command2());
client.Add(new Command3());

// Execute transmits all the commands to the server 
var results = client.Execute();

服务器代码 -

List<CommandResult> Execute(List<CommandBase> commands)
{
   List<CommandResult>  results = new List<CommandResult>();
   foreach(CommandBase command in commands)
   {
       if(command.GetType == Command1)
       {
           results.Add(new Command1Executor(command).Execute())
       }
       else if(command.GetType == Command2)
       {
           results.Add(new Command1Executor(command).Execute())
       }
       else if(command.GetType == Command3)
       {
           results.Add(new Command3Executor(command).Execute())
       }    
       ..................     
   }
}

对于每个命令,都有一个独特的执行函数,不能作为客户端 SDK 的一部分公开。如何进行设计更改,以便摆脱大量的 if/else 块?有大量的命令需要支持。我尝试按照这里的建议应用命令模式 - using the command and factory design patterns for executing queued jobs 但这需要每个命令都实现 ICommand 接口,这是不可能的

有更好的设计方法吗?

【问题讨论】:

  • 将所有命令执行器(或创建它们的函数)放入字典中,按命令类型键入。这样你就不需要 if else 链。

标签: c# design-patterns client-server factory-pattern command-pattern


【解决方案1】:

基本上,您需要将CommandNExcecutor 类型映射到CommandN 类型。

1) 使用字典。这是最简单的方法:

private static readonly Dictionary<Type, Type> map = new Dictionary<Type, Type>
{
    {  typeof(Command1), typeof(Command1Executor) },
    {  typeof(Command2), typeof(Command2Executor) },
    ...
};

List<CommandResult> Execute(List<CommandBase> commands)
{
    return commands
        .Select(command =>
        {
            var executor = Activator.CreateInstance(map[command.GetType], command);
            return executor.Execute();
        })
        .ToList();
}

2) 使用元数据(属性)。这适合基于插件的场景,当命令类型可以动态添加时,无需重建核心功能。它可以是您自己的实现,也可以是现有的 DI 容器实现(其中许多公开了元数据 API)。

[AttributeUsage(AttributeTargets.Class)]
public sealed class CommandExecutorAttribute : Attribute
{
    public CommandExecutorAttribute(Type commandType)
    {
        CommandType = commandType;
    }

    public Type CommandType { get; }

    // ...
}

[CommandExecutor(typeof(Command1))]
public sealed class Command1Executor : ICommandExecutor
{
    // ...
}

List<CommandResult> Execute(List<CommandBase> commands)
{
    return commands
        .Select(command =>
        {
            // obtain executor types somehow, e.g. using DI-container or
            // using reflection;
            // inspect custom attribute, which matches command type
            var executorType = ....

            var executor = Activator.CreateInstance(executorType , command);
            return executor.Execute();
        })
        .ToList();
}

更新

如果你想避免反射,在第一种情况下,只需将字典中值的类型参数替换为Func&lt;CommandBase, ICommandExecutor&gt;

private static readonly Dictionary<Type, Func<ICommandExecutor>> map = new Dictionary<Type, Func<ICommandExecutor>>
    {
        {  typeof(Command1), command => new Command1Executor(command) },
        {  typeof(Command2), command => new Command2Executor(command) },
        ...
    };

这将允许您通过委托而不是反射来创建执行器:

var executor = map[command.GetType](command);

第二种情况不能完全避免反射,因为您需要以某种方式获取执行器类型。但它可以导致案例1(带字典)。

让懒惰的map:

private static readonly Lazy<Dictionary<Type, ConstructorInfo>> map = ...

然后,在Lazy&lt;T&gt; 初始化时,做所有反射工作。由于这是static Lazy&lt;T&gt;,因此您将为每个应用程序域执行一次。调用ConstructorInfo 已经足够快了。因此,在处理第一个命令时,您只会受到一次性能影响。

案例 2 的另一个选项(他们都假设 Lazy&lt;Dictionary&gt;)是:

  • 不是反映ConstructorInfo,而是使用ConstructorInfo 构建Expression&lt;Func&lt;CommandBase, ICommandExecutor&gt;&gt;s,编译它们并将委托放入字典 - 这将与案例 1 的委托相同,但具有动态支持的命令类型;
  • 使用 DI 容器,它发出 IL 来构建依赖项(AFAIK,NInject 这样做);
  • 自己发出 IL(IMO,这将完全重新发明*)。

最后,用最简单的方法解决这个问题,然后衡量性能,然后考虑更复杂的方法。避免过早优化。我对你的命令性质一无所知,但我怀疑,命令执行的时间比反映某些东西要长得多(当然,有可能,我错了)。

希望这会有所帮助。

【讨论】:

  • 绝对要避免Activator.CreateInstance和Reflection,因为性能是优先考虑的。
  • @user1542794:实际上,反射并不是答案的重点。但我已经对其进行了更新,为您提供了一些如何加快代码速度的想法。
  • 感谢丹尼斯,您的解决方案非常干净地解决了问题。
【解决方案2】:

尝试使用策略模式。一个简单的解决方案如下:

public class CommandStrategy 
{
    private static Dictionary<CommandTypes, Action<CommandStrategy>> strategy;

    public CommandStrategy()
    {
        strategy = new Dictionary<CommandTypes, Action<CommandStrategy>>();
        strategy.Add(CommandTypes.Command1, one => new Command1Executor().Execute());
        strategy.Add(CommandTypes.Command2, two => new Command2Executor().Execute());
        strategy.Add(CommandTypes.Command3, two => new Command3Executor().Execute());
    }

    public void Execute(CommandTypes type)
    {
        strategy[type].Invoke(this);
    }
}

最终的执行可能如下所示:

CommandStrategy strategy = new CommandStrategy();

List<CommandBase> commands = new List<CommandBase>(){
                  new Command1(), new Command2(), new Command3() };

foreach (var item in commands)
{
   CommandTypes type = (CommandTypes)item;
   strategy.Execute(type);
}

【讨论】:

  • 我尝试使用该模式,但遇到了其他问题。一旦我使用 NewtonSoft.Json 库序列化命令,在服务器端反序列化就会出现问题。尝试几种解决方案,让您随时了解情况。