很早就了解到 多用组合, 少用继承, 但从根本上并没有深入了解该设计原则.
然而在看完了 Headers First 设计模式 第一章后, 马上体会到 组合 的优势.
情景分析
书中使用 鸭子
来举例. 当有一个 鸭子
基类, 有 绿头鸭/红头鸭/橡皮鸭
等子类. 当需要扩展鸭子的能力时, 比如说添加 飞行
能力时, 很自然的就会想到在基类 鸭子
中添加 飞行
方法, 这样 绿头鸭/红头鸭
不用修改就直接具有 飞行
能力.
然而, 副作用就是, 导致所有的 鸭子
子类都具有飞行能力, 例如 橡皮鸭
, 但其不应该具有 飞行
能力的.
这时候就要求我们重写 橡皮鸭
的 飞行
能力, 当让 橡皮鸭
飞行
时, 它什么事情都不做, 这样就解决该问题了.
但是, 又产生了另外两个问题:
- 以后每添加新的
鸭子
子类, 必须检查鸭子
所有能力, 如果其不具备该能力, 则需要重写该能力; - 以后每添加新的能力, 必须检查所有子类, 如果其不具备该能力, 也需要重写该能力.
这样就会导致以后维护成本的急剧升高. 并且, 如果以后我想修改部分 鸭子
的 飞行
能力, 比如说部分 鸭子
使用 翅膀
飞行, 部分 鸭子
使用 发动机
飞行, 那该怎么办呢?
解决方案
设计原则:
找出应用中可能需要变化之处, 把它们独立出来, 不要和那些不需要变化的代码混在一起.
换句话说, 如果每次新需求一来, 都会使某方面的代码发生变化, 那么就可以确定, 这部分的代码需要被抽出来, 和其他稳定代码有所区分. 达到 把会变化的部分取出并封装起来, 以便以后可以轻易地改动或扩充此部分, 而不影响不需要变化的其他部分.
设计原则:
针对接口编程, 而不是针对实现编程.
针对上述场景, 鸭子
的 飞行
能力是会经常发送变化的, 因此, 需要将其分离出来, 并且最好能动态变化. 这就需要抽象出 飞行接口
, 实现 用翅膀飞行
和 用发动机飞行
两种实现, 并且能够动态影响到 鸭子
的能力, 这就需要使用 组合 了.
我们不再给 鸭子
添加 飞行
方法了, 而是添加 飞行接口
字段, 然后根据需要动态赋值不同的 飞行接口实现类
的对象, 鸭子
通过调用 飞行接口实现类
的 飞行
方法实现能力.
针对接口编程 的真正意思是 针对超类型(supertype)编程.
针对超类型编程 是指变量的声明类型应该是超类型, 通常是一个抽象类或者是一个接口. 只要是具体实现此超类型的子类对象, 都可以指定给这个变量, 利用 多态 实现通过什么的超类型对象在运行时执行真正的对象类型.
鸭子
的 飞行接口
字段就是采用此思想.
有一个 可能比 是一个 更好
设计原则:
多用组合, 少用继承.
通过上面的例子, 我们可以看到其优势在于:
- 在运行时动态改变行为;
- 解耦, 使易变部分(
飞行
能力)独立于使用方(鸭子
), 便于扩展和修改;
策略模式
于是, 就引出了今天要说的 策略模式(Strategy Pattern): 定义算法族, 并分别封装起来, 让它们直接可互相替换, 此模式让算法的变化独立于使用算法的客户.
例如, 客户需要排序功能, 我们定义 排序接口
, 用不同算法实现不同的 排序实现
, 比如 插入排序
, 冒泡排序
, 快排
, 堆排序
等等, 用户可根据需要自由选择排序实现. 并且即使以后添加新的排序算法, 也不会对现有用户造成影响.
java Spring 框架中的 IoC 容器使用 依赖注入 作为实现 控制反转 的方式, 而 策略模式 是 依赖注入 的基础.
最后推荐一篇介绍 控制反转(IoC)与依赖注入(DI) 的文章.