行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制来在类间分派行为,后者采用组合或聚合在对象间分配行为。由于组合关系或聚合关系比继承关系耦合度低,满足“合成复用原则”,所以对象行为模式比类行为模式具有更大的灵活性。
行为型模式分为:
- 模板方法模式
- 策略模式
- 命令模式
- 职责链模式
- 状态模式
- 观察者模式
- 中介者模式
- 迭代器模式
- 访问者模式
- 备忘录模式
- 解释器模式
以上11种行为型模式,除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式
解释器模式
解释器顾名思义,就是对语言进行解释,根据不同的语义来做不同的事情。笼统点来讲就是给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
在解释器模式中,我们需要将待解决的问题,提取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和+-符号组成的合法序列,“1+3-2” 就是这种语言的句子。
比如计算器就是根据输入的算式,去进行解析,并根据不同的运算符来不断进行计算。
使用场景
- 当语言的文法较为简单,且执行效率不是关键问题时
- 当问题重复出现,且可以用一种简单的语言来进行表达时
- 当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候
备忘录模式
备忘录模式又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原,很多软件都提供了撤销(Undo)操作,如 Word、记事本、Photoshop、IDEA等软件在编辑时按 Ctrl+Z 组合键时能撤销当前操作,使文档恢复到之前的状态;还有在浏览器中的后退键、数据库事务管理中的回滚操作、玩游戏时的中间结果存档功能、数据库与操作系统的备份操作、棋类游戏中的悔棋功能等都属于这类。
不过备忘录模式为了去保存对象的状态,会占用大量的资源,尤其是那种属性很多的对象,需要合理的使用才能保证程序稳定运行。
模版方法模式
模板方法模式可以定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
在面向对象程序设计过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但另外一些步骤的具体实现还未知,或者说另外一些步骤的实现与具体的环境相关,暂时还不能在这个类中实现。
例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
public abstract class BankClass { public final void workProcess() { this.getNum(); this.queue(); this.businessHandling(); this.evaluate(); } public void getNum() { System.out.println("取号"); }
public void queue() { System.out.println("排队"); }
public abstract void businessHandling();
public void evaluate(){ System.out.println("对工作人员打分"); } }
public class PersonA extends BankClass { @Override public void businessHandling() { System.out.println("取钱"); } }
public class PersonB extends BankClass { @Override public void pourVegetable() { System.out.println("存钱"); } }
public class Client { public static void main(String[] args) { AbstractBank A = new PersonA(); A.workProcess();
AbstractBank B = new PersonB(); B.cookProcess(); } }
|
注意:为防止恶意操作,一般模板方法都加上final
关键字
优缺点
优点:
缺点:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,提高了代码阅读的难度
适用场景
- 算法的整体步骤很固定,但其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现
- 需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制
责任链模式
责任链模式又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息,这增加了难度。这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。
举例,现需要开发一个请假流程控制系统。请假一天以下的假只需要小组长同意即可;请假1天到3天的假还需要部门经理同意;请求3天到7天还需要总经理同意才行。
public class LeaveRequest { private String name; private int num; private String content;
public LeaveRequest(String name, int num, String content) { this.name = name; this.num = num; this.content = content; }
public String getName() { return name; }
public int getNum() { return num; }
public String getContent() { return content; } }
public abstract class Handler { protected final static int NUM_ONE = 1; protected final static int NUM_THREE = 3; protected final static int NUM_SEVEN = 7;
private int numStart; private int numEnd; private Handler nextHandler;
public Handler(int numStart) { this(numStart, Integer.MAX_VALUE); }
public Handler(int numStart, int numEnd) { this.numStart = numStart; this.numEnd = numEnd; }
public void setNextHandler(Handler nextHandler) { this.nextHandler = nextHandler; }
public final void submit(LeaveRequest leave) { if (leave.getNum() >= this.numStart && leave.getNum() <= this.numEnd) { this.handleLeave(leave); System.out.println("流程结束"); } else if (null != this.nextHandler) { this.nextHandler.submit(leave); } else { System.out.println("无更高权限领导处理,流程终止"); } }
protected abstract void handleLeave(LeaveRequest leave); }
public class GroupLeader extends Handler { public GroupLeader() { super(Handler.NUM_ONE, Handler.NUM_THREE); }
@Override protected void handleLeave(LeaveRequest leave) { System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。"); System.out.println("小组长审批:同意。"); } }
public class Manager extends Handler { public Manager() { super(Handler.NUM_THREE, Handler.NUM_SEVEN); }
@Override protected void handleLeave(LeaveRequest leave) { System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。"); System.out.println("部门经理审批:同意。"); } }
public class GeneralManager extends Handler { public GeneralManager() { super(Handler.NUM_SEVEN); }
@Override protected void handleLeave(LeaveRequest leave) { System.out.println(leave.getName() + "请假" + leave.getNum() + "天," + leave.getContent() + "。"); System.out.println("总经理审批:同意。"); } }
public class Client { public static void main(String[] args) { LeaveRequest leave = new LeaveRequest("小花",5,"身体不适");
GroupLeader groupLeader = new GroupLeader(); Manager manager = new Manager(); GeneralManager generalManager = new GeneralManager();
groupLeader.setNextHandler(manager); manager.setNextHandler(generalManager);
groupLeader.submit(leave); } }
|
优缺点
优点:
降低了对象之间的耦合度
增强了系统的可扩展性
可以根据需要增加新的请求处理类,满足开闭原则
增强了给对象指派职责的灵活性
当工作流程发生变化,可以动态地改变链内的成员或者修改它们的次序,也可动态地新增或者删除责任
责任链简化了对象之间的连接
一个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的if或者if···else语句
责任分担
每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则
缺点:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用
命令模式
将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储、传递、调用、增加与管理
举例,去餐厅点单时,顾客只需要把订单交给服务器,然后等待上菜即可,不用关心菜是如何做出来的,是谁做的,对于厨师也相同,只需要按照订单做菜即可,不需要关心是谁点的。
public interface Command { void execute(); }
public class OrderCommand implements Command {
private SeniorChef receiver; private Order order;
public OrderCommand(SeniorChef receiver, Order order){ this.receiver = receiver; this.order = order; }
public void execute() { System.out.println(order.getDiningTable() + "桌的订单:"); Set<String> keys = order.getFoodDic().keySet(); for (String key : keys) { receiver.makeFood(order.getFoodDic().get(key),key); }
try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(order.getDiningTable() + "桌的饭弄好了"); } }
public class Order { private int diningTable;
private Map<String, Integer> foodDic = new HashMap<String, Integer>();
public int getDiningTable() { return diningTable; }
public void setDiningTable(int diningTable) { this.diningTable = diningTable; }
public Map<String, Integer> getFoodDic() { return foodDic; }
public void setFoodDic(String name, int num) { foodDic.put(name,num); } }
public class SeniorChef {
public void makeFood(int num,String foodName) { System.out.println(num + "份" + foodName); } }
public class Waitor {
private ArrayList<Command> commands;
public Waitor() { commands = new ArrayList(); } public void setCommand(Command cmd){ commands.add(cmd); }
public void orderUp() { System.out.println("新订单来了......."); for (int i = 0; i < commands.size(); i++) { Command cmd = commands.get(i); if (cmd != null) { cmd.execute(); } } } }
public class Client { public static void main(String[] args) { Order order1 = new Order(); order1.setDiningTable(1); order1.getFoodDic().put("西红柿鸡蛋面",1); order1.getFoodDic().put("小杯可乐",2);
Order order2 = new Order(); order2.setDiningTable(3); order2.getFoodDic().put("尖椒肉丝盖饭",1); order2.getFoodDic().put("小杯雪碧",1);
SeniorChef receiver=new SeniorChef(); OrderCommand cmd1 = new OrderCommand(receiver, order1); OrderCommand cmd2 = new OrderCommand(receiver, order2); Waitor invoker = new Waitor(); invoker.setCommand(cmd1); invoker.setCommand(cmd2);
invoker.orderUp(); } }
|
优缺点
优点:
- 降低系统的耦合度。命令模式能将调用操作的对象与实现该操作的对象解耦
- 增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,满足“开闭原则”,对扩展比较灵活
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令
- 方便实现 Undo 和 Redo 操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复
缺点:
- 使用命令模式可能会导致某些系统有过多的具体命令类
- 系统结构更加复杂
使用场景
- 系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
- 系统需要在不同的时间指定请求、将请求排队和执行请求
- 系统需要支持命令的撤销(Undo)操作和恢复(Redo)操作
策略模式
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。
策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
举例,一家百货公司在定年度的促销活动。针对不同的节日推出不同的促销活动,由促销员将促销活动展示给客户
代码如下:
public interface Strategy { void show(); }
public class StrategyA implements Strategy {
public void show() { System.out.println("买一送一"); } }
public class StrategyB implements Strategy {
public void show() { System.out.println("满200元减50元"); } }
public class StrategyC implements Strategy {
public void show() { System.out.println("满1000元加一元换购任意200元以下商品"); } }
public class SalesMan { private Strategy strategy; public SalesMan(Strategy strategy) { this.strategy = strategy; } public void salesManShow(){ strategy.show(); } }
public class Client { public static void main(String[] args) { SalesMan salesMan = new SalesMan(new StrategyA); salesMan.salesManShow(); } }
|
优缺点
优点:
策略类之间可以自由切换
由于策略类都实现同一个接口,所以使它们之间可以自由切换
易于扩展
增加一个新的策略只需要添加一个具体的策略类即可,基本不需要改变原有的代码,符合开闭原则
避免使用多重条件选择语句(if else),充分体现面向对象设计思想
缺点:
- 客户端必须知道所有的策略类,并自行决定使用哪一个策略类
- 策略模式将造成产生很多策略类,可以通过使用享元模式在一定程度上减少对象的数量
使用场景
- 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中
- 一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句
- 系统中各算法彼此完全独立,且要求对客户隐藏具体算法的实现细节时
- 系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的数据结构
- 多个类只区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为
迭代器模式
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示
举例,定义一个可以存储学生对象的容器对象,将遍历该容器的功能交由迭代器实现:-
定义迭代器接口,声明hasNext、next方法
public interface StudentIterator { boolean hasNext(); Student next(); }
|
定义具体的迭代器类,重写所有的抽象方法
public class StudentIteratorImpl implements StudentIterator { private List<Student> list; private int position = 0;
public StudentIteratorImpl(List<Student> list) { this.list = list; }
@Override public boolean hasNext() { return position < list.size(); }
@Override public Student next() { Student currentStudent = list.get(position); position ++; return currentStudent; } }
|
定义抽象容器类,包含添加元素,删除元素,获取迭代器对象的方法
public interface StudentAggregate { void addStudent(Student student);
void removeStudent(Student student);
StudentIterator getStudentIterator(); }
|
定义具体的容器类,重写所有的方法
public class StudentAggregateImpl implements StudentAggregate {
private List<Student> list = new ArrayList<Student>();
@Override public void addStudent(Student student) { this.list.add(student); }
@Override public void removeStudent(Student student) { this.list.remove(student); }
@Override public StudentIterator getStudentIterator() { return new StudentIteratorImpl(list); } }
|
优点:
- 支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式
- 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计
- 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足开闭原则要求
使用场景
- 当需要为聚合对象提供多种遍历方式时
- 当需要为遍历不同的聚合结构提供一个统一的接口时
- 当访问一个聚合对象的内容而无须暴露其内部细节的表示时
中介者模式
中介者模式也叫做调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。
比如我们想要去外面租房,但是我们怎么知道哪里有可以租的房子呢?于是我们就会上各大租房APP上去找房源,同样的,如果我们现在有房子需要出租,我们也不知道谁会想要租房子,同样的我们也会把房子挂在租房APP上展示,而当我们去租房时或是出租时,就会有一个称为中介的人来跟我们对接,实际上就是一种中介者模式。
案例实现:
public class Mediator { private final Map<String, User> userMap = new HashMap<>();
public void register(String address, User user){ userMap.put(address, user); }
public User find(String address){ return userMap.get(address); } }
|
用户有两种角色,一种是租房者,一种是出租者:
public class User { String name; String tel;
public User(String name, String tel) { this.name = name; this.tel = tel; } public User find(String address, Mediator mediator){ return mediator.find(address); }
@Override public String toString() { return name+" (电话:"+tel+")"; } }
|
测试类:
public static void main(String[] args) { User user0 = new User("小明", "10086"); User user1 = new User("小红", "10010"); Mediator mediator = new Mediator();
mediator.register("地址A", user0);
User user = user1.find("地址B", mediator); if(user == null) System.out.println("没有找到");
user = user1.find("地址A", mediator); System.out.println(user); }
|
中介者模式优化了原有的复杂多对多关系,而是将其简化为一对多的关系。
观察者模式
在Java中,一个对象的状态发生改变,可能就会影响到其他的对象,与之相关的对象可能也会联动的进行改变。还有我们之前遇到过的监听器机制,当具体的事件触发时,我们在一开始创建的监听器就可以执行相关的逻辑。可以使用观察者模式来实现这样的功能,当对象发生改变时,观察者能够立即观察到并进行一些联动操作。
观察者模式又被称为发布-订阅(Publish/Subscribe)模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态变化时,会通知所有的观察者对象,使他们能够自动更新自己。
我们先定义一个观察者接口:
public interface Observer { void update(); }
|
接着我们来写一个支持观察者的实体类:
public class Subject { private final Set<Observer> observerSet = new HashSet<>();
public void observe(Observer observer) { observerSet.add(observer); }
public void modify() { observerSet.forEach(Observer::update); } }
|
接着我们就可以测试一下了:
public static void main(String[] args) { Subject subject = new Subject(); subject.observe(() -> System.out.println("我是一号观察者!")); subject.observe(() -> System.out.println("我是二号观察者!")); subject.modify(); }
|
这样,我们就简单实现了一下观察者模式,JDK也提供了实现观察者模式相关的接口:
import java.util.Observable;
public class Subject extends Observable {
public void modify(){ System.out.println("对对象进行修改!"); this.setChanged(); this.notifyObservers(new Date()); } }
|
编写测试类:
public static void main(String[] args) { Subject subject = new Subject(); subject.addObserver((o, arg) -> System.out.println("观察者1:监听到变化,并得到参数:" + arg)); subject.addObserver((o, arg) -> System.out.println("观察者1:监听到变化,并得到参数:" + arg)); subject.addObserver((o, arg) -> System.out.println("观察者1:监听到变化,并得到参数:" + arg)); subject.modify(); }
|
状态模式
在标准大气压下,水在0度时会结冰变成固态,在0-100度之间时,会呈现液态,100度以上会变成气态,水这种物质在不同的温度下呈现出不同的状态,而Java中的对象,可能也会像这样存在很多种状态,甚至在不同的状态下会有不同的行为,就可以通过状态模式来实现。
状态模式可以对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
举例,假设有一个电灯开关,电灯有两种状态:开和关。按下开关时,电灯会根据当前状态切换行为:
如果电灯是“开”状态,按下开关后会关闭电灯。
如果电灯是“关”状态,按下开关后会打开电灯。
状态抽象类:
public interface State { void handle(); }
|
具体状态类:
public class OnState implements State { @Override public void handle() { System.out.println("电灯已打开"); } }
public class OffState implements State { @Override public void handle() { System.out.println("电灯已关闭"); } }
|
电灯开关:
public class LightSwitch { private State currentState;
public LightSwitch() { this.currentState = new OffState(); }
public void setState(State state) { this.currentState = state; }
public void pressSwitch() { currentState.handle(); if (currentState instanceof OffState) { setState(new OnState()); } else if (currentState instanceof OnState) { setState(new OffState()); } } }
|
测试类:
public class Client { public static void main(String[] args) { LightSwitch lightSwitch = new LightSwitch(); lightSwitch.pressSwitch(); lightSwitch.pressSwitch(); } }
|
状态模式更加强调当前的对象所处的状态,我们需要根据对象不同的状态决定其他的处理逻辑。
访问者模式
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作。
举例,现在养宠物的人特别多,宠物分为狗,猫等,要给宠物喂食的话,主人可以喂,其他人也可以喂食。
创建抽象访问者接口
public interface Person { void feed(Cat cat);
void feed(Dog dog); }
|
创建不同的具体访问者角色(主人和其他人),都需要实现 Person
接口
public class Owner implements Person {
@Override public void feed(Cat cat) { System.out.println("主人喂食猫"); }
@Override public void feed(Dog dog) { System.out.println("主人喂食狗"); } }
public class Someone implements Person { @Override public void feed(Cat cat) { System.out.println("其他人喂食猫"); }
@Override public void feed(Dog dog) { System.out.println("其他人喂食狗"); } }
|
定义抽象节点——宠物:
public interface Animal { void accept(Person person); }
|
定义实现Animal
接口的具体节点:
public class Dog implements Animal { @Override public void accept(Person person) { person.feed(this); System.out.println("汪汪!"); } }
public class Cat implements Animal { @Override public void accept(Person person) { person.feed(this); System.out.println("喵喵喵!"); } }
|
定义对象结构,即主人的家:
public class Home { private List<Animal> nodeList = new ArrayList<Animal>();
public void action(Person person) { for (Animal node : nodeList) { node.accept(person); } }
public void add(Animal animal) { nodeList.add(animal); } }
|
测试类:
public class Client { public static void main(String[] args) { Home home = new Home(); home.add(new Dog()); home.add(new Cat());
Owner owner = new Owner(); home.action(owner);
Someone someone = new Someone(); home.action(someone); } }
|
不同的访问者对于某一件事务的处理可能会不同。访问者模式把数据结构和作用于结构上的操作解耦,使得操作集合可相对自由地演化。