没有“switch”语句的策略模式?

52

我最近阅读了有关策略模式的文献,并且有一个问题。我编写了下面这个非常基础的控制台应用程序来解释我的问题。

我看到在实现策略模式时,使用'switch'语句是一个警示信号。但是,在这个例子中,我似乎无法避免使用'switch'语句。我错过了什么吗?我已经成功地将逻辑从Pencil中删除,但是现在我的Main中有了一个switch语句。我知道我可以轻松创建一个新的TriangleDrawer类,并且不必打开Pencil类,这很好。然而,我需要打开Main,以便它知道要传递哪种类型的IDrawerPencil。如果我依赖用户输入,这就是必须要做的吗?如果有一种方法可以避免使用switch语句,我很乐意尝试!

class Program
{
    public class Pencil
    {
        private IDraw drawer;

        public Pencil(IDraw iDrawer)
        {
            drawer = iDrawer;
        }

        public void Draw()
        {
            drawer.Draw();
        }
    }

    public interface IDraw
    {
        void Draw();
    }

    public class CircleDrawer : IDraw
    {
        public void Draw()
        {
            Console.Write("()\n");
        }
    }

    public class SquareDrawer : IDraw
    {
        public void Draw()
        {
            Console.WriteLine("[]\n");
        }
    }

    static void Main(string[] args)
    {
        Console.WriteLine("What would you like to draw? 1:Circle or 2:Sqaure");

        int input;
        if (int.TryParse(Console.ReadLine(), out input))
        {
            Pencil pencil = null;

            switch (input)
            {
                case 1:
                    pencil = new Pencil(new CircleDrawer());
                    break;
                case 2:
                    pencil = new Pencil(new SquareDrawer());
                    break;
                default:
                    return;
            }

            pencil.Draw();

            Console.WriteLine("Press any key to exit...");
            Console.ReadKey();
        }
    }
}

下面展示已实现的解决方案(感谢所有回答者!) 这个解决方案让我达到了一个点,即我只需要创建一个新的IDraw对象就可以使用它。

public class Pencil
    {
        private IDraw drawer;

        public Pencil(IDraw iDrawer)
        {
            drawer = iDrawer;
        }

        public void Draw()
        {
            drawer.Draw();
        }
    }

    public interface IDraw
    {
        int ID { get; }
        void Draw();
    }

    public class CircleDrawer : IDraw
    {

        public void Draw()
        {
            Console.Write("()\n");
        }

        public int ID
        {
            get { return 1; }
        }
    }

    public class SquareDrawer : IDraw
    {
        public void Draw()
        {
            Console.WriteLine("[]\n");
        }

        public int ID
        {
            get { return 2; }
        }
    }

    public static class DrawingBuilderFactor
    {
        private static List<IDraw> drawers = new List<IDraw>();

        public static IDraw GetDrawer(int drawerId)
        {
            if (drawers.Count == 0)
            {
                drawers =  Assembly.GetExecutingAssembly()
                                   .GetTypes()
                                   .Where(type => typeof(IDraw).IsAssignableFrom(type) && type.IsClass)
                                   .Select(type => Activator.CreateInstance(type))
                                   .Cast<IDraw>()
                                   .ToList();
            }

            return drawers.Where(drawer => drawer.ID == drawerId).FirstOrDefault();
        }
    }

    static void Main(string[] args)
    {
        int input = 1;

        while (input != 0)
        {
            Console.WriteLine("What would you like to draw? 1:Circle or 2:Sqaure");

            if (int.TryParse(Console.ReadLine(), out input))
            {
                Pencil pencil = null;

                IDraw drawer = DrawingBuilderFactor.GetDrawer(input);

                pencil = new Pencil(drawer); 
                pencil.Draw();
            }
        }
    }

1
引起后续开闭原则违反的switch语句是不好的。策略模式有助于将switch语句与您希望保持封闭的位置分离,但仍然需要在某个地方处理选择策略/实现,无论是switch语句、if/else/if还是使用LINQ Where(这是我最喜欢的 :-))。顺便说一下,策略模式还通过允许您轻松模拟策略实现来帮助单元测试。 - kimsk
5个回答

64

策略模式并不是一个神奇的反开关解决方案。它的作用是将代码模块化,使得业务逻辑与大型开关混杂在一起的维护噩梦得以分离。

  • 你的业务逻辑被隔离出来,便于扩展
  • 你有多种创建具体类的选项(比如工厂模式)
  • 你的基础设施代码(main 函数)可以非常干净,没有混杂的代码

例如,如果你将 main 函数中的开关移动到一个类中,该类接受命令行参数并返回 IDraw 接口的实例(即对开关进行了封装),则你的 main 函数就变得清晰明了,而开关将被放置在一个唯一目的是实现选择的类中。


19
+1 - 我总觉得策略和工厂是相辅相成的。 - NG.
感谢您帮助我理解策略模式的是/不是。我已经编辑了我的帖子,展示了我最终是如何实现它的。 - JSprang
1
@Brabster,有时我发现很难抓住适当的时机,从switch语句转换到策略模式(pun intended)。例如,如果您有30个非常简单的switch-case,而在采用策略模式的情况下,这些switch-case将变成额外的30个类——这是切换的好理由,还是最好保留这些不太干净的代码片段呢? - mmmm
1
@mmmm 我倾向于等到必须要改变开关到策略时才进行更改。如果你发现自己不断地需要在两个地方进行更改,而且你经常忘记或者这对你或你正在与之合作的人来说都不方便,那么花点时间重构成策略可能是值得的。 - brabster

16

以下是一个过度设计的解决方案,纯粹为了避免使用if/switch语句。

CircleFactory: IDrawFactory
{
  string Key { get; }
  IDraw Create();
}

TriangleFactory: IDrawFactory
{
  string Key { get; }
  IDraw Create();
}

DrawFactory
{
   List<IDrawFactory> Factories { get; }
   IDraw Create(string key)
   {
      var factory = Factories.FirstOrDefault(f=>f.Key.Equals(key));
      if (factory == null)
          throw new ArgumentException();
      return factory.Create();
   }
}

void Main()
{
    DrawFactory factory = new DrawFactory();
    factory.Create("circle");
}

16

我认为你在示例应用程序中的此处切换实际上并不是策略模式本身的一部分,它只是用于运用你定义的两种不同策略。

"在策略模式中使用开关是一个警示信号"指的是在策略内部使用开关;例如,如果你定义了一个“通用绘图器”策略,并使用一个开关根据参数值来确定用户需要的是“正方形绘图器”还是“圆形绘图器”,那么你将无法获得策略模式的好处。


14

你还可以通过使用字典来消除if

Dictionary<string, Func<IDraw> factory> drawFactories = new Dictionary<string, Func<IDraw> factory>() { {"circle", f=> new CircleDraw()}, {"square", f=> new SquareDraw()}}();

Func<IDraw> factory;
drawFactories.TryGetValue("circle", out factory);

IDraw draw = factory();

我喜欢这个!我正在扩展它,从我的依赖注入容器中填充工厂字典,注册多个工厂接口的命名实现。 - Will

5

虽然有些晚了,但对于仍然有兴趣完全删除条件语句的人。

     class Program
     {
        Lazy<Dictionary<Enum, Func<IStrategy>>> dictionary = new Lazy<Dictionary<Enum, Func<IStrategy>>>(
            () =>
                new Dictionary<Enum, Func<IStrategy>>()
                {
                    { Enum.StrategyA,  () => { return new StrategyA(); } },
                    { Enum.StrategyB,  () => { return new StrategyB(); } }
                }
            );

        IStrategy _strategy;

        IStrategy Client(Enum enu)
        {
            Func<IStrategy> _func
            if (dictionary.Value.TryGetValue(enu, out _func ))
            {
                _strategy = _func.Invoke();
            }

            return _strategy ?? default(IStrategy);
        }

        static void Main(string[] args)
        {
            Program p = new Program();

            var x = p.Client(Enum.StrategyB);
            x.Create();
        }
    }

    public enum Enum : int
    {
        StrategyA = 1,
        StrategyB = 2
    }

    public interface IStrategy
    {
        void Create();
    }
    public class StrategyA : IStrategy
    {
        public void Create()
        {
            Console.WriteLine("A");
        }
    }
    public class StrategyB : IStrategy
    {
        public void Create()
        {
            Console.WriteLine("B");
        }
    }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接