设计模式(一)策略模式 & 模板方法模式
以往在开发或者重构代码的时候,会经常用上一些技巧来达到抽象和解耦等目的,但并没有系统地详细了解和分类总结过程中所用到的方法,现在回过头来看一些设计模式的方法论,借鉴前人总结的开发经验,感触还蛮大的。而且在了解这些正统、专业的方法论后,后续与其它程序员在沟通代码设计时就有更高效的语言去描述了,当然前提是大家都对这有所了解。
上一年初对公司的iOS项目进行了一次大重构后,今年又有计划要对Android那边的项目也进行一次优化重构,这次必须得在行动前梳理梳理这设计模式的知识,尽量在工作中需要的地方清晰合理地应用起来。
这个系列主要都是参考《Head First》,也是按书本上所用的语言Java来做Demo(合乎我接下来搞Android呢)。好,那就开始吧~(为了节省篇幅,下面方法体中的规范会有所将就)
策略模式
应用场景
已知有父类鸭子和它的一些子类,如青头鸭、橡皮鸭等,起初实现需求时,他们都还没有飞翔功能,现在想对能飞翔的子类添加飞方法实现。
方案一
利用继承实现的话,如下
public abstract class Duck1 {
public void quack() {System.out.println("quack");}
public void swim() {System.out.println("swim");}
public abstract void display();
public void fly() {System.out.println("fly");};
}
public class MallardDuck1 extends Duck1 { //青头鸭
public void display() {System.out.println("MallardDuck display");}
}
public class RedheadDuck1 extends Duck1 { //红头鸭
public void display() {System.out.println("RedheadDuck display");}
}
public class RubberDuck1 extends Duck1 { //橡皮鸭
public void display() {System.out.println("RubberDuck display");}
@Override
public void quack() {System.out.println("squeak");}
}
public class DecoyDuck1 extends Duck1 { //诱利鸭
public void display() {System.out.println("RubberDuck display");}
@Override
public void quack() { }
}
从上面的代码看,要添加一个 fly 方法,其实是不适合直接在父类 Duck 中通过继承的方式实现而让子类获得此方法,因为并非所有子类鸭子都会飞。若为使大部分能飞的鸭子子类可以复用 fly 方法而硬要在父类中添加,则一些不会飞鸭子子类需要重写 fly 方法改为什么都不做。同时发现,已经实现了的 quack 方法也是与 fly 一样的情况。
方案二
利用接口改进,如下
public interface FlyBehavior {
public void fly();
}
public interface QuackBehavior {
public void quack();
}
public abstract class Duck2 {
public void swim() {System.out.println("swim");}
public abstract void display();
}
public class MallardDuck2 extends Duck2 implements FlyBehavior, QuackBehavior { //青头鸭
public void display() {System.out.println("MallardDuck display");}
public void fly() {System.out.println("MallardDuck fly");};
public void quack() {System.out.println("MallardDuck quack");};
}
public class RedheadDuck2 extends Duck2 implements FlyBehavior, QuackBehavior { //红头鸭
public void display() {System.out.println("RedheadDuck display");}
public void fly() {System.out.println("RedheadDuck fly");};
public void quack() {System.out.println("RedheadDuck quack");};
}
public class RubberDuck2 extends Duck2 implements QuackBehavior { //橡皮鸭
public void display() {System.out.println("RubberDuck display");}
public void quack() {System.out.println("RubberDuck quack");};
}
public class DecoyDuck2 extends Duck2 { //诱利鸭
public void display() {System.out.println("DecoyDuck display");}
}
这样修改后,会发现重复的代码多了,像 fly 和 quack 接口方法,若子类很多且大部分都实现了 fly 或 quack 接口的时候(即使有部分的实现不一样),在需要调整 fly 或 quack 的实现时要改动到的地方就会变得很多了。
方案三
利用组合的形式改进,如下
public interface FlyBehavior {
public void fly();
}
public class FlyWithWings implements FlyBehavior {
public void fly() {System.out.println("Fly with wings");}
}
public class FlyNoWay implements FlyBehavior {
public void fly() {System.out.println("Can't fly");}
}
public interface QuackBehavior {
public void quack();
}
public class Qucak implements QuackBehavior {
public void quack() {System.out.println("Quack");}
}
public class Squeak implements QuackBehavior {
public void quack() {System.out.println("Squeak");}
}
public class MuteQuack implements QuackBehavior {
public void quack() {System.out.println("Silence");}
}
public abstract class Duck3 {
FlyBehavior flyBehavior;
QuackBehavior quackBehavior;
void performFly() {
flyBehavior.fly();
}
void performQuack() {
quackBehavior.quack();
}
public void swim() {System.out.println("swim");}
public abstract void display();
}
public class MallardDuck3 extends Duck3 { //青头鸭
public MallardDuck3() {
quackBehavior = new Qucak();
flyBehavior = new FlyWithWings();
}
public void display() {System.out.println("MallardDuck display");}
}
public class RedheadDuck3 extends Duck { //红头鸭
public RedheadDuck3() {
quackBehavior = new Qucak();
flyBehavior = new FlyWithWings();
}
public void display() {System.out.println("RedheadDuck display");}
}
public class RubberDuck3 extends Duck { //橡皮鸭
public RubberDuck3() {
quackBehavior = new Squeak();
flyBehavior = new FlyNoWay();
}
public void display() {System.out.println("RubberDuck display");}
}
public class DecoyDuck3 extends Duck { //诱利鸭
public DecoyDuck3() {
quackBehavior = new MuteQuack();
flyBehavior = new FlyNoWay();
}
public void display() {System.out.println("DecoyDuck display");}
}
现在,飞翔和啼叫的动作委托了两个抽象的实例变量去处理。而且在 Duck 中添加两个实例变量的 set 方法,更是可以在运行时动态地设定这两种行为。
这总结出3个设计原则:
- 找出变化之处独立出来,封装变化;
- 针对接口编程而不要针对实现编程(即是对方法进行抽离抽象成接口后使用,若在父类或子类中直接实现后使用就会因太过于通用而失去了灵活性);
- 多组合,少继承。
总结
策略模式,就是一套算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
模板方法模式
应用场景
假设有茶和咖啡两个类(如下所示代码),它们在冲泡的过程中,有部分步骤方法实现是重复的,现在需要进行优化抽取出公共的代码。
public class Coffee1 {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
void boilWater() {System.out.println("Boiling water");}
void brewCoffeeGrinds() {System.out.println("Dripping Coffee through filter");}
void pourInCup() {System.out.println("Pouring into cup");}
void addSugarAndMilk() {System.out.println("Adding Sugar and Milk");}
}
public class Tea1 {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
void boilWater() {System.out.println("Boiling water");}
void steepTeaBag() {System.out.println("Steeping the tea");}
void pourInCup() {System.out.println("Pouring into cup");}
void addLemon() {System.out.println("Adding Lemon");}
}
方案一
创建一个咖啡因饮料的基类,将咖啡和茶的公共方法抽取到此基类中,它们各自实现自己特有的步骤方法
public abstract class CaffeineBeverage2 {
void prepareRecipe() {
boilWater();
pourInCup();
}
public void boilWater() {System.out.println("Boiling water");}
public void pourInCup() {System.out.println("Pouring into cup");}
}
public class Coffee2 extends CaffeineBeverage2 {
void prepareRecipe() {
boilWater();
brewCoffeeGrinds();
pourInCup();
addSugarAndMilk();
}
void brewCoffeeGrinds() {System.out.println("Dripping Coffee through filter");}
void addSugarAndMilk() {System.out.println("Adding Sugar and Milk");}
}
public class Tea2 extends CaffeineBeverage2 {
void prepareRecipe() {
boilWater();
steepTeaBag();
pourInCup();
addLemon();
}
void steepTeaBag() {System.out.println("Steeping the tea");}
void addLemon() {System.out.println("Adding Lemon");}
}
此时,会发现 prepareRecipe 并没有完全抽取到基类,因为子类的 prepareRecipe 实现里面是包含子类特有的步骤方法,基类中的 prepareRecipe 其实并没什么实质的意义,只是包含上基类知道的公共步骤方法,具有真正意义的只能靠子类重写的 prepareRecipe。
但这时问题又来了,子类重写的 prepareRecipe 方法里还是存在重复的代码,就是 boilWater 和 pourInCup 这两个步骤,而且如果 prepareRecipe 其中的实现变动(例如步骤顺序等改变)那要改的地方就很多了(每一个子类里),需要再进一步抽象 prepareRecipe 。
方案二
抽象出特性的步骤方法
public abstract class CaffeineBeverage3 {
void prepareRecipe() {
boilWater();
brew();
pourInCup();
addCondiments();
}
public void boilWater() {System.out.println("Boiling water");}
public void pourInCup() {System.out.println("Pouring into cup");}
abstract void brew();
abstract void addCondiments();
}
public class Coffee3 extends CaffeineBeverage3 {
public void brew() {System.out.println("Dripping Coffee through filter");}
public void addCondiments() {System.out.println("Adding Sugar and Milk");}
}
public class Tea3 extends CaffeineBeverage3 {
public void brew() {System.out.println("Steeping the tea");}
public void addCondiments() {System.out.println("Adding Lemon");}
}
抽象出特性的步骤方法后,prepareRecipe 可以完全抽取到基类中实现了,而基类也不用关注两个抽象步骤方法 brew 和 addCondiments 的具体实现。
这里咖啡和茶经过两次的方法抽取后,算法原本在这两个子类中控制的,改由基类咖啡因饮料主导,从而可以保护和约束这个算法的框架,即是基类更专注于算法本身,子类提供完整的实现。
这个例子中是在继承的关系上演示的,实际情况可能会有变种,例如算法本身并不在基类上,而是在另外一个独立类上实现,完整的实现通过接口(iOS上对应的是协议)去获取。
最后,关于这个例子中子类实现的完整步骤方法,有时候子类可能想要动态控制是否执行这个步骤,所以设计者在模板算法中对于可选执行的步骤,应该提供钩子hook给子类去自行决定是否执行(基类或算法所在的类上可以实现默认或空的钩子方法,等子类选择重写),如下例
public abstract class CaffeineBeverageWithHook {
void prepareRecipe() {
boilWater();
brew();
pourInCup();
if (customerWantsCondiments()) {
addCondiments();
}
}
abstract void brew();
abstract void addCondiments();
public void boilWater() {System.out.println("Boiling water");}
public void pourInCup() {System.out.println("Pouring into cup");}
boolean customerWantsCondiments() {
return true;
}
}
public class CoffeeWithHook extends CaffeineBeverageWithHook {
public void brew() {System.out.println("Dripping Coffee through filter");}
public void addCondiments() {System.out.println("Adding Sugar and Milk");}
public boolean customerWantsCondiments() {
String answer = getUserInput();
if (answer.toLowerCase().startsWith("y")) {
return true;
} else {
return false;
}
}
private String getUserInput() {
String answer = null;
System.out.print("Would you like milk and sugar with your coffee (y/n)?");
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
try {
answer = in.readLine();
} catch (IOException ioe) {
System.err.println("IO error trying to read your answer");
}
if (answer == null) {
return "no";
}
return answer;
}
}
一般,都是底层组件(具体实现者)挂钩到高层组件(抽象执行者)上,
另外需要注意一点,算法里的步骤切割得太细就会让子类实现的步骤方法麻烦复杂,但太粗太少就会比较没有弹性,这个需要折衷考虑。
总结
模板方法模式其实是定义了一个算法的大纲,定义了算法的步骤,并由它的子类或接口实现者定义其中的某些步骤的内容,它也是一种代码复用的技巧,当中的抽象类可以定义具体方法、抽象方法和钩子,为防止子类改变模板方法,可以将模板方法声明为final。
延伸,对比一下,模板方法模式和策略模式有点相似,但是策略模式通过组合委托对象的形式是可以动态更换算法中的实现步骤,不过算法的封装因为散落在子类而没有绝对的控制权,会较容易出现重复代码。而工厂方法则是模板方法的一种特殊版本。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 mingfungliu@gmail.com
文章标题:设计模式(一)策略模式 & 模板方法模式
文章字数:2.9k
本文作者:Mingfung
发布时间:2019-01-13, 23:41:00
最后更新:2019-02-12, 13:11:48
原始链接:http://blog.ifungfay.com/设计模式/设计模式(一)策略模式 & 模板方法模式/版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。