23种设计模式总结与分析override4
总算到了最后一章了,肝到这里也不容易。参考了很多资料,也写下了很多测试代码。纸上得来终觉浅,绝知此事要躬行很多模式都是在工作学习中遇到实际问题不知不觉用起来就逐渐顺手觉得对项目十分有帮助了。一起加油吧,写出更加艺术优雅、简洁高效的代码~
共勉!
19 Memento(备忘录)模式
该模式人如其名:用于记录状态,便于回滚;或记录快照(瞬时(时间节点)状态)
我们想要恢复到以前某个状态的文件内容或者属性,或者想要回退到之前某个时间节点状态。我们可以记录下每个节点各个属性的状态信息,或者在想要存储的节点存储状态然后一遍遍回退,就可以找到这个"备忘录",回退到该状态。这个模式通常会和命令(Undo)模式一起配合使用。最主要的应用就是游戏里面的存盘(档);系统里面做快照;数据对象的序列化\持久化;以及网络传输的序列化。
比如在坦克游戏中我们可以加入save方法,对当前所有对象状态进行存储:
public void save() throws IOException {
File file = new File("d:/xx/Java_Project/xxxx");
ObjectOutputStream outputStream = null;
try {
outputStream.writeObject(myTank);
outputStream = new ObjectOutputStream(new FileOutputStream(file));
outputStream.writeObject(myTank);
outputStream.writeObject(objects);
} catch (IOException e) {
e.printStackTrace();
} finally {
if (outputStream != null) {
outputStream.close();
}
}
}
有保存必然有读取:
public void load() throws IOException {
File file = new File("d:/xx/Java_Project/xxxx");
ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream(file));
try {
myTank = (Player) inputStream.readObject();
objects =(List<AbstractGameObject>) inputStream.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
通过输入键盘命令控制存储、加载
private class TankKeyListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if(key == KeyEvent.VK_S) save();
else if(key == KeyEvent.VK_L) load();
else gm.getMyTank().keyPressed(e);
}
}
当然所有的对象,坦克类、子弹类以及引用的对象,策略接口都需要序列化
public class GameModel implements Serializable
public interface FireStrategy extends Serializable
public interface Collider extends Serializable
运行save方法,这时就记录下了这时所有对象的状态,load一下,就可以马上回到save节点地状态了。
20 Template Method(模板方法)模式
模板方法,也有一个我们熟悉的叫法,叫钩子函数或者回调函数。
此种设计模式可以说是我们日常开发使用的最平常最不经意的方法了。只要我们写了一个抽象类,定义了里面的抽象方法以及抽象类的属性,再写一个实体类去继承它,再在场景中调用该类,这就可以说是模板方法了,该抽象类就是一个模板。其中抽象类中的某些方法和属性不希望其它类、场景能够访问到,只希望其子类可以访问,所以访问修饰符应为protected。
具体场景类图如下:
isAlarm(),悍马汽车喇嘛要不要响, 以及setAlarm(boolean isAlarm) 业务\场景来决定喇叭要不要想,都是钩子函数
public class HummerH1Model extends HummerModel {
private boolean alarmFlag = true; //是否要响喇叭
@Override
protected void alarm() {
System.out.println("悍马H1鸣笛...");
}
@Override
protected void engineBoom() {
System.out.println("悍马H1引擎声音是这样在...");
}
@Override
protected void start() {
System.out.println("悍马H1发动...");
}
@Override
protected void stop() {
System.out.println("悍马H1停车...");
}
@Override
protected boolean isAlarm() {
return this.alarmFlag;
}
//要不要响喇叭,是有客户的来决定的
public void setAlarm(boolean isAlarm){
this.alarmFlag = isAlarm;
}
}
当setAlarm(false)运行起来时,喇叭就不会发出声音了。
最后,模板方法的通用类图如下:
其中 TemplateMethod 就是模板方法,operation1 和 operation2 就是基本方法,模板方法就是通过汇总
或排序基本方法而产生的结果集。
还有一个经验之谈:如果你需要拓展一个功能,可以去继承这个抽象类,然后去修改 protected 方法,然后就是调用一个类似execute 方法(对外释放的接口),就完成你的扩展开发,这就是模板方法,一个简单通用的设计模式。
21 Status(状态)模式
状态模式,也是一个不是很复杂的模式:归一来讲就是根据对应的状态选择相应的反应以及动作。
通过开关、运行电梯的栗子就可以理解状态模式了我们每天都在乘电梯,那我们来看看电梯有哪些动作(映射到 Java 中就是有多少方法):开门、关门、运行、停止,当然电梯门不是随时都可以开的你不可能电梯在运行的时候突然开门吧?!电梯也不会出现停止了但是不开门的情况吧?!(要有也是电梯出事故的时候),这里我们就可以分析分析电梯有什么那些特定状态:
- 门敞状态
- 门闭状态
- 运行状态
- 停止状态
用一张表来表示电梯状态和动作之间的关系:
根据电梯不同的状态选择相对应的运行动作
先看类图:
//接口类
public interface ILift {
public final int OPENING_STATUS = 1;
public final int RUNNING_STATUS = 2;
public final int STOP_STATUS = 3;
public final int CLOSED_STATUS = 0;
// 设置电梯状态
public void setStatus(int status);
//打开电梯动作
public void open();
//关闭电梯动作
public void close();
//停止电梯动作
public void stop();
//运行电梯动作
public void run();
}
//实现类
public class Left implements ILift {
private int status;
private void openWithStatus(){
System.out.println("电梯门开启...");
}
private void closeWithStatus(){
System.out.println("电梯门关闭...");
}
private void runWithStatus(){
System.out.println("电梯在运行...");
}
private void stopWithStatus(){
System.out.println("电梯停止...");
}
@Override
public void setStatus(int status) {
this.status = status;
}
@Override
public void open() {
//电梯在什么状态才能开启
switch (this.status){
//如果已经在门敞状态,则什么都不做
case OPENING_STATUS:
//do nothing;
break;
//如是电梯时关闭状态,则可以开启
case CLOSED_STATUS:
//停止状态,当然要开门了
case STOP_STATUS:
this.openWithStatus();
this.setStatus(OPENING_STATUS);
break;
case RUNNING_STATUS:
//do nothing;
break;
}
}
@Override
public void close() {
switch (this.status){
case OPENING_STATUS: //如果已经在门敞状态,那肯定要先停下来的,什么都不做
//do nothing;
break;
case CLOSED_STATUS: //如是电梯时关闭状态,则当然可以停止了
case RUNNING_STATUS: //正在运行状态,有运行当然那也就有停止了
this.stopWithStatus();
this.setStatus(CLOSED_STATUS);
break;
case STOP_STATUS: //停止状态,什么都不做
//do nothing;
break;
}
}
@Override
public void stop() {
switch(this.status) {
case OPENING_STATUS: //如果已经在门敞状态,那肯定要先停下来的,什么都不做
//do nothing;
break;
case CLOSED_STATUS: //如是电梯时关闭状态,则当然可以停止了
case RUNNING_STATUS: //正在运行状态,有运行当然那也就有停止了
this.stopWithStatus();
this.setStatus(STOP_STATUS);
break;
case STOP_STATUS: //停止状态,什么都不做
//do nothing;
break;
}
}
@Override
public void run() {
switch(this.status){
case OPENING_STATUS: //如果已经在门敞状态,则不你能运行,什么都不做
//do nothing;
break;
case CLOSED_STATUS: //如是电梯时关闭状态,则可以运行
case STOP_STATUS: //停止状态,可以运行
this.runWithStatus();
this.setStatus(RUNNING_STATUS);
break;
case RUNNING_STATUS: //正在运行状态,则什么都不做
//do nothing;
break;
}
}
}
//模拟电梯的动作
public class Client {
public static void main(String[] args) {
ILift lift = new Lift();
//电梯的初始条件应该是停止状态
lift.setStatus(ILift.STOP_STATUS);
//首先是电梯门开启,人进去
lift.open();
//然后电梯门关闭
lift.close();
//再然后,电梯跑起来,向上或者向下
lift.run();
//电梯可以停止
lift.stop();
}
}
我们设置电梯的起始是停止状态,看运行结果:
接下来review一下代码:
首先 Lift.java 这个文件有点长,因为我们使用了大量的 switch…case 这样的判断;其次,扩展性非常的不好,大家来想想,如果电梯还需要增加状态该怎么办呢,比如添加通电状态和断电状态,方法中都要增加判断条件。也就是说 switch 判断体中还要增加 case 项,也就说与开闭原则相违背了;;再其次,我们来思考我们的业务,电梯在门敞开状态下就不能上下跑了吗?电梯有没有发生过只有运行没有停止状态呢(从 40 层直接坠到 1 层嘛)?电梯故障嘛,还有电梯在检修的时候,可以在 stop状态下不开门,这也是正常的业务需求呀。针对以上的问题,我们该怎么来进行修改呢?
现在我们换个角度来看问题,我们来想电梯在具有这些状态的时候,能够做什么事情,也就是说在电梯处于一个具体状态时,我们来思考这个状态是由什么动作触发而产生以及在这个状态下电梯还能做什么事情 思考过后我们来看类图:
类图中,定义了一个 LiftState 抽象类,声明了一个受保护的类型 Context 变量,用于串联各个状态的封装类。封装的目的很明显,就是电梯对象内部状态的变化不被调用类知晓,也就是迪米特法则了
public abstract class LiftState {
//定义一个环境角色,也就是封装状态的变换引起的功能变化
protected Context context;
public void setContext(Context _context){
this.context = _context;
}
//首先电梯门开启动作
public abstract void open();
//电梯门有开启,那当然也就有关闭了
public abstract void close();
//电梯要能上能下,跑起来
public abstract void run();
//电梯还要能停下来,停不下来那就扯淡了
public abstract void stop();
}
//在电梯们开启的情况下可以做什么
public class OpeningStatus extends LiftState {
@Override
public void open() {
System.out.println("电梯门开启...");
}
//开启当然可以关闭了,测试一下电梯门开关功能
@Override
public void close() {
//状态修改
super.context.setLiftState(Context.openningState);
//动作委托为CloseState来执行
super.context.getLiftState().close();
}
//门开着电梯就想跑,这电梯是要上天。。。
@Override
public void run() {
//do nothing
}
@Override
public void stop() {
do nothing;
}
}
//封装状态类
public class Context {
//定义出所有电梯的状态
public final static OpeningStatus openningState = new OpeningStatus();
public final static ClosingStatus closingStatus = new ClosingStatus();
public final static RunningStatus runningStatus = new RunningStatus();
public final static StoppingStatus stoppingStatus = new StoppingStatus();
//当前电梯的状态
private LiftState liftState;
public void setLiftState(LiftState liftState){
this.liftState = liftState;
//把当前的环境通知到各个实现类中
this.liftState.setContext(this);
}
public LiftState getLiftState() {
return liftState;
}
public void open(){
this.liftState.open();
}
public void close(){
this.liftState.close();
}
public void run(){
this.liftState.run();
}
public void stop(){
this.liftState.stop();
}
}
Context 是一个环境角色,它的作用是串联各个状态的过渡,在LiftSate 抽象类中我们定义了并把这个环境角色聚合进来,并传递到了子类,也就是四个具体的实现类中自己根据环境来决定如何进行状态的过渡。
接下来的三个状态类ClosingStatus、runningStatus、stoppingStatus可以照葫芦画瓢,读者可以自行编写,这里不再赘述了。
最后就是具体业务实现场景了:
public class Client {
public static void main(String[] args) {
Context context = new Context();
//首先电梯是关闭着的
context.setLiftState(new ClosingStatus());
context.open();
context.run();
context.stop();
context.close();
}
}
Client 调用类太简单了,只要定义个电梯的初始状态,然后调用相关的方法,就完成了,完全不用考虑状态的变更
GOF里面对状态模式的描述是当一个对象内在状态改变时允许其改变行为,这个对象看起来像是改变了其类,
,也就是说状态模式封装的非常好,状态的变更引起了行为的变更。其通用类图如下:
状态模式首先避免了过多的 swith…case 或者 if…else 语句的使用,避免了程序的复杂性;其次是很好的使用体现了开闭原则和单一职责原则,每个状态都是一个子类,你要增加状态就增加子类,你要修改状态,你只修改一个子类就可以了;最后一个好处就是封装性非常好,这也是状态模式的基本要求,状态变换放置到了类的内部来实现,外部的调用不用知道类内部如何实现状态和行为的变换。
当然,这里状态间的过渡与切换逻辑是简单直接的
但是实际项目中大多数都是一个状态转换为几种状态:
举个实际例子来说,一些收费网站的用户就有很多状态,比如普通用户,普通会员,VIP 会员,白金级用户等等,这个状态的变更你不允许跳跃?!这不可能。比如电梯的这个栗子:我要一个正常的电梯运行逻辑,规则是开门->关门->运行->停止;还要一个紧急状态(比如火灾)下的运行逻辑,关门->停止,紧急状态电梯当然不能用了;再要一个维修状态下的运行逻辑,这个状态任何情况都可以,开着门电梯运行?可以!门来回开关?可以!永久停止不动?可以! 那这怎么实现呢?需要我们把已经有的几种状态按照一定的顺序再重新组装一下,那这个是什么模式?什么模式?对,建造模式+状态模式会起到非常好的封装作用。
我们应该或多或少做过工作流开发,工作流框架里面就应该有个状态机管理比如一个 Activity(节点)有初始化状态(Initialized State)、挂起状态(Suspended State)、完成状态(Completed State)等等,流程实例也是有这么多状态,那这些状态怎么管理呢?通过状态机(State Machine)来管理
- 状态机
22 Interpreter(解释器)模式
我们都知道,在银行、金融以及证券业务中,往往会有很多模型指标的运算,通过统计现有数据,通过海量大数据的运算回归,建立模型训练(现在很火爆的机器学习),从而预测未来可能发生的商业行为。比如建立一个模型公式,分析一个城市的消费倾向,进而影响银行的营销和业务扩张方向,一般的模型运算都有一个或多个运算公式,通常都是加减乘除的四则运算,偶尔也有开方、指数运算。但是具体到一个业务中,类似的模型公式是非常复杂的,公式有可能有十多个参数,而且上百个业务品各有不同的取参路径,同时关联表的数据数量一般都大于百万级。确实很复杂,今天就来看看这个模型公式怎么实现。
需求:输入一个模型公式,然后输入模型中的参数,运算出结果;
设计要求:
- 公式可以修改;符合正常算术书写方式,例如 a+b-c;
- 高扩展性,未来增加指数、开方、极限、求导等运算逻辑时,减少改动量;
- 效率可以不用考虑,晚间批量运算;
假设现在有三个业务种类:
业务种类 1 要求的公式:a+b+c-d;
业务种类 2 要求的公式:a+b+e-d;
业务种类 3 要求的公式:a-f;
其中 a、b、c、d、e、f 参数的值都可以取得,如果直接计算数值必须为每个品种写一个算法如果业务种类上双甚至上百个,该怎么写呢??所以建立公式,然后通过公式运算才是王道。
分析到这里,我们就可以先画一个简单的类图:
VarExpression 用来解析运算元素,每个运算元素对应了一个 VarExpression 对象。SybmolExpression 是负责运算符号解析的,分别有两个子类AddExpression(负责加法运算)和 SubExpression(负责减法运算)来实现。之后,我们还要考虑安排运行的先后顺序(加减法是不用考虑,但是乘除法呢?注意扩展性)。这里需要添加封装类来处理进行封装,我们就定义为Calculator 类:
Calcuator 的作用就是起到封装的作用,根据迪米特原则,Client 只和直接的朋友 Calcuator 交流,与其他类没关系。
每个类的职责各不尽相同:
先看Expression抽象类:
public abstract class Expression {
//解析公式和数值,其中var中的key值是是公式中的参数,value值是具体的数字
public abstract int interpreter(HashMap<String,Integer> var);
}
然后就是变量解析器:
public class VarExpression extends Expression {
private String key;
public VarExpression(String _key){
this.key = _key;
}
//从map中取之
public int interpreter(HashMap<String, Integer> var) {
return var.get(this.key);
}
}
其中输入参数为 HashMap 类型,key 值为模型中的参数,如 a、b、c 等,value 为运算时取得的具体数字,然后就是运算符号解析器:
public abstract class SymbolExpression extends Expression {
protected Expression left;
protected Expression right;
//所有的解析公式都应只关心自己左右两个表达式的结果
public SymbolExpression(Expression _left,Expression _right){
this.left = _left;
this.right = _right;
}
}
加减法的解析器如下:
public class AddExpression extends SymbolExpression {
public AddExpression(Expression _left,Expression _right){
super(_left,_right);
}
//把左右两个表达式运算的结果加起来
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) + super.right.interpreter(var);
}
}
public class SubExpression extends SymbolExpression {
public SubExpression(Expression _left,Expression _right){
super(_left,_right);
}
//左右两个表达式相减
public int interpreter(HashMap<String, Integer> var) {
return super.left.interpreter(var) - super.right.interpreter(var);
}
}
解析的工作已经完成,但是还有很大一段代码没有实现,还需要进行封装再调用,Calculator 类的代码如下:
public class Calculator {
//定义的表达式
private Expression expression;
//构造函数传参,并解析
public Calculator(String expStr){
//定义一个堆栈,安排运算的先后顺序
Stack<Expression> stack = new Stack<Expression>();
//表达式拆分为字符数组
char[] charArray = expStr.toCharArray();
//运算
Expression left = null;
Expression right = null;
for (int i = 0; i < charArray.length; i++){
switch(charArray[i]) {
//加法
case '+':
//加法结果放到堆栈中
left = stack.pop();
right = new VarExpression(String.valueOf(charArray[++i]));
stack.push(new AddExpression(left,right));
break;
case '-':
//减法结果放到堆栈中
left = stack.pop();
right = new VarExpression(String.valueOf(charArray[++i]));
stack.push(new SubExpression(left,right));
break;
default: //公式中的变量
stack.push(new VarExpression(String.valueOf(charArray[i])));
}
}
// 运算结果抛出来
this.expression = stack.pop();
}
//开始运算
public int run(HashMap<String,Integer> var){
return this.expression.interpreter(var);
}
}
为满足业务要求,设计了一个 Client 类来模拟用户情况,用户要求可以扩展,可以修改公式,通过接受键盘事件来处理:
public class Client {
//运行四则运算
public static void main(String[] args) throws IOException{
String expStr = getExpStr();
//赋值
HashMap<String,Integer> var = getValue(expStr);
Calculator cal = new Calculator(expStr);
System.out.println("运算结果为:"+expStr +"="+cal.run(var));
}
//获得表达式
public static String getExpStr() throws IOException{
System.out.print("请输入表达式:");
return (new BufferedReader(new InputStreamReader(System.in))).readLine();
}
//获得值映射
public static HashMap<String,Integer> getValue(String exprStr) throws IOException{
HashMap<String,Integer> map = new HashMap<String,Integer>();
//解析有几个参数要传递
for(char ch:exprStr.toCharArray()){
if(ch != '+' && ch != '-'){
if(!map.containsKey(String.valueOf(ch))){ //解决重复参数的问题
System.out.print("请输入" + ch + "的值:");
String in = (new BufferedReader(new InputStreamReader(System.in))).readLine();
map.put(String.valueOf(ch),Integer.valueOf(in));
}
}
}
return map;
}
}
其中 getExpStr 是从键盘事件中获得表达式,getValue 方法是从键盘事件中获得表达式中的元素映射值。
首先,要输入公式(表达式),其次输入变量参数。最后算出结果。我们随时都可以变化运算公式以及变量赋值。是不是类似、初中学习的“代数”这门课?先公式,然后赋值,运算出结果。;若需要扩展也非常容易,只要扩展增加 SymbolExpression 的子类就可以了。这就是解释器模式。
解释器模式(Interpreter Pattern)是一种按照规定语法进行解析的模式,在现在项目中使用较少(谁没事干会去写一个 PHP 或者 RUBY 的解析器)
其通用类图如下:
AbstractExpression:抽象解释器,具体的解释任务由各个实现类
TerminalExpression:终结符表达式,实现与文法中的元素相关联的解释操作。通常一个模式中只有一个终结符表达式,但是可以有多个实例,对应不同的终结符。
NonterminalExpression:非终结符表达式,每条规则对应一个非终结表达式。具体到实例中就是加减法规则分别对应到 AddExpression 和 SubExpression 两个类。
Context:环境角色,封装工具。具体到我们的例子中是采用 HashMap 代替。
总结:
- 解释器模式的优点: 是一个简单语法分析工具,它最显著的优点就是扩展性,修改语法规则只要修改相应的非终结
符表达式就可以了,若扩展语法,则只要增加非终结符类就可以了。 - 解释器模式的缺点: 首先,解释器模式会引起类膨胀。每个语法都要产生一个非终结符表达式,语法规则比较复杂时,就
可能产生大量的类文件,为维护带来了非常多的麻烦。效率问题,解释器模式由于使用了大量的循环和递归,效率是个不容忽视的问题,特别是用于解析复杂、冗长的语法时,效率是难以忍受的。 - 解释器模式的注意事项: 尽量不要在项目中使用解释器模式(没必要,而且把简单场景复杂化了)。可以使用 shell、JRuby、Groovy 等脚本语言来代替,完全可以满足模型分析过程。
23 Multition(多例)模式
多礼模式通常是指有上限的多例模式(没上限的多例模式和new一个对象没区别)。具体如何实现呢,先看类图:
上菜(代码)!
/**
* 皇帝类
* 明朝土木堡之变后,
* 明英宗被俘虏,明景帝即位,但是明景帝当上皇帝后乐疯了,竟然忘记把他老哥明英宗削为太上皇,
* 也就是在这一个多月的时间内,中国有两个皇帝!!!
*/
@SuppressWarnings("all")
public class Emperor {
//最多只能有连个皇帝
private static int maxNumOfEmperor = 2;
//皇帝叫什么名字
private static ArrayList emperorInfoList=new ArrayList(maxNumOfEmperor);
//装皇帝的列表;
private static ArrayList emperorList=new ArrayList(maxNumOfEmperor);
//正在被人尊称的是那个皇帝
private static int countNumOfEmperor =0;
//先把2个皇帝产生出来
static{
//把所有的皇帝都产生出来
for(int i=0;i<maxNumOfEmperor;i++){
emperorList.add(new Emperor("皇"+(i+1)+"帝"));
}
}
//就这么多皇帝了,不允许再推举一个皇帝(new 一个皇帝)
private Emperor(){
//世俗和道德约束你,目的就是不让你产生第二个皇帝
}
private Emperor(String info){
emperorInfoList.add(info);
}
public static Emperor getInstance(){
Random random = new Random();
//随机拉出一个皇帝
countNumOfEmperor = random.nextInt(maxNumOfEmperor);
return (Emperor)emperorList.get(countNumOfEmperor);
}
//皇帝叫什么名字呀
public static void emperorInfo(){
System.out.println(emperorInfoList.get(countNumOfEmperor));
}
}
/**
* 大臣类
* 大臣们悲惨了,一个皇帝都伺候不过来了,现在还来了两个个皇帝。不管了,找到个皇帝,磕头,请按就成了!
*/
@SuppressWarnings("all")
public class Minister {
public static void main(String[] args) {
int ministerNum =10; //10个大臣
for(int i=0;i<ministerNum;i++){
Emperor emperor = Emperor.getInstance();
System.out.print("第"+(i+1)+"个大臣参拜的是:");
emperor.emperorInfo();
}
}
}
运行结果如下:
看看,是不是很简单,直接一个getInstance(param)就解决了实例化对象的问题。多例模式中,我们规定好实例的具体个数(多个),然后不允许再多new出对象,在构建对象时,直接用getInstance()构建,方便又省事(类型间的转换可以不用考虑)。
总结: 六大设计原则
好了,总算把23种设计模式肝完了✌。当然,这只是一个对设计模式的归纳与总结,实际工作生产中要用到设计模式的地方有很多,还是要多学多应用多思考
- 可维护性
- 可复用性
- 可拓展性: 添加功能无需修改原来代码\少量修改原来代码
- 灵活性
设计原则
- 单一职责原则
- 里氏替换原则
- 开闭原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特法则