行为型模式
行为型模式用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎么相互协同共同完成单个对象都无法单独完成的任务,它设计算法于对象间职责的分配。
行为型模式分为类行为模式和对象行为模式,前者采用继承机制在类间分派行为。由于组合关系或聚合关系比继承关系耦合度低,满足”合成复用原则“,所以对象行为模式比类行为模式具有更大的灵活性。
行为型模式分为:模板方法模式,策略模式,命令模式,职责链模式,状态模式,观察者模式,中介者模式,迭代器模式,访问者模式,备忘录模式,解释器模式
除了模板方法模式和解释器模式是类行为型模式,其他的全部属于对象行为型模式
模板方法模式
概述
在面向对象程序设计的过程中,程序员常常会遇到这种情况:设计一个系统时知道了算法所需的关键步骤,而且确定了这些步骤的执行顺序,但某些步骤的实现与具体的环境有关
例如,去银行办理业务一般要经过以下4个流程:取号,排队,办理具体业务,对银行工作人员进行评分等,其中取号,排队和对银行工作人员进行评分对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款,取款或者转账等,可以延迟到子类中实现。
定义:定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些步骤
模板方法模式包含的角色:
抽象类:负责给出一个算法的轮廓和骨架,它由一个模板方法和若干个基本方法构成。
模板方法:定义了算法的骨架,按某种顺序调用其包含的基本方法。
基本方法:是实现算法各个步骤的方法,是模板方法的组成部分,基本方法又分为三种。
抽象方法:一个抽象方法由抽象类声明,由其具体子类实现。
具体方法:一个具体方法由一个抽象类或具体类声明并实现,其子类可以覆盖或继承
钩子方法:在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法
一般的钩子方法是用于判断的逻辑方法,该类方法名一般为isXxx,返回值boolean类型
具体子类:实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤
案例实现
例子:炒菜
炒菜的步骤:倒油,热油,倒蔬菜,倒调料,翻炒等步骤,模拟如下
代码
抽象类
public abstract class AbstractClass {
//模板方法
public final void cookProcess() {
pourOil();
heatOil();
pourVegetable();
pourSource();
fry();
}
//基本方法
public void pourOil() {
System.out.println("倒油!");
}
public void heatOil() {
System.out.println("热油!");
}
public void fry() {
System.out.println("翻炒!");
}
//抽象方法.
public abstract void pourVegetable();
public abstract void pourSource();
}
具体子类
public class ConcreteClass_BaoCai extends AbstractClass {
@Override
public void pourVegetable() {
System.out.println("倒包菜!");
}
@Override
public void pourSource() {
System.out.println("倒调料!");
}
}
测试
优缺点和使用场景
优点:
提高代码复用性:将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
实现了反向控制:通过一个父类调用其子类的操作,通过对子类的具体的实现扩展不同的行为。符合开闭原则。
缺点:
对每个不同的实现都需要定义一个子类,这回导致类的个数增加,系统更加庞大,设计也更加抽象
父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,提高了代码阅读的难度
使用场景:
算法的整体步骤很固定,d按其中个别部分易变时,这时候可以使用模板方法模式,将容易变的部分抽象出来,供子类实现。
需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。
JDK源码解析-InputStream
InputStreanm类就使用了模板方法模式,在InputStream类中定义了多个read()方法,如下
抽象方法,让子类去实现
模板方法
算法的骨架,调用了子类实现的抽象方法
策略模式
概述
先看下面的图片,我们去旅游选择出行模式有很多种,可以骑自行车,坐汽车,火车,飞机
而作为程序猿,开发需要选择一款开发工具,当然可以进行代码开发的工具由很多,可以选择Idea进行开发,也可以是Eclipse....
定义:
策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理
策略模式的主要角色:
抽象策略类:这是一个抽象角色,通常有一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
具体策略类:实现了抽象策略定义的接口,提供了具体的算法实现或行为。
环境类:持有一个策略类的引用,最终给客户端调用
案例实现
例子:促销活动
一家百货公司制定年度促销活动,针对不同的节日推出不同的促销活动,由促销员将活动展示
代码:
抽象策略类
public interface Strategy {
void show();
}
具体策略类
public class StrategyA implements Strategy {
@Override
public void show() {
System.out.println("买一送一");
}
}
环境类
public class SaleMan {
private Strategy strategy;
public SaleMan(Strategy strategy) {
this.strategy = strategy;
}
//促销员展示促销活动
public void saleManShow() {
strategy.show();
}
}
测试
优缺点和使用场景
优点:
策略类之间可以自由切换:都实现同一个接口,可以自由切换
易于扩展:增加新的策略只需添加一个具体的策略类,不需要修改原有代码,符合开闭原则
避免使用多重条件选择语句(if,else),充分体现面向对象设计原则
缺点:
客户端必须知道所有的策略类,并自行决定使用哪一个策略类
策略模式将造成参审很多策略类,可以通过享元模式在一定程度上减少对象的数量
使用场景:
一个系统需要动态的在几种算法中选择一种时,可将每个算法封装到策略类中。
一个类定义了多种行为,并且这些行为在这个类的操作中以多个条件语句的形式出现,可将每个条件分支移入它们各自的策略类中以代替这些条件语句
系统中个算法彼此独立,且要求对客户隐藏具体算法的实现细节时
系统要求使用算法的客户不应该知道其操作的数据时,可使用策略模式来隐藏与算法相关的结构
多个类之区别在表现行为不同,可以使用策略模式,在运行时动态选择具体要执行的行为
JDK源码解析-Comparator
Comparator中的策略模式,在Arrays类中有一个Sort()方法,如下
Arrays就是一个环境角色类,这个sort方法可以传一个新策略让Arrays根据这个算法进行排序
Comparator是抽象策略角色,而new的匿名实现类是具体策略角色
那么Arrays类的sort方法到底有没有使用我们提供的具体策略角色的compare方法?查看TimSort类
确实用到了
命令模式
概述
日常生活中,我们出去吃饭会遇到下面的场景
定义:
将一个请求封装为一个对象,使其发出请求责任和执行请求的责任分割开,这样两者之间通过命令对象进行沟通,这样方便将命令对象进行存储,传递,调用,增加与管理。
命令模式包含的角色:
抽象命令角色:定义命令的接口,声明执行的方法。
具体命令角色:具体的命令,实现命令接口;通常会持有接收者,并调用接收者的功能来完成命令要执行的操作
实现者/接收者角色:接收者,真正执行命令的对象,任何类都可能成为一个接收者,只要它能够实现命令要求实现的相应功能
调用者/请求者角色:要求命令执行请求,通常会持有命令对象,可以持有很多的命令对象。这个是客户端真正触发命令并要求命令执行相关操作的地方,也就是说相当于使用命令对象的入口
案例实现
服务员:调用者角色,由她来发起命令
大厨:接收者角色,真正命令执行的对象
订单:命令中包含订单
代码
抽象命令角色
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;
}
@Override
public void execute() {
System.out.println(order.getDiningTable() + "桌的订单开始制作!");
Map<String, Integer> foodDir = order.getFoodDir();
Set<String> keys = foodDir.keySet();
for (String key : keys) {
Integer num = foodDir.get(key);
receiver.makeFood(num, key);
}
System.out.println(order.getDiningTable() + "桌的订单制作完毕!");
}
}
实现者
public class SeniorChef {
public void makeFood(int num, String foodName) {
System.out.println("厨师做" + num + "份" + foodName);
}
}
调用者
public class Waiter {
//持有多个命令对象
private List<Command> commands = new ArrayList<>();
public void setCommand(Command cmd) {
commands.add(cmd);
}
//发起命令的方法
public void orderUp() {
System.out.println("新订单来喽");
//遍历命令集合
for (Command command : commands) {
if (command != null) {
command.execute();
}
}
}
}
测试
优缺点和使用场景
优点:
降低系统的耦合度,命令模式能够将调用操作的对象与实现该操作的对象解耦
增加或删除命令非常方便,不会影响其他类,满足开闭原则,对扩展比较灵活
可以实现宏命令,命令模式可以和组合模式结合,将多个命令装配成一个组合命令,即宏命令
方便实现Undo和Redo操作,命令模式可以与后面的备忘录模式进行结合,实现命令的撤销与恢复
缺点:
使用命令模式可能会导致某些系统有过多的具体命令类
系统结构更加复杂
使用场景:
系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互
系统需要在不同的时间指定请求,将请求排队和执行请求
系统需要支持命令的撤销和恢复操作
JDK源码解析-Runnable
Runnable是一个典型命令模式,Runnable担当命令的角色,Thread充当的是调用者,start方法就是其执行方法
抽象命令接口
Thread类中聚合我们的具体命令角色
在start方法中调用一个start0()方法
start0()会调用系统方法,开启一个线程,而接收者是对程序员开发的,可以自定义
具体命令角色中自定义接收者
测试
责任链模式
概述
在现实生活中,常常会出现这样的情况,一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。例如,公司员工请假,可批准的领导有部门负责人,副总经理,总经理等,但每个领导能批准的天数不同,员工必须根据自己要请假的天数找不同的领导签名,也就是说员工必须记住每个领导的姓名,电话和地址等信息,这增加了难度,这样的例子还有很多,如找领导出差报销,击鼓传花游戏等。
定义:
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有的请求处理这通过前一个对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
职责链模式包含的角色:
抽象处理者角色:定义一个处理请求的接口,包含抽象处理方法和一个后续连接
具体处理者角色:实现抽象处理者的处理方法,判断能否处理本次请求,可以处理则处理,否则将请求传给它的后继者
客户类角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程
案例实现
例子:请假流程控制系统
代码
抽象处理者
public abstract class Handler {
protected static final int NUM_ONE = 1;
protected static final int NUM_THREE = 3;
protected static final int NUM_SEVEN = 7;
protected static final int NUM_FOUR = 30;
//定义该领导处理的请假天数区间
private int numStart;
private int numEnd;
//声明后继者
private Handler nextHandler;
public Handler(int numStart) {
this.numStart = numStart;
}
public Handler(int numStart, int numEnd) {
this.numStart = numStart;
this.numEnd = numEnd;
}
public void setNextHandler(Handler nextHandler) {
this.nextHandler = nextHandler;
}
//各级领导处理请假条的方法
protected abstract String handleLeave(LeaveRequest leave);
//提交请假条
public final void submit(LeaveRequest leave) {
//领导进行审批
String result = this.handleLeave(leave);
if (this.nextHandler != null && leave.getNum() > this.numEnd) {
this.nextHandler.submit(leave);
} else {
System.out.println("流程结束:" + result);
}
}
}
具体处理者
public class GroupLeader extends Handler {
public GroupLeader() {
super(NUM_ONE, NUM_THREE);
}
@Override
protected String handleLeave(LeaveRequest leave) {
int num = leave.getNum();
String reason = leave.getReason();
if (num < 0) {
return "你欠我几天是吧!";
} else if (reason.contains("摸鱼")) {
return "小组长不允许" + reason + "!";
} else {
return "小组长批准" + leave.getName() + "请假" + num + "天,去" + reason + "!";
}
}
}
测试
优缺点
优点:
降低了请求发送者和接收者之间的耦合度
可以根据需求增加新的请求处理类,满足开闭原则,增加了可扩展性
当工作流程发生了变化,可以动态的改变链内的成员或它们的次序,也可动态新增或者删除责任,增加了给对象指派职责的灵活性
简化了对象之间的连接,一个对象只需要保存一个指向其后继者的引用,不需要保存所有处理者,避免了使用众多的if或else语句
责任分担,每个类只需要处理自己该处理的工作,不能处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则
缺点:
不能保证每个请求一定被处理,由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理
对比较长的责任链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响
职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
源码解析
在javaWeb应用开发中,FilterChain是职责链(过滤器)模式的典型应用,以下是Filter的模拟实现
模拟web请求Request以及web响应Response
public interface Request {
}
public interface Response {
}
模拟web过滤器Filter 抽象处理者
public interface Filter {
void doFilter(Request request, Response response, FilterChain fc);
}
具体处理者
public class FirstFilter implements Filter {
@Override
public void doFilter(Request request, Response response, FilterChain chain) {
System.out.println("FirstFilter前置处理");
chain.doFilter(request, response);
System.out.println("FirstFilter后置处理");
}
}
模拟实现FilterChain
public class FilterChain {
private List<Filter> filters = new ArrayList<Filter>();
private int index = 0;
//链式调用
public FilterChain addFilter(Filter filter) {
this.filters.add(filter);
return this;
}
public void doFilter(Request request, Response response) {
if (index == filters.size()) {
return;
}
Filter filter = filters.get(index);
index++;
filter.doFilter(request, response, this);
}
}
测试
状态模式
概述
例子:电梯状态
通过按钮来控制一个电梯的状态,一个电梯有开门状态,关门状态,停止状态,运行状态。每一种状态改变,都有可能要根据其他状态来更新处理。例如,如果电梯门现在处于运行时状态,就不能进行开门操作
引入案例实现
ILift
public interface ILift {
//定义四个电梯状态的常量
int OPENING_STATE = 1;
int CLOSING_STATE = 2;
int RUNNING_STATE = 3;
int STOPPING_STATE = 4;
//设置电梯状态的功能
void setState(int state);
//电梯操作功能
void open();
void close();
void run();
void stop();
}
Lift
public class Lift implements ILift {
//记录当前电梯状态
private int state;
@Override
public void setState(int state) {
this.state = state;
}
@Override
public void open() {
switch (state) {
case OPENING_STATE:
case RUNNING_STATE:
//啥也不干
break;
case CLOSING_STATE:
case STOPPING_STATE:
System.out.println("电梯打开!");
//设置电梯状态为开启
setState(OPENING_STATE);
break;
}
}
@Override
public void close() {
switch (state) {
case RUNNING_STATE:
case CLOSING_STATE:
case STOPPING_STATE:
//啥也不干
break;
case OPENING_STATE:
System.out.println("电梯关闭!");
//设置电梯状态为关闭
setState(CLOSING_STATE);
break;
}
}
@Override
public void run() {
switch (state) {
case OPENING_STATE:
case RUNNING_STATE:
//啥也不干
break;
case CLOSING_STATE:
case STOPPING_STATE:
System.out.println("电梯运行!");
//设置电梯状态为开启
setState(RUNNING_STATE);
break;
}
}
@Override
public void stop() {
switch (state) {
case OPENING_STATE:
case STOPPING_STATE:
//啥也不干
break;
case CLOSING_STATE:
case RUNNING_STATE:
System.out.println("电梯停止!");
//设置电梯状态为关闭
setState(STOPPING_STATE);
break;
}
}
}
问题分析:
使用了大量的switch...case这样的判断(if-else也一样),使程序可阅读性变差。
扩展性很差,如果加入了断电的状态,我们需要修改上面的判断逻辑
状态模式定义:对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
状态模式包含的角色:
环境角色:也称上下文,它定义了客户程序所需要的接口,维护一个当前状态,并将与状态相关的操作委托给当前状态对象来处理
抽象状态角色:定义一个接口,用以封装环境对象中的特定状态所对应的行为
具体状态角色:实现抽象状态所对应的行为
案例实现
对上述案例使用状态模式进行改进
代码
抽象状态类
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 Context {
//定义对应状态对象的常量
public final static OpeningState OPENING_STATE = new OpeningState();
public final static ClosingState CLOSING_STATE = new ClosingState();
public final static RunningState RUNNING_STATE = new RunningState();
public final static StoppingState STOPPING_STATE = new StoppingState();
//定义当前状态的变量
private LiftState liftState;
public LiftState getLiftState() {
return liftState;
}
public void setLiftState(LiftState liftState) {
this.liftState = liftState;
//设置当前状态对象中的Context对象
this.liftState.setContext(this);
}
public void open() {
this.liftState.open();
}
public void close() {
this.liftState.close();
}
public void run() {
this.liftState.run();
}
public void stop() {
this.liftState.stop();
}
}
具体状态类
public class OpeningState extends LiftState {
@Override
public void open() {
System.out.println("电梯开启");
}
@Override
public void close() {
//修改状态
super.context.setLiftState(Context.CLOSING_STATE);
//动作委托给CloseState来执行
super.context.getLiftState().close();
}
@Override
public void run() {
//do nothing
}
@Override
public void stop() {
//do nothing
}
}
测试
优缺点和使用场景
优点:
将所有与状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。
允许状态转换逻辑和状态对象合成一体,而不是某一个巨大的条件语句块
缺点:
状态模式的使用必然会增加系统和对象的个数
状态模式的结构和实现都比较复杂,如果使用不当将导致程序结构和代码的混乱
状态模式对开闭原则的支持不太友好
使用场景:
当一个对象的行为取决于它的状态,并且它必须运行时根据状态改变它的行为时,考虑使用
一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时。
观察者模式
概述
定义:
又被称为发布-订阅模式,它定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象,这个主题对象在状态发生改变时,会通知所有的观察者对象,是他们能够自动更新自己
观察者模式包含的角色:
抽象主题(抽象被观察者):抽象主题角色把所有观察者对象保存在一个集合里,每个主题都可以有任意数量的观察者,抽象主题提供一个接口,可以增加和删除观察者对象
具体主题:具体被观察者,该角色将有关状态存入具体观察者对象,在具体主题的内部发生改变时,给所有注册过的观察者发送通知
抽象观察者:定义了一个更新接口,使得在得到该主题更新通知时更新自己
具体观察者:实现了抽象观察者定义的更新接口,以便在的得到主题更新通知时更新自身状态
案例实现
例子:微信公众号
代码
抽象主题
public interface Subject {
//添加观察者的方法
void attach(Observer observer);
//删除观察者的方法
void detach(Observer observer);
//通知订阅者更新消息
void notify(String message);
}
具体主题
public class SubscriptionSubject implements Subject {
//一个集合存储,多个观察者对象
private List<Observer> winXinUserList = new ArrayList<>();
@Override
public void attach(Observer observer) {
winXinUserList.add(observer);
}
@Override
public void detach(Observer observer) {
winXinUserList.remove(observer);
}
@Override
public void notify(String message) {
for (Observer observer : winXinUserList) {
observer.update(message);
}
}
}
抽象观察者
public interface Observer {
void update(String msg);
}
具体观察者
public class WinXinUser implements Observer {
private String name;
public WinXinUser(String name) {
this.name = name;
}
@Override
public void update(String msg) {
System.out.println(name + "接收到:" + msg);
}
}
测试
优缺点和使用场景
优点:
降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。
被观察者发送通知,所有注册的观察者都会收到信息(可以实现广播机制)
缺点:
如果观察者非常多的话,那么所有的观察者收到被观察者发送的通知会耗时
如果被观察者有循环依赖的话,bane被观察者发送通知会使观察者循环调用,系统崩溃
使用场景:
对象间存在一对多关系,一个对象的状态发生改变会影响其他对象
当一个抽象模式有两个方面,其中一个方面依赖于另一方面时。
JDK中提供的实现-Observer&Observable
在Java中,通过java.util.Observable和java.util.Observer接口定义了观察者模式,只要实现它们的子类就可以编写观察者模式实例
抽象主题Observable
有一个Vector集合成员变量,用于保存所有要通知的观察者,和添加,删除,通知观察者的方法
抽象观察者
当目标发生变化,得到通知,调用update方法
例子:警察抓小偷
小偷 具体被观察者
public class Thief extends Observable {
private String name;
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void steal() {
System.out.println("开偷,开偷!");
super.setChanged();
super.notifyObservers();
}
}
警察 具体观察者
public class Police implements Observer {
@Override
public void update(Observable o, Object arg) {
Thief thief = (Thief) o;
System.out.println("收到消息" + thief.getName() + "正在偷东西!");
System.out.println("去抓人!!!");
}
}
中介者模式
概述
一般来说,同事类之间的关系是比较复杂的,多个同事类之间相互关联时,它们之间的关系会呈现为复杂的网状结构,这是一种过度耦合的架构,即不利于类的复用,也不稳定。例如在下左图中,有六个同事类对象,假如对象1发生变化,那么将会有4个对象收到影响。如果对象2发生变化,那么将会有5个对象受到影响。也就是说,同事类之间直接关联的设计时不好的。
如果引入中介者模式,那么同事类之间的关系将转变为星型结构,从下有图可以看出,任何一个类发生变化,只会影响类的本身,以及中介者,这样就减少了系统的耦合。一个好的设计,必定不会把所有的对象关系处理逻辑封装到本类中,而是使用一个专门的类来管理那些不属于自己的行为。
定义:
又叫调停模式,定义一个中介角色来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立的改变它们之间的交互。
中介者模式包含的角色:
抽象中介者:它是中介者的接口,提供了同事对象注册和转发同时对象信息的抽象方法。
具体中介者:实现中介者接口,定义了一个List来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
抽象同事类:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
具体同事类:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互
案例实现
例子:房屋中介
代码
抽象同事类
public abstract class Person {
protected String name;
protected Mediator mediator;
public Person(String name, Mediator mediator) {
this.name = name;
this.mediator = mediator;
}
//和同事沟通的方法
public abstract void contact(String message);
//获取同事反馈信息
public abstract void getMessage(String message);
}
具体同事类
public class Tenant extends Person {
public Tenant(String name, Mediator mediator) {
super(name, mediator);
}
//和中介沟通的方法
@Override
public void contact(String message) {
mediator.contact(message, this);
}
@Override
//获取中介信息的方法
public void getMessage(String message) {
System.out.println("租房者" + name + "获取到的信息是:" + message);
}
}
抽象中介者
public abstract class Mediator {
public abstract void contact(String message, Person person);
}
具体中介者
public class MediatorStructure extends Mediator {
//聚合房主和租房者
private HouseOwner houseOwner;
private Tenant tenant;
public HouseOwner getHouseOwner() {
return houseOwner;
}
public void setHouseOwner(HouseOwner houseOwner) {
this.houseOwner = houseOwner;
}
public Tenant getTenant() {
return tenant;
}
public void setTenant(Tenant tenant) {
this.tenant = tenant;
}
@Override
public void contact(String message, Person person) {
if (person == houseOwner) {
tenant.getMessage(message);
} else {
houseOwner.getMessage(message);
}
}
}
测试
优缺点和使用场景
松散耦合:中介者模式通过把多个同时对象之间的交互封装到中介者对象里面,从而使得同事对象之间松散耦合,基本可以做到互补依赖。这样一来,同事对象就可以独立的变化和复用,而不再像以前那样“牵一处而动全身了”。
集中控制交互:多个同事对象的交互,被封装在中介者对象里面集中管理,使得这些交互行为发生变化的时候,只需要修改中介者对象就可以了,当然如果是已经做好的系统,那么就扩展中介者对象,而各个同事类不需要做修改。
一对多关联变为一对一关联:没有使用中介者模式的时候,同事对象之间的关系通常是一对多的,引入中介者对象以后,中介者对象和同事对象的关系通常变为双向的一对一,这会让对象的关系更容易理解和实现。
缺点:
当同事类太多时,中介者的职责会很大,它会变得复杂而庞大,以至于系统难以维护
使用场景:
系统中对象存在复杂的引用关系,系统结构混乱且难以理解
当想创建一个运行于多个类之间的对象,又不想生成新的子类时
迭代器模式
提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示
迭代器模式的角色:
抽象聚合角色:定义存储,添加,删除聚合元素以及创建迭代器对象的接口
具体聚合角色:实现抽象聚合类,返回一个具体迭代器的实例
抽象迭代器角色:定义访问和遍历聚合元素的接口,通常包含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() {
return list.get(position++);
}
}
抽象聚合
public interface StudentAggregate {
//添加学生功能
void addStudent(Student student);
//删除学生功能
void removeStudent(Student student);
//获取迭代器对象功能
StudentIterator getStudentIterator();
}
具体聚合
public class StudentAggregateImpl implements StudentAggregate {
private List<Student> list = new ArrayList<>();
@Override
public void addStudent(Student student) {
list.add(student);
}
@Override
public void removeStudent(Student student) {
list.remove(student);
}
//获取迭代器对象
@Override
public StudentIterator getStudentIterator() {
return new StudentIteratorImpl(list);
}
}
测试
优缺点和使用场景
优点:
它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法,我们也可以自己定义迭代器的子类以支持新的遍历方式。
迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历的方法,这样可以简化聚合类的设计。
在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有的代码,满足”开闭原则“的要求。
缺点:
增加了类的数量,这在一定程度上增加了系统的复杂性
使用场景:
当需要为聚合对象提供多种遍历方式时。
当需要为遍历不同的聚合结构提供一个统一的接口时。
当访问一个聚合对象的内容而无须暴露其内部细节的表示时。
JDK源码解析
Java源码中迭代器的使用
以ArrayList举例说明
ArrayList中的代码,Itr是具体迭代器类
访问者模式
概述
封装一些作用于某种数据结构中的各元素的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的操作。
访问者模式包含的角色:
抽象访问者角色:定义了对每一个元素的访问的行为,它的参数就是可以访问的元素,它的方法个数理论上讲与元素类个数是一样的,从这点不难看出,访问者模式要求元素类的个数不能改变。
具体访问者角色:给出对每一种元素访问时所产生的具体行为。
抽象元素角色:定义了一个接受访问者的方法,其意义是指:每一个元素都要可以被访问者访问
具体元素角色:提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
对象结构角色:定义当中所提到的对象结构,对象结构是一个抽象表述,具体点可以理解为一个具有容器性质或者复合对象特性的类,它会包含一组元素,并且可以迭代这些元素,供访问者访问。
案例实现
例子:给宠物喂食
访问者角色:给宠物喂食的人
具体访问者角色:主人,其他人
抽象元素角色:动物抽象类
具体元素角色:猫,狗
对象结构角色:主人家
代码
抽象元素角色
public interface Animal {
//接受访问者访问
void accept(Person person);
}
具体元素角色
public class Dog implements Animal {
@Override
public void accept(Person person) {
person.feed(this);
System.out.println("真好吃,汪!");
}
}
访问者角色
public interface Person {
//喂猫
void feed(Cat cat);
//喂狗
void feed(Dog dog);
}
具体访问者角色
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 Home {
//存储元素对象的集合
List<Animal> animals = new ArrayList<>();
//添加元素的功能
public void add(Animal animal) {
animals.add(animal);
}
public void action(Person person) {
//遍历集合获取元素
for (Animal animal : animals) {
animal.accept(person);
}
}
}
测试
优缺点和使用场景
优点:
扩展性好:在不修改对象结构中元素的情况下,为对象结构中的元素添加新的功能
复用性好:通过访问者来定义整个对象结构通用的功能,从而提高复用程度
分离无关操作:通过访问者来分离无关操作,把相关的行为封装在一起,构成一个访问者,这样每一个访问者的功能都比较单一
缺点:
对象结构变化困难:每增加一个新的元素类,都要在每一个具体访问者中增加相应的具体操作,这样违背了开闭原则
违反了依赖倒置原则:访问者模式依赖了具体类,而没有依赖抽象类。
使用场景:
对象结构相对稳定,但其操作算法经常变化的程序。
对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的改变
扩展
访问者模式用到了一种双分派的技术。
分派:
变量被声明是的类型叫做变量的静态类型,有些人又把静态类型叫做明显类型;而变量所引用的对象的真实类型又叫做变量的实际类型。比如Map map=new HashMap(),map变量的静态类型是Map,实际类型是HashMap,根据对象的类型而对方法进行的选择,就是分派,分派又分为两种即动态分派和静态分派。
静态分派发生在编译时期,分派根据静态类型信息发生。静态分派对应我们来说并不陌生,方法重载就是静态分派。
动态分派发生在运行时期,动态分派动态的置换掉某个方法,Java通过方法的重写支持动态分派。
动态分派
多态
Java编译器在编译时期并不总是知道哪些代码会执行,因为编译器仅仅知道对象的静态类型,而不知道对象的真实类型;而方法的调用则是根据对象的真实类型,而不是静态类型。
静态分派
重载
声明三个类
一个Execute类,重载三个方法
测试
输出三个Animal
因为重载的分派是根据静态类型进行的,这个分派过程在编译时期就完成了。
双分派
所谓双分派技术就是在选择一个方法的时候,不仅要根据接收者运行时的区别,还要根据参数的运行时区别
上面的代码中,将Execute对象作为参数传递给Animal类型的变量调用的方法,这里完成第一次分派,这是方法重写,所以是动态分派。也就是执行实际类型中的方法,同时将this作为参数传递进去,这里就完成了第二次分派,Execute类中有多个重载的方法,而传递的是this,就是具体的实际类型的对象。
双分派就是可以实现方法的动态绑定,它的本质就是在重载方法委派的前面加上了继承体系中覆盖的环节,由于覆盖是动态的,所以重载就是动态的了。
备忘录模式
备忘录模式提供了一种状态恢复的实现机制,使得用户可以方便的回到特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态恢复,很多软件都提供了撤销操作,比如Word,笔记本,Photoshop,IDEA等软件在编辑的时候Ctrl+Z可以撤销当前操作,时文档恢复到之前的状态,还有浏览器中的后退键,数据库事务管理中的回滚操作,玩游戏时的存档功能,数据库与操作心痛的备份功能,棋类游戏的悔棋等。
定义:
又叫快照模式,在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。
备忘录角色的主要角色:
发起人角色:记录当前时刻的内部状态信息,提供创建备忘录和恢复备忘录的功能,实现其他业务功能,它可以访问备忘录里的所有信息。
备忘录角色:负责存储发起人的内部状态,在需要的时候提供这些状态给发起人。
管理者角色:对备忘录进行管理,提供保存和获取备忘录的功能,但不能对备忘录的内容进行访问和修改
备忘录有两个等效的接口:
窄接口:管理者对象(和其他发起人对象之外的任何对象)看到的是备忘录的窄接口,这个窄接口只允许它把备忘录对象传给其他的对象
宽接口:与管理者看到的窄接口相反,发起人对象可以看到一个宽接口,这个宽接口允许它读取所有的数据,以便根据这些数据恢复这个发起人对象的内部状态
案例实现
例子:游戏打BOSS
游戏中的某个场景,一游戏角色有生命力,攻击力,防御力等数据(打BOSS前后很可能不一样,要求打的如果效果不理想可以回到原来的状态)
要实现上述案例,有两种方式
”白箱“备忘录模式
"黑箱"备忘录模式
”白箱“备忘录模式
备忘录角色对任何对象都提供宽接口,备忘录角色的内部所存储的状态对所有对象公开
代码
备忘录角色
public class RoleStateMemento {
private int vit;
private int atk;
private int def;
public RoleStateMemento() {
}
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
发起人角色
public class GameRole {
private int vit;
private int atk;
private int def;
//初始化状态的方法
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight() {
//被打掉半条命
this.vit -= 50;
//杀红眼了
this.atk += 10;
//被破防了
this.def -= 20;
}
//保存角色状态功能
public RoleStateMemento saveState() {
return new RoleStateMemento(vit, atk, def);
}
//恢复角色状态
public void recoverState(RoleStateMemento roleStateMemento) {
//将备忘录对象中存储的状态赋值
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
//展示当前状态
public void stateDisplay() {
System.out.println("生命力" + vit);
System.out.println("攻击力" + atk);
System.out.println("防御力" + def);
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
管理者角色
public class RoleStateCaretaker {
//声明RoleStateMemento类型的变量
private RoleStateMemento roleStateMemento;
public RoleStateMemento getRoleStateMemento() {
return roleStateMemento;
}
public void setRoleStateMemento(RoleStateMemento roleStateMemento) {
this.roleStateMemento = roleStateMemento;
}
}
测试
白箱备忘录模式是破坏封装性的,但是通过程序员的自律,同样可以在一定程度上实现模式的大部分用意。
“黑箱”备忘录模式
备忘录角色对发起人角色提供一个宽接口,而为其他对象提供一个窄接口。在Java语言中,实现双重接口的方法就是将备忘录类设计成发起人类的内部成员类。
将RoleStateMemento设为GameRole的内部类,从而将RoleStateMemento对象封装在GameRole里面;在外面提供一个Memento给RoleStateCaretaker及其他对象使用。这样GameRole类看到的是RoleStateMemento所有的接口,而RoleStateCaretaker及其他对象看到的仅仅是表示接口Memento所暴露出来的接口,从而维护了封装性
代码
对外提供的窄接口
public interface Memento {
}
管理者
public class RoleStateCaretaker {
//声明RoleStateMemento类型的变量
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
发起人
public class GameRole {
private int vit;
private int atk;
private int def;
//初始化状态的方法
public void initState() {
this.vit = 100;
this.atk = 100;
this.def = 100;
}
//战斗
public void fight() {
//被打掉半条命
this.vit -= 50;
//杀红眼了
this.atk += 10;
//被破防了
this.def -= 20;
}
//保存角色状态功能
public Memento saveState() {
return new RoleStateMemento(vit, atk, def);
}
//恢复角色状态
public void recoverState(Memento memento) {
RoleStateMemento roleStateMemento = (RoleStateMemento) memento;
//将备忘录对象中存储的状态赋值
this.vit = roleStateMemento.getVit();
this.atk = roleStateMemento.getAtk();
this.def = roleStateMemento.getDef();
}
//展示当前状态
public void stateDisplay() {
System.out.println("生命力" + vit);
System.out.println("攻击力" + atk);
System.out.println("防御力" + def);
}
//定义私有的成员内部类
private class RoleStateMemento implements Memento {
private int vit;
private int atk;
private int def;
public RoleStateMemento() {
}
public RoleStateMemento(int vit, int atk, int def) {
this.vit = vit;
this.atk = atk;
this.def = def;
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
public int getVit() {
return vit;
}
public void setVit(int vit) {
this.vit = vit;
}
public int getAtk() {
return atk;
}
public void setAtk(int atk) {
this.atk = atk;
}
public int getDef() {
return def;
}
public void setDef(int def) {
this.def = def;
}
}
管理者
public class RoleStateCaretaker {
//声明RoleStateMemento类型的变量
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}
优缺点和使用场景
优点:
提供了一种可以恢复状态的机制,当用户需要时能够比较方便的将数据恢复到某个历史的状态
实现了内部状态的封装,处理创建它的发起人之外,其他对象都不能够访问这些状态信息
简化了发起人类,发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则
缺点:
资源消耗大,如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源
使用场景:
需要保存与恢复数据的场景,比如游戏存档功能。
需要提供一个可回滚操作的场景,如Word,笔记本,Photoshop,IDEA等软件在编辑的时候Ctrl+Z可以撤销,数据库事务操作。
解释器模式
概述
设计一个软件进行加减计算,我们的第一想法是使用工具类,提供对应的加法和减法的工具方法。
上面的形式比较单一,有限,如果形式变化非常多,这就不符合要求,因为加法和减法运算,两个运算符与数值可以有无限种组合方式,比如1+2+3+4,1+2+3-4等。
显然,现在需要一种翻译识别机器,能够解析由数字以及 + - 符号构成的合法的运算序列,如果把运算符和数字都看作节点的话,能够逐个节点的进行读取解析运算,这就是解释器模式的思维。
定义
给定一个语言,定义它的文法表示,并定义一个解释器。这个解释器使用该文法规则表示来解释语言中的句子
在解释器模式中,我们需要将待解决的问题,抽取出规则,抽象为一种“语言”。比如加减法运算,规则为:由数值和+-符号组成的合法序列,如“1+5-4”就是这种语言的句子。
文法(语法)规则:
文法是用于描述语言的语法结构的形式规则。
注意:这里的符号“::=”表示“定义为”的意思,竖线 | 表示或,左右的其中一个,引号内为字符本身,引号外为语法。
上面的规则描述为:
表达式可以是一个值,也可以是plus或者minus运算,而plus和minus又是由表达式结合运算符构成,值的类型为整数型。
抽象语法树:
在计算机科学中,抽象语法树,或简称语法树,是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
用树形来表示符合文法规则的句子。
结构:
解释器模式包含的角色:
抽象表达式角色:定义解释器的接口,约定解释器的解释操作,主要包含解释方法interpret()。
终结符表达式角色:是抽象表达式的子类,用来实现文法中与终结符相关的操作,文法中的每一个终结符都有一个具体终结表达式与之相对应。
非终结符表达式角色:也是抽象表达式的子类,用来实现文法中与非终结符相关的操作,文法中的每一条规则都对应一个非终结符表达式。
环境角色:通常包含各个解释器需要的数据或是公共的功能,一般用来传递被所有解释器共享的数据,后面的解释器可以从这里获取那些值。
客户端:主要任务是将需要分析的句子或表达式转换成使用解释器对象描述的抽象语法树,然后调用解释器的解释方法,当然也可以通过环境角色间接访问解释器的解释方法。
案例实现
代码
抽象表达式角色
public abstract class AbstractExpression {
public abstract int interpret(Context context);
}
环境角色
public class Context {
//定义一个map集合用来存储变量及对应的值
private Map<Variable, Integer> map = new HashMap<>();
//添加变量的功能
public void assign(Variable var, Integer value) {
map.put(var, value);
}
//根据变量获取对应的值
public int getValue(Variable var) {
return map.get(var);
}
}
非终结符表达式角色
public class Plus extends AbstractExpression {
//加号左边的表达式
private AbstractExpression left;
//加号右边的表达式
private AbstractExpression right;
public Plus(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
//将左边表达式的结果和右边相加
return left.interpret(context) + right.interpret(context);
}
public String toString() {
return "(" + left.toString() + "+" + right.toString() + ")";
}
}
终结符表达式角色
public class Variable extends AbstractExpression {
//声明存储变量名的成员变量
private String name;
public Variable(String name) {
this.name = name;
}
@Override
public int interpret(Context context) {
//返回变量的值
return context.getValue(this);
}
@Override
public String toString() {
return name;
}
}
测试
优缺点和使用场景
优点:
易于改变和扩展文法:由于解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法,每一条文法规则都可以表示为一个类,因此可以方便的实现一个简单的语言。
实现文法较为容易:在抽象语法树中每个表达式类的实现方式都是相似的,这些类的代码编写都不会特别复杂。
增加新的解释表达式较为方便:如果用户需要新增解释表达式只需要对应增加一个新的终结符表达式或非终结符表达式类,原有表达式无需修改,符合“开闭原则”。
缺点:
对于复杂文法难以维护:在解释器模式中,每一条规则至少需要定义一个类,因此如果一个语言包含太多文法规则,类的个数就会急剧增加,导致系统难以管理和维护。
执行效率较低:由于在解释器模式中使用了大量的循环和递归调用,因此在解释较为复杂的句子时其速度很慢,而且代码的调试过程也比较麻烦。
使用场景:
当语言的文法较为简单,且执行效率不是关键问题时。
当问题重复出现,且可以用一种简单的语言来进行表达时。
当一个语言需要解释执行,并且语言中的句子可以表示为一个抽象语法树的时候。