31

16. 设计模式-策略模式

 3 years ago
source link: https://segmentfault.com/a/1190000023950160
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

示例

策略模式是我们工作中比较常用的一个设计模式,但是初次理解起来可能会有点困难,因此我们还是先看一个例子,假设现在需要开发一个画图工具,画图工具中有钢笔,笔刷和油漆桶,其中,钢笔可以用于描边,但不能填充,笔刷既可以描边,也可以填充,油漆桶只能用于填充,但不能描边。

看到这个需求,最容易想到的可能就是通过继承方式实现了,即钢笔,笔刷和油漆桶都继承自画图工具,然后都实现描边和填充功能,只不过钢笔的填充方法什么都不做,油漆桶的描边方法也什么都不做。(该部分在设计原则中有示例代码,是当时的一个遗留问题)

但是仔细一想,好像哪里不对劲,因为钢笔和油漆桶部分方法不实现,很明显违背了里氏替换原则。而且,正常情况应该是画图工具有用钢笔,笔刷,油漆桶画图的能力,而不是钢笔,笔刷,油漆桶继承自画图工具。因此,我们可以如下实现:

public class Graphics
{
    public void Stroke(ToolEnum tool)
    {
        switch (tool)
        {
            case ToolEnum.Pen:
                Console.WriteLine($"用钢笔描边图形");
                break;
            case ToolEnum.Brush:
                Console.WriteLine($"用笔刷描边图形");
                break;
            case ToolEnum.Bucket:
                Console.WriteLine("油漆桶不能描边图形");
                break;
            default:
                throw new NotSupportedException("不支持的画图工具");
        }
    }
        
    public void Fill(ToolEnum tool)
    {
        switch (tool)
        {
            case ToolEnum.Pen:
                Console.WriteLine($"钢笔不能填充图形");
                break;
            case ToolEnum.Brush:
                Console.WriteLine($"用笔刷填充图形");
                break;
            case ToolEnum.Bucket:
                Console.WriteLine("用油漆桶填充图形");
                break;
            default:
                throw new NotSupportedException("不支持的画图工具");
        }
    }
}

通过上面枚举的方式,我们实现了让画图工具具备描边和填充的能力,但是这样的 switch-case (或者 if-else )给扩展和维护都带来了很大的麻烦,而且通过前面对其他设计模式的学习,我相信大家看到这样的代码,一定是不能接受的。起码应该将 PenBrushBucket 定义成类并且继承自同一个基类,然后组合到 Graphics 中来,而不是直接使用条件判断,因为用组合代替继承是我们学习设计模式过程中百试不爽的经验。但是,这次好像不怎么灵了,因为,一旦这样做,我们就又回到了原点---钢笔和油漆桶不得不实现不需要的方法。事实上,用组合替代继承是没有错的,但是该怎么组合?组合谁呢?这是个问题。感觉瞬间陷入了两难的局面,这时候策略模式就派上用场了,它不抽象钢笔、笔刷、油漆桶等具体事物,而是直接抽象描边和填充这两种能力,站在代码的角度上看,就是将方法封装成了对象(正应了那句一切皆对象),这正是策略模式最让人费解,但又最妙不可言的地方。我们直接看看用策略模式改进后的代码是怎样的,先抽象能力:

public interface IStrokeStrategy
{
    void Stroke();
}

public class PenStrokeStrategy : IStrokeStrategy
{
    public void Stroke()
    {
        Console.WriteLine($"用钢笔描边图形");
    }
}

public class BrushStrokeStrategy : IStrokeStrategy
{
    public void Stroke()
    {
        Console.WriteLine($"用笔刷描边图形");
    }
}

public interface IFillStrategy
{
    void Fill();
}

public class BrushFillStrategy : IFillStrategy
{
    public void Fill()
    {
        Console.WriteLine($"用笔刷填充图形");
    }
}

public class BucketFillStrategy : IFillStrategy
{
    public void Fill()
    {
        Console.WriteLine("用油漆桶填充图形");
    }
}

看到了吗?直接将 StrokeFill 两种能力定义成了接口,再通过不同的子类去实现这种能力。然后再看看 Graphics 类如何组合:

public class Graphics
{
    private IStrokeStrategy _strokeStrategy;
    private IFillStrategy _fillStrategy;
        
    public Graphics(IStrokeStrategy strokeStrategy,
                    IFillStrategy fillStrategy)
    {
        this._strokeStrategy = strokeStrategy;
        this._fillStrategy = fillStrategy;
    }

    public void Stroke()
    {
        this._strokeStrategy.Stroke();
    }
        
    public void Fill()
    {
        this._fillStrategy.Fill();
    }
}

画图工具直接拥有了两种能力,但是跟钢笔、笔刷、油漆桶没有直接关系,也就是说,只要给画图工具一个填充的工具,就可以完成填充功能了,至于给的具体是笔刷还是油漆桶,或者其他什么东西,画图工具并不关心。而且, Graphics 类中的条件判断语句也都去掉了,隔离了变化,整个类都变得稳定了。

如下就是通过策略模式实现的类图:

6JRraaE.png!mobile

定义

再来看一下定义,策略模式定义一系列算法,把他们一个个封装起来,并且使他们可以互相替换。该模式使得算法可以独立于使用它的客户程序而变化。

这里的一系列算法就是不同的描边和填充方式了。不同的描边方式可以相互替换,不同的填充方式也可以相互替换,并且也可以方便的扩展更多的描边和填充方式子类。

UML类图

上面的例子中用到了两组算法,抽象简化之后就得到了如下策略模式的UML类图:

rYbU7v.png!mobile

  • Context :策略上下文,持有 IStrategy 的引用,负责和具体的策略实现交互;
  • IStrategy :策略接口,约束一系列具体的策略算法;
  • ConcreteStrategy :具体的策略实现。

优点

  • 策略可以互相替换;
  • 解决 switch-caseif-else 带来的难以维护的问题;
  • 策略易于扩展,满足开闭原则.

缺点

  • 客户端必须知道每一个策略类,增加了使用难度。
  • 随着策略的扩展,策略类数量会增多;

第一个缺点无法避免,因为策略模式的一大优点就是算法可以相互替换,但是如果使用者连每个算法代表的是什么意思,优缺点是什么都不知道,又如何替换呢?但第二个缺点却可以通过结合工厂模式,由工厂模式创建具体的策略子类来进行一定程度的缓解,至于具体该怎么实现,这就是工厂模式的知识了,大家可以自行回忆一下。

应用场景

在业务场景中,商家促销活动就非常适合用到策略模式了,因为,商家促销打折可能存在会员折扣,节日折扣,生日折扣等等几十种方式,而且在不同的条件下可以相互替换。

而非业务场景中,日志框架中我们可能会使用 log4Net , NLog , Serilog 等,而记录位置也可能是控制台,文件,数据库等;系统中的缓存,可以用 Redis 做分布式缓存,也可以用 MemeryCache 做本地缓存等,这些场景也都非常适合使用策略模式。

总之,策略模式简约但不简单,学好它妙用无穷!

源码链接

更多内容,欢迎关注公众号:

BRvQrmy.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK