此文写给想要理解设计模式,但认为《设计模式》中所举的例子过于复杂的读者。
为了使程序具有更高的可复用性,前人推荐我们使用如下设计模式:
结构型模式:通过组合类和对象来获得更大的结构
1. 适配器模式(Adapter)
2. 装饰器模式(Decorator)
3. 外观模式(Facade)
行为模式:涉及到算法和对象间职责的分配
1. 策略模式(Strategy)
2. 模板模式(Template)
3. 迭代器模式(Iterator)
1.1 适配器模式
客户想要根据半径计算圆的面积(他自己不会算,甚至不知道圆是什么)
public static void main(String[] args) { double radius = 5; System.out.println("我要怎么办?"); }
翻了翻代码库,发现有一个已有的工具类Mathematician里有一个计算圆的面积的方法
public class Mathematician { /** * 根据圆的周长计算面积 * @param C 圆的周长 * @return 圆的面积 */ public double CalculateAreaFromPerimeter(double C) { return Math.pow((C / Math.PI / 2), 2) * Math.PI; } }
客户感到非常绝望,给我打了电话,拜托我给他设计一个新的工具类,能够实现用半径算面积的功能。
可问题是我并不会算圆的面积,只会根据半径算周长。于是我只好想方设法利用已有的方法了。
public class SuperMathTool { private final Mathematician mathematician; /** * 构造方法,需要使用旧的工具类来完成组合 * @param mathematician 一个旧的工具类对象 */ public SuperMathTool(Mathematician mathematician) { this.mathematician = mathematician; } /** * 根据半径算圆的周长 * @param radius 半径 * @return 圆的周长 */ public double CalculateAreaFromRadius(double radius) { double perimeter = 2 * radius * Math.PI; double ans = mathematician.CalculateAreaFromPerimeter(perimeter); return ans; } }
这样还不算完工。再用接口包装一下。
public interface MathTool { public double CalculateAreaFromRadius(double radius); }
还要记得让SuperMathTool实现这个接口。这样就算完成了。
为什么要用接口呢?我可以写详细的Javadoc方便用户使用,也方便用户以后扩展,还规定了一个优秀的数学工具类必须有根据圆的半径计算面积的功能。
为什么要用组合(委派)呢?因为这样的话,除了计算半径以外的其他工具方法,都可以委派给旧工具箱类完成,我自己不用重写实现,也不用复制代码。
(用继承的方法也可以实现同样的功能)
最后,我告诉用户,我的工具不能单独使用,想要使用的话就要这样:
public static void main(String[] args) { double radius = 5; MathTool tool = new SuperMathTool(new Mathematician()); tool.CalculateAreaFromRadius(radius); }
1.2 装饰器模式
客户想要根据边长计算正方形的面积(他自己不会算,甚至不知道正方形是什么)
public static void main(String[] args) { double sideLength = 5; System.out.println("怎么办啊?"); }
翻了翻代码库,发现有一个已有的数学工具类接口
public interface MathTool { /** * 根据半径计算圆的面积 * @param radius 半径 * @return 圆的面积 */ public double CalculateAreaFromRadius(double radius); }
和它的一个实现类
public class Mathematician implements MathTool { @Override public double CalculateAreaFromRadius(double radius) { double ans = Math.pow(radius, 2) * Math.PI; return ans; } }
这东西只能算圆的面积啊!客户感到非常绝望,给我打了电话,拜托我给他设计一个新的工具类,能够实现用边长算正方形面积的功能,同时不要把旧功能丢掉。
那么我要开始工作了:写一个新的抽象类实现原接口(防止遗漏原有方法),然后用旧工具类组合,将原有方法委派给它
public abstract class MathToolDecorator implements MathTool { private final MathTool oldTool; /** * 构造方法:完成组合 * @param mathematician 旧工具箱对象 */ public MathToolDecorator(Mathematician mathematician) { this.oldTool = mathematician; } /** * 原有的方法委派给旧工具箱 */ @Override public double CalculateAreaFromRadius(double radius) { return oldTool.CalculateAreaFromRadius(radius); } /** * 根据边长计算正方形面积 * @param sideLength 边长 * @return 正方形面积 */ public abstract double CalculateAreaFromSideLength(double sideLength); }
使用抽象类的原因是,计算正方形面积的新方法可能有很多种实现,这样写方便以后扩展;并且,除了新方法之外的部分已经在抽象类中写好了,一劳永逸,以后写不同实现时就只用写正方形,原有方法就不用再写了(等下就能看到)。
接下来,在具体类中完成计算正方形面积的实现:
public class ConcreteMathToolDecorator extends MathToolDecorator { /** * 构造方法:直接调用抽象类构造方法即可 * @param mathematician */ public ConcreteMathToolDecorator(Mathematician mathematician) { super(mathematician); } @Override public double CalculateAreaFromSideLength(double sideLength) { double ans = Math.pow(sideLength, 2); return ans; } }
这样就算完成了。最后告诉用户应该这样使用:
public static void main(String[] args) { double sideLength = 5; MathToolDecorator tool = new ConcreteMathToolDecorator(new Mathematician()); tool.CalculateAreaFromSideLength(sideLength); }
1.3 外观模式
用户想要计算a * b mod c,他想要调用已有的方法来完成这个复杂的计算。他翻了翻代码库,找到了这个工具类:
public class SuperMathTool { /** * 计算a + b * @param a * @param b * @return a + b */ public int plus(int a, int b) { return a + b; } /** * 计算a % b * @param a * @param b * @return a % b */ public int mod(int a, int b) { return a % b; } }
这些计算非常精妙,但想要完成计算必须反复调用它们很多次,实在是太繁琐了!用户觉得懒得动弹,于是给我打了电话,让我写一个方便计算a * b mod c的工具类。
这不难做,把繁琐的操作包装起来方便用户使用,这是我的强项。
public class EasyTool { private final SuperMathTool tool = new SuperMathTool(); /** * 一个简便的操作 * 计算a * b mod c */ public int calculate(int a, int b, int c) { int ans = 0; for (int i = 1; i <= b; i++) { ans = tool.plus(a, i); } ans = tool.mod(ans, c); return ans; } }
最后,告诉用户这样使用:
public class Main { public static void main(String[] args) { int a = 53; int b = 97; int c = 89; EasyTool tool = new EasyTool(); tool.calculate(a, b, c); } }
两行即可解决问题!
2.1 策略模式
众所周知,java的整数乘法乘法有两种实现方式,一种是直接乘起来,另一种是累加。
大猪头最近正在设计一个数学工具类,其中有一个方法就是计算乘法。
public class MathTool { /** * 整数加法 */ public int plus(int a, int b) { return a + b; } /** * 整数乘法 */ public int multiply(int a, int b) { return a * b; } }
但他认为这不够完美,因为他实现乘法的方式是固定的。他有一个想法:让用户在运行时,动态地决定用哪种实现方式来计算乘法。于是我建议他使用策略模式:
1. 首先写一个策略接口,声明一下将要完成什么工作
public interface Strategy { /** * 计算整数乘法a * b * @param a * @param b * @return a * b */ public int multiply(int a, int b); }
2. 写出两种不同的实现
/** * 乘法的第一种实现 */ public class Strategy1 implements Strategy { @Override public int multiply(int a, int b) { int ans = a * b; return ans; } }
/** * 乘法的第二种实现 */ public class Strategy2 implements Strategy { @Override public int multiply(int a, int b) { int ans = 0; for (int i = 1; i <= b; i++) { ans += a; } return ans; } }
3. 将Strategy组合到原有的数学工具类中:
public class MathTool { private final Strategy strategy; /** * 构造方法:完成组合 * @param strategy 将要使用的乘法策略 */ public MathTool(Strategy strategy) { this.strategy = strategy; }
然后计算乘法的部分委派给strategy完成
/** * 整数加法 */ public int plus(int a, int b) { return a + b; } /** * 整数乘法 */ public int multiply(int a, int b) { return strategy.multiply(a, b); } }
完成啦。
这样,用户使用MathTool的自由度就更大啦,比如:
public static void main(String[] args) { MathTool tool1 = new MathTool(new Strategy1()); tool1.multiply(3, 4); MathTool tool2 = new MathTool(new Strategy2()); tool2.multiply(5, 6); }
2.2 模板模式
大猪头打算设计一系列游戏,我建议他采用模板模式,因为尽管游戏内容不同,所有游戏都有相同的三个步骤:
1. 初始化
2. 开始
3. 结束
不同游戏的区别在于,这三个步骤的具体实现是不同的,但不同的游戏都必须按这个顺序执行这三个步骤,这个顺序是固定的。
先写一个游戏的抽象类:
public abstract class Game { /** * 模板方法:按照特定顺序调用其他方法完成操作 * 注意:加上final标签防止被重写 */ public final void play(){ //初始化游戏 initialize(); //开始游戏 startPlay(); //结束游戏 endPlay(); } abstract void initialize(); abstract void startPlay(); abstract void endPlay(); }
它有一个final的模板方法,规定了各个操作的顺序,要求无论什么样的实现都必须按照这个顺序执行。
它的不同实现需要重写初始化、开始、结束的操作:
/** * 足球游戏 */ public class Football extends Game { @Override void initialize() { System.out.println("足球游戏初始化中..."); } @Override void startPlay() { System.out.println("足球游戏开始!"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } @Override void endPlay() { System.out.println("足球游戏结束!"); } }
/** * 拳击游戏 */ public class Boxing extends Game { @Override void initialize() { System.out.println("拳击游戏初始化中..."); } @Override void startPlay() { System.out.println("拳击游戏开始!"); try { Thread.sleep(1500); } catch (InterruptedException e) { e.printStackTrace(); } } @Override void endPlay() { System.out.println("拳击游戏结束!"); } }
用户使用的方法如下:
public static void main(String[] args) { Game game1 = new Football(); game1.play(); Game game2 = new Boxing(); game2.play(); }
2.3 迭代器模式
大猪头设计了一个模拟行星系中行星运动的轨道系统:
public class CircularOrbit { private Map<Double, String> objects = new HashMap<>(); // AF // objects是轨道系统各物体按照其轨道半径的索引
// Map<Double, String>中,Double是轨道半径,String是物体名称
为了方便地访问各个轨道物体(并且最好能从轨道半径从小到大访问),他需要给这个ADT实现Iterable接口,
public class CircularOrbit implements Iterable {
然后实现Iterator方法:
@Override public Iterator<String> iterator() { return new OrbitIterator(this); }
这个OrbitIterator是自己定义的一个迭代器类(当然了,Java可不知道我们会设计怎样的ADT,当然不可能给我们准备好Iterator)
最好能写成内部类,更加安全。以下展示CircularOrbit的完成版本:
public class CircularOrbit implements Iterable<String> { private Map<Double, String> objects = new HashMap<>(); // AF // objects是轨道系统各物体按照其轨道半径的索引 // Map<Double, String>中,Double是轨道半径,String是物体名称 @Override public Iterator<String> iterator() { return new OrbitIterator(this); } class OrbitIterator implements Iterator<String> { private final List<String> objs = new ArrayList<>(); private int index; // AF // objs按从里向外的顺序储存轨道系统中的轨道物体 // index表示当前遍历到的元素在list中的下标 // RI // index >= 0 // Safety // index是private的不可变类 // objs是private final的可变类,且没有操作向外返回该对象 /** * 构造方法:将CircularOrbit中的物体有里向外存在list中 * @param c 轨道系统对象 */ private OrbitIterator(CircularOrbit c) { List<Double> radiuses = new ArrayList<>(); radiuses.addAll(c.objects.keySet()); radiuses.sort(Comparator.naturalOrder()); for (int i = 0; i < radiuses.size(); i++) { objs.add(c.objects.get(radiuses.get(i))); } index = 0; } @Override public boolean hasNext() { if (index < objs.size()) return true; else return false; } @Override public String next() { return objs.get(index); } } }
注意:Iterable和Iterator后的尖括号中写的是想要让Iterator.next()返回的对象类型。
用户使用方法如下:
public static void main(String[] args) { CircularOrbit c = new CircularOrbit(); Iterator<String> it = c.iterator(); while (it.hasNext()) System.out.println(it.next()); }
举例就到此结束了。
为了方便理解,选用了过分简单的例子,请多多包涵。