【问题标题】:Best design pattern for multiple if statements in an interface implementation接口实现中多个 if 语句的最佳设计模式
【发布时间】:2016-02-14 08:04:17
【问题描述】:

我的 c# 项目中有一个IComposer 接口:

public interface IComposer
{
    string GenerateSnippet(CodeTree tree);
}

CodeTree 是一个基类,其中包含从CodeTree 继承的类List<CodeTree>。例如:

public class VariablesDecleration : CodeTree
{
     //Impl
}

public class LoopsDecleration : CodeTree
{
     //Impl
}

我可以有几个实现 IComposer 的类,并且在每个类中我都有 GenerateSnippet 循环在 List<CodeTree> 上,基本上是这样:

foreach (CodeTree code in tree.Codes)
{
    if (code.GetType() == typeof(VariablesDecleration))
    {
        VariablesDecleration codeVariablesDecleration = (VariablesDecleration) code;
        // do class related stuff that has to do with VariablesDecleration
    }
    else if (code.GetType() == typeof(LoopsDecleration))
    {
        LoopsDecleration codeLoopsDecleration = (LoopsDecleration) code;
        // do class related stuff that has to do with LoopsDecleration
    }
}

我在每个实现IComposer 的类中重复了这个foreachif 语句。

我想知道是否有更好的设计模式来处理这种情况。假设 tommrow 我添加了一个继承自 CodeTree 的新类 - 我必须检查所有实现 IComposer 的类并修改它们。

我在考虑访问者设计模式 - 但不确定也不确定是否以及如何实现它。对于这种情况,Visitor 是否是正确的解决方案?

【问题讨论】:

  • @DarshanPatel 语法如果对我来说在这个问题上不重要。我试图理解正确的设计模式,以使代码可读和可维护,同时保持开闭原则。顺便说一句 - switch-case 不适用于类型:switch 表达式或 case 标签必须是 bool、char、string、integral、enum 或相应的可空类型
  • 访客很可能是对的。您能否提供有关 IComposer 实现的更多详细信息(类的名称、它与 CodeTree 基类的关系等)?我不清楚。
  • 有一个通用的重构策略模式,即用多态调用替换 if 语句(在类型上)。 refactoring.com/catalog/replaceConditionalWithPolymorphism.html 但是,在 Narayana 的回答中,您说“将此逻辑移至数据类型不是一种选择”——这就是为什么我们需要有关具体 Composers 的更多信息(在您的代码中)。

标签: c# design-patterns visitor-pattern


【解决方案1】:

VariablesDeclerationLoopsDecleration中移动与具体类相关的实现,在CodeTree中提供抽象实现。然后在您的循环中,在 CodeTree 中简单地调用该方法,无需 if...else 检查。

public class VariablesDecleration : CodeTree
{
    //Impl
    public void SomeStuff()
    {
        //... specific to Variables
    }
}

public class LoopsDecleration : CodeTree
{
    //Impl
    public void SomeStuff()
    {
        //... specific to Loops
    }
}

public class CodeTree : ICodeTree
{
    void SomeStuff();
}

foreach (CodeTree code in tree.Codes)
{
    code.SomeStuff();
}

根据 cmets,您可能需要这样的东西:

public interface IComposer
{
    string DoStuff();
}

public class LoopComposer1 : IComposer
{
    string DoStuff(){ .. }
}

public class VariableComposer1 : IComposer
{
    string DoStuff(){ .. }
}

public class ComposerCollection
{
    private IEnumerable<IComposer> composers;
    string GenerateSnippet()
    {
        foreach(var composer in composers)
        {
            composer.DoStuff();
        }
        ...
        ...
    }
}

当然,现在关系已经反转,你的代码树或其创建者必须为其定义作曲家集合。

【讨论】:

  • 实现取决于作曲家的类型。对于相同的 VariablesDecleration / LoopsDecleration 对象,类型 A 的作曲家将给出与作曲家 B 不同的结果 - 因此不能将此逻辑移至数据类型。 VariablesDecleration / LoopsDecleration 应该不知道对其数据做了什么。
  • CodeTree 需要抽象
  • LoopsDecleration 可能会在从 IComposer1 调用时产生结果 A,在从 IComposer2 调用时产生 B - 作曲家只需要 LoopsDecleration 持有的信息来创建结果。所以实现不能是该对象的一部分,因为它不知道谁在使用它。
  • 您可能应该使用 List 而不是 List。您创建 Composer1,它具有 LoopComposer1、VariableComposer1 等。每个都需要 LoopDeclaration 和 VariableDeclaration 以便创建/初始化。然后循环 LC1 和 VC1,并在其上调用 GenerateSnippet。只要你有一个通用列表,但你想做一些具体的事情,你就必须检查类型。
【解决方案2】:

您的问题是如何对不同类型的代码树节点执行操作,对吗?

首先声明一个名为INodeActor 的新接口,它为您提供关于代码如何作用于代码树节点的合同。它的定义看起来像这样:

public interface INodeActor
{
    bool CanAct(CodeTree treeNode);
    void Invoke(CodeTree treeNode);
}

现在您可以使用以下代码:

foreach (CodeTree code in tree.Codes)
{
    if (code.GetType() == typeof(VariablesDecleration))
    {
        VariablesDecleration codeVariablesDecleration = (VariablesDecleration) code;
        // do class related stuff that has to do with VariablesDecleration
    }
    else if (code.GetType() == typeof(LoopsDecleration))
    {
        LoopsDecleration codeLoopsDecleration = (LoopsDecleration) code;
        // do class related stuff that has to do with LoopsDecleration
    }
}

然后分解:

public class VariablesDeclerationActor : INodeActor
{
    public void CanAct(CodeTree node)
    {
        return node.GetType() == typeof(VariablesDecleration);
    }

    public void Invoke(CodeTree node)
    {
         var decl = (VariablesDecleration)node;
         // do class related stuff that has to do with VariablesDecleration
    }
}

public class LoopsDeclerationActor : INodeActor
{
    public void CanAct(CodeTree node)
    {
        return node.GetType() == typeof(LoopsDecleration);
    }

    public void Invoke(CodeTree node)
    {
         var decl = (LoopsDecleration)node;
         // do class related stuff that has to do with LoopsDecleration
    }
}

作曲家

将作曲家想象为工作协调员。它不需要知道实际工作是如何完成的。它的职责是遍历代码树并将工作委托给所有注册的参与者。

public class Composer
{
    List<INodeActor> _actors = new List<INodeActor>();

    public void AddActor(INodeActor actor)
    {
        _actors.Add(actor);
    }

    public void Process(CodeTree tree)
    {
         foreach (CodeTree node in tree.Codes)
         {
             var actors = _actors.Where(x => x.CanAct(node));
             if (!actors.Any())
                 throw new InvalidOperationException("Got no actor for " + node.GetType());

             foreach (var actor in actors)
                 actor.Invoke(node);
         }
    }
}

用法

您可以随意自定义执行,而无需更改遍历或任何现有代码。因此,代码现在遵循 SOLID 原则。

var composer = new Composer();
composer.Add(new VariablesDeclerationActor());
composer.Add(new PrintVariablesToLog());
composer.Add(new AnalyzeLoops());

如果你想构建一个结果,你可以引入一个传递给INodeActor调用方法的上下文:

public interface INodeActor
{
    bool CanAct(CodeTree treeNode);
    void Invoke(InvocationContext context);
}

上下文包含要处理的节点,可能是一个 StringBuilder 来存储结果等。将其与 ASP.NET 中的 HttpContext 进行比较。

【讨论】:

    【解决方案3】:

    您可以为所有作曲家定义一个基类并在其中实现 GenerateSnippet,避免为每个作曲家重新编写此代码。 此外,您可以通过实现 composer.DoStuff(); 来改进您的 foreach 循环;正如@Narayana 建议的那样。

    public class Composer:IComposer
    {
        string GenerateSnippet()
        {
            foreach (CodeTree code in tree.Codes)
            {
                 if (code.GetType() == typeof(VariablesDecleration))
                 {
                     VariablesDecleration codeVariablesDecleration = (VariablesDecleration) code;
                   // do class related stuff that has to do with VariablesDecleration
                 }
                 else if (code.GetType() == typeof(LoopsDecleration))
                 {
                      LoopsDecleration codeLoopsDecleration = (LoopsDecleration) code;
                     // do class related stuff that has to do with LoopsDecleration
                 }
            }
        }
    }
    
    
    public class ClassA: Composer
    {
    
    }
    
    public class ClassB: Composer
    {
    
    }
    

    【讨论】:

      【解决方案4】:

      也许依赖倒置会有所帮助?

      class Program
      {
          static void Main(string[] args)
          {
              var composer1 = new ComposerA(new Dictionary<Type, Func<CodeTree, string>>()
              {
                  { typeof(VariablesDeclaration), SomeVariableAction },
                  { typeof(LoopsDeclaration), SomeLoopAction }
              });
      
              var composer2 = new ComposerB(new Dictionary<Type, Func<CodeTree, string>>()
              {
                  { typeof(VariablesDeclaration), SomeOtherAction }
      
              });
      
              var snippet1 = composer1.GenerateSnippet(new CodeTree() {Codes = new List<CodeTree>() {new LoopsDeclaration(), new VariablesDeclaration()}});
              var snippet2 = composer2.GenerateSnippet(new CodeTree() { Codes = new List<CodeTree>() { new VariablesDeclaration() } });
      
              Debug.WriteLine(snippet1); // "Some loop action Some variable action  some composer A spice"
              Debug.WriteLine(snippet2); // "Some other action  some composer B spice"
          }
      
          static string SomeVariableAction(CodeTree tree)
          {
              return "Some variable action ";
          }
      
          static string SomeLoopAction(CodeTree tree)
          {
              return "Some loop action ";
          }
      
          static string SomeOtherAction(CodeTree tree)
          {
              return "Some other action ";
          }
      }
      
      public interface IComposer
      {
          string GenerateSnippet(CodeTree tree);
      }
      
      public class CodeTree
      {
          public List<CodeTree> Codes;
      }
      
      public class ComposerBase
      {
          protected Dictionary<Type, Func<CodeTree, string>> _actions;
      
          public ComposerBase(Dictionary<Type, Func<CodeTree, string>> actions)
          {
              _actions = actions;
          }
      
          public virtual string GenerateSnippet(CodeTree tree)
          {
              var result = "";
      
              foreach (var codeTree in tree.Codes)
              {
                  result = string.Concat(result, _actions[codeTree.GetType()](tree));
              }
      
              return result;
          }
      }
      
      public class ComposerA : ComposerBase
      {
          public ComposerA(Dictionary<Type, Func<CodeTree, string>> actions) : base(actions)
          {
          }
      
          public override string GenerateSnippet(CodeTree tree)
          {
              var result = base.GenerateSnippet(tree);
      
              return string.Concat(result, " some composer A spice");
          }
      }
      
      public class ComposerB : ComposerBase
      {
          public ComposerB(Dictionary<Type, Func<CodeTree, string>> actions) : base(actions)
          {
          }
      
          public override string GenerateSnippet(CodeTree tree)
          {
              var result = base.GenerateSnippet(tree);
      
              return string.Concat(result, " some composer B spice");
          }
      }
      
      public class VariablesDeclaration : CodeTree
      {
          //Impl
      }
      
      public class LoopsDeclaration : CodeTree
      {
          //Impl
      }
      

      【讨论】:

        【解决方案5】:

        首先,考虑将 GenerateSnippet 从其他类继承的基类中移出。 SOLID 的单一责任原则就是要这个。作曲家必须作曲,而 CodeTree 必须只做自己的工作。

        下一步是你的 if-else 块。我认为您可以使用简单的 Dictionary 来存储不同类型的 CodeTree 项目:

        public interface IComposer {
            string GenerateSnippet(List<CodeTree> trees);
            void RegisterCodeTreeType<T>(T codeType) where T:CodeTree;
        }
        
        public abstract class ComposerBase {
            private readonly Dictionary<Type, CodeTree> _dictionary;
        
            public ComposerBase() {
                _dictionary = new Dictionary<Type, CodeTree>();
            }
        
            public void RegisterCodeTreeType<T>(T codeType) where T:CodeTree {
               _dicionary.Add(typeof(T), codeType);
            }
        
            public string GenerateSnippet(List<CodeTree> trees) {
                StringBuilder fullCode = new StringBuilder();
                foreach(var tree in trees) {
                    fullCode.Append(_dictionary[tree.GetType()].GenerateSnippet();
                }
            }
        }
        

        希望你能想到一个主意。您应该在应用程序启动时使用 Composer RegisterCodeTreeType 方法注册所有类型。现在它不取决于你有多少类型。请注意,这只是一个快速代码,请谨慎使用。

        【讨论】:

          【解决方案6】:

          好的,正如您所意识到的那样,必须编写那些关于类型检查的 If 语句很糟糕,并且它违背了您已经做过的抽象和子类化的目的。

          您希望呼叫代码保持不变。 (OCP)

          现在 CodeTree 对象负责找出每个具体实现的逻辑。 问题是责任属于每个具体的。你的 for 循环应该只是转换为接口类型IComposer 并调用方法string GenerateSnippet(CodeTree tree); 来获得结果。每个具体应处理实现细节。 而不是循环通过 CodeTree 对象,您应该循环通过 IComposer 对象。

          将实现移动到特定类型对象不需要您对执行代码进行更改,而只需在发生时添加新类型进行扩展。 如果您的实现细节有很大不同,您可以查看 Type Object Pattern。它将处理所有细节,并且您的 CodeTree 对象可以保持更简单。

          【讨论】:

            猜你喜欢
            • 2017-03-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-07-17
            • 1970-01-01
            • 2018-05-02
            • 2021-10-31
            • 2021-10-24
            相关资源
            最近更新 更多