首页 > 技术文章 > 从鸭子身上学到的策略模式

xiaozhang9 2017-01-17 01:11 原文

很早就了解到 多用组合, 少用继承, 但从根本上并没有深入了解该设计原则.
然而在看完了 Headers First 设计模式 第一章后, 马上体会到 组合 的优势.

情景分析

书中使用 鸭子 来举例. 当有一个 鸭子 基类, 有 绿头鸭/红头鸭/橡皮鸭 等子类. 当需要扩展鸭子的能力时, 比如说添加 飞行 能力时, 很自然的就会想到在基类 鸭子 中添加 飞行 方法, 这样 绿头鸭/红头鸭 不用修改就直接具有 飞行 能力.

然而, 副作用就是, 导致所有的 鸭子 子类都具有飞行能力, 例如 橡皮鸭, 但其不应该具有 飞行 能力的.
这时候就要求我们重写 橡皮鸭飞行 能力, 当让 橡皮鸭 飞行 时, 它什么事情都不做, 这样就解决该问题了.
但是, 又产生了另外两个问题:

  1. 以后每添加新的 鸭子 子类, 必须检查 鸭子 所有能力, 如果其不具备该能力, 则需要重写该能力;
  2. 以后每添加新的能力, 必须检查所有子类, 如果其不具备该能力, 也需要重写该能力.

这样就会导致以后维护成本的急剧升高. 并且, 如果以后我想修改部分 鸭子飞行 能力, 比如说部分 鸭子 使用 翅膀 飞行, 部分 鸭子 使用 发动机 飞行, 那该怎么办呢?

解决方案

设计原则:

找出应用中可能需要变化之处, 把它们独立出来, 不要和那些不需要变化的代码混在一起.

换句话说, 如果每次新需求一来, 都会使某方面的代码发生变化, 那么就可以确定, 这部分的代码需要被抽出来, 和其他稳定代码有所区分. 达到 把会变化的部分取出并封装起来, 以便以后可以轻易地改动或扩充此部分, 而不影响不需要变化的其他部分.

设计原则:

针对接口编程, 而不是针对实现编程.

针对上述场景, 鸭子飞行 能力是会经常发送变化的, 因此, 需要将其分离出来, 并且最好能动态变化. 这就需要抽象出 飞行接口, 实现 用翅膀飞行用发动机飞行 两种实现, 并且能够动态影响到 鸭子 的能力, 这就需要使用 组合 了.

我们不再给 鸭子 添加 飞行 方法了, 而是添加 飞行接口 字段, 然后根据需要动态赋值不同的 飞行接口实现类 的对象, 鸭子 通过调用 飞行接口实现类飞行 方法实现能力.

针对接口编程 的真正意思是 针对超类型(supertype)编程.

针对超类型编程 是指变量的声明类型应该是超类型, 通常是一个抽象类或者是一个接口. 只要是具体实现此超类型的子类对象, 都可以指定给这个变量, 利用 多态 实现通过什么的超类型对象在运行时执行真正的对象类型.

鸭子飞行接口 字段就是采用此思想.

有一个 可能比 是一个 更好

设计原则:

多用组合, 少用继承.

通过上面的例子, 我们可以看到其优势在于:

  1. 在运行时动态改变行为;
  2. 解耦, 使易变部分(飞行能力)独立于使用方(鸭子), 便于扩展和修改;

策略模式

于是, 就引出了今天要说的 策略模式(Strategy Pattern): 定义算法族, 并分别封装起来, 让它们直接可互相替换, 此模式让算法的变化独立于使用算法的客户.

例如, 客户需要排序功能, 我们定义 排序接口, 用不同算法实现不同的 排序实现, 比如 插入排序, 冒泡排序, 快排, 堆排序 等等, 用户可根据需要自由选择排序实现. 并且即使以后添加新的排序算法, 也不会对现有用户造成影响.

java Spring 框架中的 IoC 容器使用 依赖注入 作为实现 控制反转 的方式, 而 策略模式依赖注入 的基础.

最后推荐一篇介绍 控制反转(IoC)与依赖注入(DI) 的文章.

推荐阅读