Simple Demo
假如我们设计一款RPG游戏,里面有各种职阶的角色可以选择:剑士、弓箭手、枪兵、骑师等。
该游戏内部设计使用了标准的面向对象技术,设计了一个角色超类,并让各种职阶角色继承该超类。子类先以剑士、枪兵为例。
现在突然有了一个需求:在玩家有一段时间没有操作游戏角色后,游戏角色可以在等待时哼唱本游戏的主题曲,可以简单理解为让游戏角色唱歌。
1、使用继承解决
对于一个面向对象的程序,这很简单。只要在 Character 类上加上 sing()方法,这样所有职阶的角色类都会继承 sing()。
但是发生了一个问题,(作为理性被夺走的补偿全能力强化的)狂战士,不存在理性和人格,自然也不可能会唱歌,这样破坏了设定,并不是所有的职阶角色都能唱歌。
虽然可以将sing()方法覆盖掉,在方法体内什么都不做。
为了“复用”而使用继承,但这样以后每当有新职阶的角色类出现,就要被迫检查该角色是否能够有唱歌这个行为,并可能需要覆盖sing()。
2、那么换成用接口又如何?
将 sing()从超类中取出来,放进一个“ Singable 接口”中,只有会唱歌的职阶角色才实现此接口。
但这样每个会唱歌的角色都要实现sing()方法,会造成重复代码变多,代码无法复用,而且角色哼唱的方式可能还有多种变化。
3、使用策略模式
①先找出应用中可能需要变化之处,将会变化的部分取出封装起来,让该部分改变不会影响其他部分。
我们知道 Character 类内的 sing()会随着职阶角色的不同而改变,将其从 Character 类中取出来,建立一组新的类来代表这个唱歌行为。
②如何实现唱歌的行为的类呢?针对接口编程。
我们利用接口 SingBehavior 代表这个行为,并有一些具体实现。
类图:
实际代码:
public interface SingBehavior {//行为接口 void sing(); } public class SingSoftly implements SingBehavior { // 轻轻唱 @Override public void sing() { System.out.println("hum hum hum ~ ~"); } } public class SingNoWay implements SingBehavior { // 没法唱 @Override public void sing() { System.out.println("......"); // 沉默 } }
针对接口编程是为了针对超类型(一个抽象类或者是一个接口)编程,将变量声明为超类型后,其具体实现类的对象都可以指定给这个变量,根据具体实现执行实际行为。
声明了SingBehavior(超类型)变量后,我们甚至不需要知道实际的子类型,只关心它能够进行正确的sing()的行为就够了。
这样将其分离出来后,可以让唱歌的行为被其他对象复用,这个行为已经和 Character 类无关了。
③将 SingBehavior 和 Character 组合起来(多用组合,少用继承)。
在 Character 类中加入一个 SingBehavior 实例变量,每个角色对象都会动态的设置这个变量,在运行时就可以引用正确的行为类。
public abstract class Character { protected SingBehavior singBehavior; public Character{ singBehavior = new SingSoftly();//默认能够轻轻唱 } public void fight() {// 进行战斗 System.out.println("I can fight"); } public abstract void capability(); public void performSing() { singBehavior.sing(); } public void setSingBehavior(SingBehavior singBehavior) { //用于动态设定唱歌行为 this.singBehavior = singBehavior; } // 其他方法...... }
这样想要进行唱歌的动作,只需委托给 singBehavior 去唱歌就可以了,不需要知道 singBehavior 真正引用的对象到底是什么。
下面看三个具体实现类:
Character 已经在构造器里初始化了 singBehavior,所以能够唱歌的 Saber 类和 Lancer 类不需要再初始化该变量。
public class Saber extends Character { @Override public void capability() { System.out.println("I can use the sword");// 使用剑 } } public class Lancer extends Character { @Override public void capability() { System.out.println("I can use the lance");// 使用长矛 } } public class Berserker extends Character { public Berserker() { singBehavior = new SingNoWay(); //默认沉默无声 } @Override public void capability() { System.out.println("I can destroy everything");// 破坏一切 } }
测试代码:
public class CharacterSingTest { public static void main(String[] args) { Character saber = new Saber(); System.out.println("It‘s Saber"); saber.performSing(); Character berserker = new Berserker(); System.out.println("It‘s Berserker"); berserker.performSing(); // Berserker默认沉默无声 //一开始Berserker的设定是失去理性的,假设有办法让他短暂的恢复理性 berserker.setSingBehavior(new SingBehavior() { //动态的设置 singBehavior @Override public void sing() { // 短暂的恢复了理性,能够唱歌了 System.out.println("I come to my senses temporarily , hum hum hum ~ ~"); } }); berserker.performSing(); } }
打印结果:
It‘s Saber hum hum hum ~ ~ It‘s Berserker ...... I come to my senses temporarily , hum hum hum ~ ~
重新设计后的类图:
重新设计后的类结构:所有职阶的角色类继承 Character ,具体唱歌行为实现 SingBehavior 接口,后将Character和SingBehavior组合起来,委托SingBehavior执行唱歌行为。
这一组唱歌行为可以想象成一个算法族,将每一个算法封装起来,且SingSoftly 和 SingNoWay 这些具体算法实现了同一个接口,是可以互换复用的;算法以后发生的变化独立于使用客户(Character)。这就是策略模式。
策略模式类图:
4:JDK中存在的策略模式
JDK中的比较器Comparator就应用了策略模式。
我们使用静态方法Collections.sort()方法给集合排序时,有两个重载方法:
①Collections.sort(List)
要求参数List集合里的元素必须实现Comparable接口(实现compareTo()方法),这样才能根据元素自定义的排序算法进行排序,但每次更换排序方式时都需要去修改compareTo()方法。
②Collections.sort(List, Comparator)
不要求List集合里的元素实现了Comparable接口,排序时会根据Comparator的具体实现算法进行排序。
这里的Collections.sort(List, Comparator)方法就是Context(环境角色),Comparator是Strategy(抽象的策略),而真正传入参数需要的是实现了Comparator的具体实现类(具体策略类)。
这样每次更换排序方式时只需要替换具体的比较器类,而不需要再去修改集合里的元素类。
参考书籍:《Head First 设计模式》