从“策略模式”聊聊“设计模式”有多重要?
ID:嵌入式Hacker
作者: 可爱的东东
对于技术领域的知识点,我个人喜欢简单地划分为2类:
1.基础类2.工具类
我判断一个知识点属于哪一类的主要依据有2点:
1.这个知识点是否经久不衰;2.这个知识点是否没有替代品;
如果上述2点都满足,则我会认为这是基础类知识,属于可以长期投资的价值股; 典型的例如操作系统、数据结构、Linux环境编程、软件模式(设计模式、架构模式...)等我会归类为基础类知识;
而例如 Qt、Git、Docker、甚至各种编程语言(C、C++、Java、Python)等知识点我都会暂时归类为工具类。不要误会,这些都只是我个人的喜好,并没有要贬低你心爱的技术的意思,只要是你在工作里需要重度使用的技术,你都应该把它归类为基础类,以便提醒自己需要深耕该技术。
说了这么多,无非是想告诉你,我认为设计模式很重要,仅此而已。即便你从事的是底层软件相关的工作,你只用 C 语言进行开发,情况也是一样的。
下面是正文(策略模式入门)
需求:
模拟现实生活中的鸭子,鸭子会游泳,鸣叫,飞。
1. 利用继承?
阶段1
一个父类 Duck 定义 鸭子 的一些特性,子类继承它的特性,并覆盖(override) 父类的部分特性。至此没什么问题,子类共同拥有父类的特性,消除了代码的重复性。
新的需求:
一些鸭子(例如野鸭)是会飞的,我们要让 Duck 具有 fly() 的特性。
阶段2
在 Duck 类中加上 fly() 方法:
引入新的问题:
在父类 Duck 类中增加 fly() 后,导致所有的子类鸭子都会 fly 了。
而真实的情况是:橡皮鸭子 RubberDuck 不应该会 fly。
思考:
1.对代码的局部修改,影响层面不只是局部;
2.继承虽然能复用代码,但是它并不完美;
阶段3
在 RubberDuck 中 override fly() 方法,方法内什么都不做。
利用继承来提供Duck的行为的做法,存在什么缺点?
1.大量的代码在多个 Duck 子类中冗余,例如 RubberDuck 的 fly();
2.父类Duck的改动会牵连所有子类Duck也要跟着改动
2. 使用接口效果如何?
阶段4
让部分而非全部鸭子可飞或者可叫。
把 fly() 从父类中提取出来定义为 Flyable 接口,只有会飞的鸭子才实现此接口 。quack() 也类似地定义为 Quackable 接口。
例如橡皮鸭 RubberDuck 不会飞,所有它不实现 Flyable 接口。
思考:
1.这样避免了 “阶段3” 中类似 RubberDuck->fly() 的冗余代码,但是又造成了 fly() 毫无复用性的问题( Java 接口内不能有代码实现);
2.设计原则:把会变化的部分独立出来,不要和不需要变化的代码混在一起(Identify the aspects of your application that vary and separate them from what stays the same.);
3.各种设计模式都有一样的目的:把会变化的部分取出并封装起来,以便以后轻易的改动和扩展此部分,而不影响不需要变化的其他部分;
4.软件开发的不变真理: 软件总是要变化的;
3. 划分变化与不变的部分
总结前面的做法:
1.阶段2: 行为 ( fly 和 quake ) 来自于 Duck 类内具体实现 ( concrete implementation );
2.阶段4: 行为来自于继承某个接口的子类内的专属实现 ( specialized implementation );
3.无论是阶段2还是阶段4,都是针对具体实现编程。即每一种会飞的鸭子,都必须要有自己专属的关于 fly 的具体实现(例如 MallarDuck->fly() / RedheadDuck->fly()),而无法共用同一类 fly 的方式(例如FlyWithWings / FlyWithRocket / FlyNoWay);
提取出变化的部分:
将会变化的鸭子的行为 ( 包括fly行为和quake行为 ) 从 Duck 类中提取出来。
4. 设计鸭子的行为
阶段5
让鸭子的行为可以动态改变:
用接口代表 ( represent ) 行为,定义2个接口:FlyBehavior and QuackBehavior。
行为的每一个具体实现(implementation)都会实现(implement) 对应的接口:
前人的经验:
1.设计原则:要针对接口编程,不要针对具体实现(implementation)编程 / Program to an interface, not an implementation;
2.这里的接口是一个“抽象概念”,并不是专门指Java 里的interface;
3.针对接口编程的另一个说法是针对超类型(supertype)编程,超类型在编程语法上一般是一个抽象类(abstract class)或者接口(interface);
实现鸭子的行为:
这样做有什么好处?
1.多个行为之间相互独立,可以轻松地添加更多的行为接口;
2.可以轻松地添加更多的行为实现;
例如添加一个用火箭来飞行的行为:添加了一个FlyRocketPowered类,它实现FlyBehavior接口即可
3.具体的行为实现都被封装在XXXBehavior接口内,使用者不用关心具体的行为细节;
4.行为接口 XXXBehavior 可以供其他 client 复用,例如鸡;
整合鸭子的行为:
1.鸭子将飞行和叫的行为委托 (delegate) 给别人处理,而不是在 Duck 类或者子类中自己来实现;
2.在 Duck 类中加入行为实例 xxxBehavior 和行为执行函数 performXxx();
3.实现 performQuack();
public class Duck {
QuackBehavior quackBehavior;
}
public void performQuack() {
quackBehavior.quack();
}
4.初始化行为实例变量,例如在 MallardDuck 类中:
public class MallardDuck extends Duck {
public MallardDuck() {
quackBehavior = new Quack();
flyBehavior = new FlyWithWings();
}
这里的做法并不完美:因为 MallardDuck 的构造函数里使用了 Quack 类 这个具体实现,即 MallardDuck 和 具体实现类 Quack 绑定在了一起。
由于xxxBehavior是可以在运行时被改变的,所以目前的做法已有足够的弹性了,暂时不用理会构造函数里的瑕疵。
5.允许动态地设置鸭子的行为,添加setXxxBehavior():
public void setFlyBehavior(FlyBehavior fb) {
flyBehavior = fb;
}
到这里,模拟鸭子的整个设计就已经完成了,整个设计框图如下:
5. 测试当前代码
测试代码:
public class MiniDuckSimulator {
public static void main(String[] args) {
Duck mallard = new MallardDuck();
mallard.performQuack();
mallard.performFly();
mallard.setFlyBehavior(new FlyRocketPowered());
mallard.performFly();
}
}
测试效果:
java MiniDuckSimulator
Quack
I’m flying!!
I'm flying with a rocker
根据测试结果总结一下:
1."有1个"比“是1个”更好,鸭子的行为不是继承来的,而是和行为对象组合而来;
2.设计原则:多用组合,少用继承(Favor composition over inheritance);
3.模拟鸭子使用的设计模式:策略模式;
什么是策略模式?
指对象有某个行为,但是在不同的场景中,该行为有不同的实现算法。
策略模式三要素:
1.定义了一族算法(业务规则);
2.封装了每个算法;
3.这族的算法可互换代替(interchangeable);
再举一个例子:
在一款游戏里,有不同的角色(国王、皇后、骑士...),角色有不同的武器(斧头、剑、刀),该怎么设计?
6. 最后的总结
懒人们专用:
7. 更多实践
有哪些开源项目使用或者借鉴了策略模式?
•Android
还等什么?赶紧分析起来吧~~
你和我各有一个苹果,如果我们交换苹果的话,我们还是只有一个苹果。但当你和我各有一个想法,我们交换想法的话,我们就都有两个想法了。如果你也对嵌入式系统开发有兴趣,并且想和更多人互相交流学习的话,请关注我的公众号:ESexpert,一起来学习吧,欢迎各种收藏/转发/批评。
嵌入式编程专辑 Linux 学习专辑 C/C++编程专辑 Qt进阶学习专辑 关注微信公众号『技术让梦想更伟大』,后台回复“m”查看更多内容,回复“加群”加入技术交流群。 长按前往图中包含的公众号关注