行为设计模式包括:观察者模式、责任链模式。
1、观察者模式(Observer)
观察者模式允许你定义一种订阅机制, 可在对象状态改变或事件发生时通知多个“观察” 该对象的其他对象。这种模式有时又称作发布-订阅模式、模型-视图模式,比如顾客对商店里的苹果很感兴趣,他会每天到店里查看有没有优惠活动,这样其实很浪费时间,这种情况就可以使用观察者模式:对苹果感兴趣的顾客作为观察者来向商店订阅苹果的活动消息,当商店的苹果搞活动的时候就通知所有订阅的顾客,顾客自行处理该通知。
//抽象观察者 interface Observer { void response(); //订阅的事件发生,进行反应 } //具体观察者 class ConcreteObserver implements Observer { public void response() { System.out.println("观察者作出反应!"); } } class Subject { protected List<Observer> observers = new ArrayList<Observer>(); public void add(Observer observer) //增加观察者 { observers.add(observer); } public void remove(Observer observer) //删除观察者 { observers.remove(observer); } public void notifyObserver() //通知所有观察者 { for (Observer obs : observers) { obs.response(); } } }
2、责任链模式
责任链模式允许你将请求沿着处理者链进行发送,处理者收到请求后均可对请求进行处理, 或将其传递给链上的下个处理者。比如开发一个网站系统,要求访问者为大陆人且为认证用户,一开始我们直接在代码里添加相关的判断处理,后来又要添加过滤来自同一 IP 地址的重复错误请求,后来又有人提议你可以对包含同样数据的重复请求返回缓存中的结果,你又要增加一个检查步骤。检查代码本来就已经混乱不堪, 而每次新增功能都会使其更加臃肿。 修改某个检查步骤有时会影响其他的检查步骤。 最糟糕的是, 当你希望复用这些检查步骤来保护其他系统组件时,你只能复制部分代码, 因为这些组件只需部分而非全部的检查步骤。
可以看到上面的这些检查必须依次进行,而且有一项检查失败, 那就没有必要进行后续检查了,这就适合使用责任链模式。再比如,我们要报销一个金额的话,需要网上层层报批,组长、经理、CFO拥有的审批金额权限不同,小于1000的报销组长可以直接审批而不用再报给经理,我们可以这样写代码来实现这个报销功能:
abstract class Handler{ //处理者接口 abstract boolean handle(int money); void setNextHandler(Handler nextHandler){ this.nexthandler = nextHandler; } protected Handler nexthandler; } class GroupLeader extends Handler{ //组长 boolean handle(int money){ if(money < 1000) { return true; } if(nexthandler != null) return nexthandler.handle(money); return false; } } class Manager extends Handler{ //经理 boolean handle(int money){ if(money < 2000) { return true; } if(nexthandler != null) return nexthandler.handle(money); return false; } } class CFO extends Handler{ //CFO boolean handle(int money){ if(money < 5000) { return true; } if(nexthandler != null) return nexthandler.handle(money); return false; } } public class Main { public static void main(String[] args){ CFO cfo = new CFO(); //CFO最大,为最终的处理者,不用设置下一处理者 Manager manager = new Manager(); manager.setNextHandler(cfo); //设置下一处理者 GroupLeader leader = new GroupLeader(); leader.setNextHandler(manager); //设置下一处理者 boolean b = leader.handle(4500); //申请4500的报销金 } }
3、命令模式
现在在一个软件中可以通过按钮、菜单、快捷键来保存数据,如下所示,一般我们是在这些GUI对象中调用业务逻辑对象的对应方法来保存数据。
如果使用命令模式的话,不建议 GUI 对象直接提交这些请求,而是将请求的所有细节 (例如调用的业务逻辑对象、方法名称和参数列表) 抽取出来组成命令类, 该类中仅包含一个用于触发请求的方法,这样GUI对象通过命令来执行对应的操作,而不是直接使用业务逻辑对象调用其方法,如下所示:
除了保存数据功能,业务逻辑里也许还有其他功能方法,如打开操作、打印操作,这些操作都是每个对应一个命令类,而且这些命令通常是实现相同的一个抽象命令接口,该接口通常只有一个没有任何参数的执行方法,这样就能在不和具体命令类耦合的情况下使用一个请求发送者来执行不同命令,而且可以在运行时切换命令对象, 以改变功能行为。这样对于按钮、菜单、快捷键来说因为是相同的功能,所以只需要一个命令对象,这样可以避免重复代码。对于需要参数的功能方法,可以在命令类的构造方法中将其传入,或者在命令类中添加一个设置参数数据的方法。如下所示:
命令模式的代码示例:
interface Command{ //命令接口(抽象类) public abstract void execute(); } class CommandA implements Command{ //具体命令 CommandA(){ receiver = new Receiver(); } public void execute() { receiver.actionA(); } private Receiver receiver; //命令中要包含接受者来执行命令 } class Receiver{ //命令接受者中包含命令对应的执行方法 public void actionA() {} } class Invoker{ //调用者通过具体命令来执行对应的方法 Invoker(Command cmd){ this.cmd = cmd; } void setCommand(Command cmd){ this.cmd = cmd; } void Fun(){ cmd.execute(); } private Command cmd; }
从上面的内容可以看出,使用命令减少了 GUI 和业务逻辑层之间的耦合,就像你使用遥控器遥控电视一样,而不用你直接操作电视,你就相当于是命令的调用者或者称命令的发送者,遥控器上有多个按钮,换台、增大音量等,这就是多个命令,你通过命令(按钮)来控制电视机(命令接受者)的功能。
命令模式的适用场景:①、命令模式中将特定方法转化为了独立的对象,这样就可以将命令进行保存、当做参数传递、或者在运行时切换已连接的命令,比如用户可以在软件运行中对于一个GUI对象配置不同的功能。
②、因为命令被实现为了对象,所以对于命令可以实现序列化,将命令写入文件、记录命令、放入队列、或者通过网络发送命令,你也可以方便的延迟或计划命令的执行。
③、想要实现回滚或者事务功能,可以使用命令模式 ,对于 Undo 和 Redo 操作,命令模式可以与备忘录模式结合,实现命令的撤销与恢复。比如编辑器一般都支持ctrl + z来回滚,一些软件提供撤回到上一步的操作,我们可以将用户的操作实现为命令,当执行操作的时候将命令保存到栈中,当执行撤回功能的时候就从栈中弹出最近的命令。
4、备忘录模式
备忘录模式又称快照模式,上面说过命令模式可以与备忘录模式结合,实现命令的撤销。比如当前有一个编辑器,想要执行撤销的话就必须先备份当前编辑器的状态,如当前文本,光标等信息,保存这种信息的类就叫快照(备忘录)。快照模式建议在编辑器中来创建快照(因为可以很方便的获得当前编辑器的信息来设置快照内容),在命令中拥有一个快照对象来管理快照,执行撤销功能的时候就执行命令的undo()撤销方法后把命令从栈中弹出:
class Snapshot{ //快照 private Editor editor; //编辑器 private String strText; //文本 int cursorPos; //光标位置 Snapshot(Editor editor, String strText, int cursorPos){ this.editor = editor; this.strText = strText; this.cursorPos = cursorPos; } void restore(){ //恢复编辑器到当前快照状态 editor.setText(strText); editor.setCursorPos(cursorPos); } } class Editor{ //编辑器,是快照的发起人(创建者),又称原发器 Snapshot createSnapshot(){ //创建快照 return new Snapshot(this, getText(), getCursorPos()); } String getText(){ return "";} //获得当前编辑器文本 void setText(String str){}; //设置当前编辑器文本 int getCursorPos(){return 0;} //获得当前光标位置 void setCursorPos(int pos){} //设置当前光标位置 } class EditCommand{ //编辑命令,是快照的管理者(拥有快照),又称负责人 private Snapshot backup; Editor editor; void setSnapshot(){ //获得快照 backup = editor.createSnapshot(); } void undo(){ //撤销命令,即设置编辑器为快照时的状态 backup.restore(); } }
5、迭代器模式
对于C++中的容器或者Java中的集合,当我们想要遍历它们的时候,可以选择使用迭代器,通过迭代器不仅可以获得当前元素,它还包含一些其他的方便方法,比如当前位置,距离末尾的大小等。迭代器通常会提供一个获取集合元素的基本方法,如next(),客户端可不断调用该方法直至它不返回任何内容, 这意味着迭代器已经遍历了所有元素。
迭代器隐藏了集合背后复杂的数据结构,为客户端提供多个访问集合元素的简单方法,而且能避免客户端在直接与集合交互时执行错误或有害的操作,从而起到保护集合的作用,满足 “开闭原则” 。
将迭代算法放置在程序业务逻辑中时, 它会让原始代码的职责模糊不清, 降低其可维护性,因此, 将遍历代码移到特定的迭代器中可使程序代码更加精炼和简洁, 满足 “单一职责原则” 。
迭代器模式为集合和迭代器提供了一些通用接口,所以如果你使用的是通用的接口,那么就可不必拘泥于具体的类型,只要是实现了这些接口的集合或迭代器就行。
6、中介者模式
现在有一个对话框,上面有文本输入栏、复选框、确定按钮等控件,当复选框点击的时候需要文本输入栏上显示一段文本,当确定按钮点击的时候需要获得用户设置后清空所有控件的输入,如果我们把复选框点击、按钮的点击这些控件的事件处理放到控件中去的话会使控件变的与其它控件高度耦合,且不可复用。推荐的做法是当按钮点击的时候通知对话框,让对话框来处理指定事件,这样对话框就好像一个中介者一样。你还可以为对话框设计一个通用的接口,接口中将声明一个所有表单元素都能使用的通知方法, 可用于将元素中发生的事件通知给对话框, 这样一来, 所有实现了该接口的对话框都能使用这个来处理元素事件了。
中介者模式建议你停止组件之间的直接交流并使其相互独立。 这些组件必须调用特殊的中介者对象, 通过中介者对象重定向调用行为, 以间接的方式进行合作。 最终, 组件仅依赖于一个中介者类, 无需与多个其他组件相耦合。
7、状态模式
程序中可能会有几种状态,而且一个时间段中程序只能是一种状态之一,程序可以从当前状态切换到另一个状态,状态一般是一个或一组成员变量来表示。典型的代表是有限状态机,状态机通常由众多条件运算符(if或switch)构成,随着状态的增加,代码会变得越来越复杂和臃肿。
状态模式建议为每个具体的状态新建一个类,状态对应的行为放到这个类中,应该创建一个抽象状态类来定义状态的通用属性和行为。我们还应该创建一个原始的对象来保存一个指向表示当前状态的状态对象的引用, 且将所有与状态相关的工作委派给该对象,这个原始对象类称为“上下文”或“环境”。
如下所示示例,当前有三种状态,小于60分为不及格,大于60小于90为中等,大于90为优秀,我们在其中定义了可能会改变当前状态的add()方法,add()方法中会使用当前状态对象来执行对应的工作(addScore()方法),如果需要改变状态的话就直接在当前状态类中进行(checkState()方法):
class ScoreContext { //上下文 private AbstractState state; ScoreContext() { state = new LowState(this); } public void add(int score) { state.addScore(score); } } abstract class AbstractState { //抽象状态类 protected ScoreContext hj; //上下文 protected String stateName; //状态名 protected int score; //分数 public abstract void checkState(); //检查当前状态 public void addScore(int x) { score += x; checkState(); } } class LowState extends AbstractState { //具体状态类:不及格 public LowState(ScoreContext h) { hj = h; stateName = "不及格"; score = 0; } public LowState(AbstractState state) { hj = state.hj; stateName = "不及格"; score = state.score; } public void checkState() { if (score >= 90) { hj.setState(new HighState(this)); } else if (score >= 60) { hj.setState(new MiddleState(this)); } } } class MiddleState extends AbstractState { //具体状态类:中等 public MiddleState(AbstractState state) { hj = state.hj; stateName = "中等"; score = state.score; } public void checkState() { if (score < 60) { hj.setState(new LowState(this)); } else if (score >= 90) { hj.setState(new HighState(this)); } } } class HighState extends AbstractState { //具体状态类:优秀 public HighState(AbstractState state) { hj = state.hj; stateName = "优秀"; score = state.score; } public void checkState() { if (score < 60) { hj.setState(new LowState(this)); } else if (score < 90) { hj.setState(new MiddleState(this)); } } } public class Main { public static void main(String[] args){ ScoreContext account = new ScoreContext(); account.add(30); //小于60分,当前状态为不及格 account.add(40); //大于60分小于90分,当前状态为中等 account.add(25); //大于90分,当前状态为优秀 account.add(-15); //大于60分小于90分,当前状态为中等 } }
状态模式与策略模式类似,但不同的是在状态模式中, 一个状态知道其他所有状态的存在, 且能触发从一个状态到另一个状态的转换; 策略则几乎完全不知道其他策略的存在。
8、策略模式
软件开发中经常会根据不同的情况来选择不同的方法,比如出行旅游应用可以让客户选择乘坐飞机、乘坐火车、自己开私家车等。如果使用if...else 语句、switch...case这些条件转移语句来选择方法的话,会使条件语句代码变得很复杂,而且后期需要修改或者删除指定方法的话要修改原代码,甚至可能会对其它方法产生影响,这样就不易维护。
策略模式将每个算法封装成单独的类,然后像状态模式一样定义一个名为“上下文(环境)”的原始类,在上下文中包含一个成员变量来存储每种方法类,客户端将所需要的指定方法类传给上下文,由上下文来调用具体的方法类。策略模式将每个算法封装起来,且算法的变化不会影响使用算法的客户,将使用算法的责任和算法的实现分割开来。
interface Strategy { //抽象策略(方法)类 public void strategyMethod(); //策略方法 } class ConcreteStrategyA implements Strategy {//具体策略(方法)类A public void strategyMethod() { System.out.println("具体策略A的策略方法被调用!"); } } class ConcreteStrategyB implements Strategy { //具体策略(方法)类B public void strategyMethod() { System.out.println("具体策略B的策略方法被调用!"); } } class Context { //上下文(环境)类 private Strategy strategy; //保存具体策略方法 public void setStrategy(Strategy strategy) { //设置具体的策略方法 this.strategy = strategy; } public void execMethod() { //执行设置的策略方法 strategy.strategyMethod(); } } public class Main { public static void main(String[] args){ Context c = new Context(); //执行A方法 Strategy sa = new ConcreteStrategyA(); c.setStrategy(sa); c.execMethod(); //执行B方法 Strategy sb = new ConcreteStrategyB(); c.setStrategy(sb); c.execMethod(); } }
9、模板模式
一些功能类经常有相似的方法,它们可能是功能相同,实现不同的方法,也可能有些方法是通用的,即可以只保留一份代码。模板模式建议将功能相同但实现不同的方法抽象到抽象父类中(这些抽象类中的方法对C++来说就是虚函数或纯虚函数,对于Java来说就是抽象方法),将相同代码的方法也统一定义到抽象父类中去。
10、访问者模式
假设我们有一个功能类已经成功上线,而后产品又提出了一个需求,这时候我们可能不想动线上功能类的代码以防出问题,或者新的需求虽然跟功能类相关但并不适合放到功能类里,这个时候我们可以新建一个访问者类,通过访问者类来访问原功能类(当然我们应该将功能类对象传给访问者),这样在访问者中只能通过调用原功能类的方法来获得或设置原功能类,而不是直接在原功能类中修改代码,更加安全。如下所示的Circle为原功能类,现在想要新增一个获得Circle的圆心后进行其他处理的功能,我们可以将新功能的代码放到一个访问者类中,如下即为handleCircle()方法:
class Circle{ //原功能类 point pt; //圆心位置 point getPoint(){ return pt; } ...... } class Visitor{ //访问者类 void handleCircle(Circle cl){ //实现新的功能的方法 //获得圆心后进行其它处理 point pt = cl.getPoint(); ...... } } public class Main { public static void main(String[] args){ Circle cl = new Circle(); Visitor v = new Visitor(); v.handleCircle(cl); //执行新的功能 } }
上面是仅有一个功能类的情况,如果原来我们有一系列的功能类的话,现在需要对这些类都进行新功能的增加,那么参考上面的实现,代码就为如下所示:
class Circle{ //原功能类:圆形 point pt; //圆心位置 point getPoint(){ return pt; } ...... } class Triangle{ //原功能类:三角形 int height; //高 int getHeight(){ return height;} ...... } class Rectangle{ //原功能类:矩形 pair<int, int> len; //长和宽 pair<int, int> getLen(){ return len; } ...... } class Diamond{} ////原功能类:菱形 ...... class Visitor{ //访问者类 void handleCircle(Circle cl){ //实现新功能的方法,针对圆形 point pt = cl.getPoint(); //获得圆形的圆心后进行其它处理 ...... } void handleTriangle(Triangle tr){ //实现新功能的方法,针对三角形 int height = tr.getHeight(); //获得三角形的高后进行其他的处理 ...... } void handleRectangle(Rectangle rect){ //实现新功能的方法,针对矩形 pair<int, int> len = rect.getLen(); //获得矩形的边长后进行其他的处理 ...... } } public class Main { ArrayList<Shape> list; //列表中有圆形、三角形、矩形...... public static void main(String[] args){ Visitor visitor = new Visitor(); for(Shape item : list){ //遍历列表对各个形状进行新功能的调用 if (item instanceof Circle) //圆形 visitor.handleCircle((Circle)item); else if (item instanceof Triangle) //三角形 visitor.handleTriangle((Triangle)item); else if (item instanceof Rectangle) //矩形 visitor.handleRectangle((Rectangle)item); else if ...... } } }
可以看到,如果有多个形状的话,需要使用if-else或switch来判断每个形状的具体类型,代码很繁琐。我们可以给原功能类添加一个acceptVisit()方法,在这个方法中根据当前对象类型来调用访问者中相应的方法(当然我们要给这个方法传入访问者对象):
class Shape{ ...... void acceptVisit(Visitor v){} } class Circle{ //原功能类:圆形 point pt; //圆心位置 point getPoint(){ return pt; } ...... void acceptVisit(Visitor v){ v.handleCircle(this); } } class Triangle{ //原功能类:三角形 int height; //高 int getHeight(){ return height;} ...... void acceptVisit(Visitor v){ v.handleTriangle(this); } } class Rectangle{ //原功能类:矩形 pair<int, int> len; //长和宽 pair<int, int> getLen(){ return len; } ...... void acceptVisit(Visitor v){ v.handleRectangle(this); } } class Diamond{} //原功能类:菱形 ...... class Visitor{ //访问者类 void handleCircle(Circle cl){ //实现新功能的方法,针对圆形 point pt = cl.getPoint(); //获得圆形的圆心后进行其它处理 ...... } void handleTriangle(Triangle tr){ //实现新功能的方法,针对三角形 int height = tr.getHeight(); //获得三角形的高后进行其他的处理 ...... } void handleRectangle(Rectangle rect){ //实现新功能的方法,针对矩形 pair<int, int> len = rect.getLen(); //获得矩形的边长后进行其他的处理 ...... } } public class Main { ArrayList<Shape> list; //列表中有圆形、三角形、矩形... public static void main(String[] args){ Circle cl = new Circle(); for(Shape item : list){ item.acceptVisit(cl); //acceptVisit()为Shape中函数,圆形、三角形、矩形等都重写了该方法 } } }