经典伴读系列文章,不是读书笔记,自己的理解加上实际项目中运用,旨在5天读懂这本书。如果这篇文章对您有些用处,请点赞告诉我O(∩_∩)O。
- 如何使用设计模式抽象实例化过程。请参考《经典伴读_GOF设计模式_创建型模式》
- 如何组合类和对象以获得更大的结构。请参考《经典伴读_GOF设计模式_结构型模式》
行为模式
GOF中23种设计模式从用途上分为三类,第三类是行为模式,描述的是算法和对象间职责的分配,主要是对象或类之间的通信模式。
Chain Of Responsibility责任链
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这 些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
1、GOF从UI页面的事件冒泡入手解释责任链,一个终端应用(Application),点击弹窗(Dialog)中的按钮(Button),按照事件冒泡规则(从内往外),请求传导应该是Button->Dialog->Application。这就是一种责任链。天塌了也有个高的顶,差不多也是这个意思,那么这样的一条责任链应该怎么构造。
public static abstract class Handler {
private Handler successor; //后继者,是不是叫next更合适些
public void handleRequest(Request request) { //处理请求
if (successor != null) {
successor.handleRequest(request);
}
}
public Handler getSuccessor() {
return successor;
}
public void setSuccessor(Handler successor) {
this.successor = successor;
}
}
public static class Request {} //封装请求参数
public static class ConcreteHandler1 extends Handler {
@Override
public void handleRequest(Request request) {
System.out.println("ConcreteHandler1处理请求");
super.handleRequest(request); //往后继续请求
}
}
public static class ConcreteHandler2 extends Handler {
@Override
public void handleRequest(Request request) {
System.out.println("ConcreteHandler2处理请求");
super.handleRequest(request); //往后继续请求
}
}
public static void main(String[] args) {
Handler handler1 = new ConcreteHandler1();
Handler handler2 = new ConcreteHandler2();
handler1.setSuccessor(handler2); //构建责任链:handler1->handler2
Request request = new Request();
handler1.handleRequest(request); //传导请求handler1->handler2
handler2.handleRequest(request); //只请求handler2
}
输出:
ConcreteHandler1处理请求
ConcreteHandler2处理请求
ConcreteHandler2处理请求
2、实际开发中链式处理的场景不少,如:过滤器,拦截器。看看它们又是如何实现责任链。
- 过滤器Filter核心方法:void doFilter(ServletRequest var1, ServletResponse var2, FilterChain var3)
- doFilter三个入参分别表示:请求参数,响应结果,过滤链,我们重点看FilterChain。
- 带filter的servlet请求链为:filter1 -> filter2 -> … filtern -> servlet ,这样一个请求链如何实现呢?
ApplicationFilterChain是FilterChain的具体实现:
......
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
private int pos = 0;
private int n = 0;
private Servlet servlet = null;
......
private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.pos < this.n) {
ApplicationFilterConfig filterConfig = this.filters[this.pos++];
......
Filter filter = filterConfig.getFilter();
......
filter.doFilter(request, response, this);
......
} else {
......
this.servlet.service(request, response);
,,,,,,
}
其中的精髓学到了么,我们给起个名字责任链2.0,将责任链从处理类中单独抽象出来,如:HandlerChain。
public static class Request {} //封装请求参数
interface Handler {
void handleRequest(Request request, HandlerChain handlerChain);
}
public static class HandlerChain {
private List<Handler> handlers = new ArrayList<>();
private int pos = 0;
public void handleRequest(Request request) { //关键不同
if (pos < handlers.size()) {
Handler handler = handlers.get(pos++);
handler.handleRequest(request, this);
}
}
public void addHandler(Handler handler) {
handlers.add(handler);
}
}
public static class ConcreteHandler1 implements Handler {
@Override
public void handleRequest(Request request, HandlerChain handlerChain) {
System.out.println("ConcreteHandler1处理请求");
handlerChain.handleRequest(request);
}
}
public static class ConcreteHandler2 implements Handler {
@Override
public void handleRequest(Request request, HandlerChain handlerChain) {
System.out.println("ConcreteHandler2处理请求");
handlerChain.handleRequest(request);
}
}
public static void main(String[] args) {
Handler handler1 = new ConcreteHandler1();
Handler handler2 = new ConcreteHandler2();
HandlerChain handlerChain = new HandlerChain();
handlerChain.addHandler(handler1);
handlerChain.addHandler(handler2); //构建责任链:handler1->handler2
handlerChain.handleRequest(new Request());
handlerChain = new HandlerChain();
handlerChain.addHandler(handler2); //只请求handler2
handlerChain.handleRequest(new Request());
}
输出:
ConcreteHandler1处理请求
ConcreteHandler2处理请求
ConcreteHandler2处理请求
Command命令
将一个请求封装为一个对象,从而使你可用不同的请求对客户进行参数化;对请求排队或记录请求日志,以及支持可撤消的操作。
定义很难理解,先看示例,如果说责任链模式可以实现事件的冒泡,那么命令模式则可以实现事件的响应。android等终端开发中最为常见。GOF也是以一个客户端程序引入,假设需要开发一个最简单的文本编辑器,左上角“文件”菜单中只有一个“新建”菜单项,点击可以新建并打开文档。
1、要开发这个客户端程序,得先选择一个漂亮的UI库,如果你是UI库的开发者,如何提供菜单与菜单项控件。
interface Command { //可以理解为监听器,如OnClickListener
void execute(); //可以理解为onclick()
}
//菜单
public static class Menu {
private String label; //菜单标签
private List<MenuItem> menuItems = new ArrayList<>();
public Menu(String label) {
this.label = label;
}
public void addItem(MenuItem menuItem) {
menuItems.add(menuItem);
}
}
//菜单项
public static class MenuItem {
private String label; //菜单项标签
private Command command; //类似于监听器OnClickListener
public MenuItem(String label) {
this.label = label;
}
public void setCommand(Command command) {
this.command = command;
}
public void click() { //点击菜单项
command.execute(); //类似调用监听器OnClickListener的onclick方法
}
}
从MenuItem控件的click方法看出,这里并没有处理点击菜单项的逻辑代码,而是调用command.execute(),将点击事件传递给Command的实现者。接下来看下客户端开发人员,如何使用这个MenuItem控件。
//最外层系统应用
public static class Application {
private List<Document> documents = new ArrayList<>();
public void addDocument(Document document) {
documents.add(document);
}
public void removeDocument(Document document) {
documents.remove(document);
}
}
//文档
public static class Document {
public Document() {
System.out.println("创建文档");
}
public void open() {
System.out.println("打开文档");
}
}
//新建文档命令
public static class CreateCommand implements Command {
private Application application;
public CreateCommand(Application application) {
this.application = application;
}
@Override
public void execute() { //点击新建文档,真正的逻辑代码在这里
Document document = new Document();//创建文档
application.addDocument(document);
document.open(); //打开文档
}
}
public static void main(String[] args) {
//创建应用
Application application = new Application();
//创建菜单与菜单项
Menu menu = new Menu("文件");
MenuItem createMenuItem = new MenuItem("新建");
menu.addItem(createMenuItem);
//绑定新建命令
CreateCommand createCommand = new CreateCommand(application);
createMenuItem.setCommand(createCommand);
//触发新建菜单项点击
createMenuItem.click();
}
输出:
创建文档
打开文档
2、命令模式支持回滚,撤销
接上例,新建的文档需要回滚,自然是要把它删除。因此创建命令中需要保留下一些创建时的数据(也称为状态status)
//新建文档命令
public static class CreateCommand implements Command {
private Application application;
private Document document; //保留创建的文档
public CreateCommand(Application application) {
this.application = application;
}
@Override
public void execute() {
document = new Document();
application.addDocument(document);
document.open();
}
//回滚,撤销
public void unexecute() {
if (document != null) {
application.removeDocument(document);
}
}
}
public static void main(String[] args) {
//创建应用
Application application = new Application();
//创建菜单与菜单项
Menu menu = new Menu("文件");
MenuItem createMenuItem = new MenuItem("新建");
menu.addItem(createMenuItem);
//绑定新建命令
CreateCommand createCommand = new CreateCommand(application);
createMenuItem.setCommand(createCommand);
createMenuItem.click();//触发新建菜单项点击
//新建命令撤销
createCommand.unexecute();
}
3、命令模式支持组合命令并排队执行
public static class MacroCommand implements Command {
private List<Command> commands = new ArrayList<>();
@Override
public void execute() {
for (Command command : commands) { //排队执行
command.execute();
}
}
public void addCommand(Command command) {
commands.add(command);
}
public void removeCommand(Command command) {
commands.remove(command);
}
}
public static void main(String[] args) {
//创建应用
Application application = new Application();
//创建菜单与菜单项
Menu menu = new Menu("文件");
MenuItem createMenuItem = new MenuItem("新建");
menu.addItem(createMenuItem);
//绑定新建命令
CreateCommand createCommand1 = new CreateCommand(application);
CreateCommand createCommand2 = new CreateCommand(application);
//点击一次创建两个文件(排队执行)
MacroCommand macroCommand = new MacroCommand();
macroCommand.addCommand(createCommand1);
macroCommand.addCommand(createCommand2);
createMenuItem.setCommand(macroCommand);
//触发新建菜单项点击
createMenuItem.click();
}
输出:
创建文档
打开文档
创建文档
打开文档
4、理解示例之后,再看下命令模式结构图(修改了下,更容易理解)。
对比的示例部分
对比之下一目了然,不用多说,ConcreteCommand中status用于回滚前保存状态,Invoke调用者是触发事件的人,Receiver接受者是实际处理事件的人。就像电视遥控器和电视的关系,点击电视遥控器,实际是电视开了。
Interpreter解释器
给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示 来解释语言中的句子。
解释器模式应用场景最为固定,当一个语言需要解释执行,并且其中的语句可以表示为一个抽象语法树(AST)时,就可以使用解释器模式。
1、先看示例,如执行“x 加 1 减 y”,首先这个语句可以表示为抽象语法树。
接下来使用解释器模式,其实就是构建语法树,以及遍历执行的过程。(注意是从根节点开始前序遍历)
//全局上下文
public static class Context extends HashMap<String, Integer> {
}
interface AbstractExpression {
int interpret(Context context);
}
//变量表达式
public static class VarExpression implements AbstractExpression {
private String name;
public VarExpression(String name) {
this.name = name;
}
@Override
public int interpret(Context context) {
return context.get(name);
}
}
//常量表达式
public static class Constant implements AbstractExpression {
private int value;
public Constant(int value) {
this.value = value;
}
@Override
public int interpret(Context context) {
return value;
}
}
//加法表达式
public static class AddExpression implements AbstractExpression {
private AbstractExpression left;
private AbstractExpression right;
public AddExpression(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
}
//减法表达式
public static class SubExpression implements AbstractExpression{
private AbstractExpression left;
private AbstractExpression right;
public SubExpression(AbstractExpression left, AbstractExpression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret(Context context) {
return left.interpret(context) - right.interpret(context);
}
}
public static void main(String[] args) {
//解释执行x 加 2 减 y
Context context = new Context();
context.put("x", 1);
context.put("y", 3);
//为了方便理解,这里先参照语法树图,从根节点开始手动构建,后面有扩展
AbstractExpression root = new SubExpression(
new AddExpression(
new VarExpression("x"), new Constant(2)),
new VarExpression("y")
);
//递归解释执行
int result = root.interpret(context);
System.out.println("result:" + result);
}
输出:
result:0
2、再来理解解释器模式结构
TerminalExpression终结符表达式,即不能再被分解的元素,如x,1等。
NonterminalExpression非终结符表达式,即可以被再分解的元素,如x+1等。
Context存放全局信息,如变量x,y的值,需要提前设置在Context中,以供递归时使用。
对比示例,其中VarExpression变量表达式如x,y,Constant常量如1,都是TerminalExpression,而AddExpression加法表达式和SubExpression减法表达式,则是NonterminalExpression。
3、最后扩展一下,根据字符串构建简单的语法树。
//判断是不是非终结符表达式
public static boolean isNonterminalExpression(String token) {
return token.equals("加") || token.equals("减");
}
//创建终结符表达式
public static AbstractExpression createTerminalExpression(String token) {
AbstractExpression terminalExpression = null;
if (token.chars().allMatch(Character::isDigit)) { //数字
terminalExpression = new Constant(Integer.parseInt(token));
} else { //字母
terminalExpression = new VarExpression(token);
}
return terminalExpression;
}
//创建非终结符表达式
public static AbstractExpression createNonterminalExpression(String token, AbstractExpression left, AbstractExpression right) {
AbstractExpression nonterminalExpression = null;
if ("加".equals(token)) {
nonterminalExpression = new AddExpression(left, right);
} else if ("减".equals(token)) {
nonterminalExpression = new SubExpression(left, right);
}
return nonterminalExpression;
}
//构建抽象语法树
public static AbstractExpression buildSyntaxTree(String text) {
Stack<AbstractExpression> stack = new Stack<>();
StringTokenizer tokenizer = new StringTokenizer(text);
AbstractExpression current, left, right;
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
if (isNonterminalExpression(token)) { //创建非终结符表达式
left = stack.pop();
right = createTerminalExpression(tokenizer.nextToken());
current = createNonterminalExpression(token, left, right);
} else { //创建终结符表达式
current = createTerminalExpression(token);
}
stack.push(current);
}
return stack.pop();
}
public static void main(String[] args) {
//解释执行x 加 2 减 y
Context context = new Context();
context.put("x", 1);
context.put("y", 3);
//根据字符串文本构建抽象语法树
AbstractExpression root = buildSyntaxTree("x 加 2 减 y");
//递归解释执行
int result = root.interpret(context);
System.out.println("result:" + result);
}
Iterator迭代器
提供一种方法顺序访问一个聚合对象中各个元素 , 而又不需暴露该对象的内部表示。
当你想要迭代查询时,可以使用迭代器(听起来像废话),如迭代集合中的元素,迭代学校里的班级,迭代班级里的学生等,不限于集合对象都可以使用迭代器模式。
1、将迭代查询这件事从集合各种操作中统一抽象出来就是GOF对迭代器模式的初衷。
ConcreteIterator是迭代器即迭代方式,多种迭代方式就有多种迭代器。ConcreteAggregate是聚合类加入了迭代器工厂。在实际代码中,迭代器只是聚合类的查询功能,它们生命周期相同,因此常以内部类形式出现。如:
//迭代器
interface Iterator {
Object first();
Object next();
boolean isDone();
Object currentItem();
}
//聚合类(不如叫迭代器工厂)
interface Aggregate {
Iterator createIterator();
}
//具体聚合类
public static class Array implements Aggregate{
private int[] datas;
public Array(int[] datas) {
this.datas = datas;
}
@Override
public Iterator createIterator() {
return new Iterator() { //具体的迭代器(匿名内部类实现)
private int index = 0;
@Override
public Object first() {
return Array.this.datas[0];
}
@Override
public Object next() {
return Array.this.datas[index++];
}
@Override
public boolean isDone() {
return index == Array.this.datas.length;
}
@Override
public Object currentItem() {
return Array.this.datas[index];
}
};
}
}
public static void main(String[] args) {
Array arr = new Array(new int[]{1,2,3,4,5});
Iterator iterator = arr.createIterator();
while (!iterator.isDone()) {
System.out.println(iterator.next());
}
}
2、JDK中对迭代模式已有支持,同样有Iterator和Iterable接口(对应Aggregate),它们名字相似,相信现在你不会再搞混,前者是迭代器,后者是迭代器工厂。
Mediator中介者
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
GOF讲到的中介者,以客户端程序的页面为例,他们认为页面上多种控件交互,必会造成依赖关系混乱,你中有我,我中有你,于是引入中介者,当所有的控件都只依赖中介者,就解决了依赖混乱的问题。千万不要和命令模式的事件监听搞混,准确说,中介者其实是一种事件总线的思想。
示例,假设页面上有文本框TextView,下拉选框Spinner,清空按钮Button,用户选择下拉选框,文本框及时显示选择信息,用户点击清空按钮,则清空文本框。那么不用事件监听,而改用事件总线如何设计。
Dialog是中介者,Spinner,TextView,Button等控件都是同事类,Mediator和所有Colleague相互引用,Colleague之间却互不相识,这就是中介者模式的特点。
//中介者
interface Mediator {
void changeEvent(Object event);
}
//具体中介者
public static class Dialog implements Mediator{
private TextView mTextView;
private Button mCleanBtn;
private Spinner mSpinner;
public Dialog() {
mTextView = new TextView(this);
mCleanBtn = new Button(this);
mSpinner = new Spinner(this);
}
@Override
public void changeEvent(Object event) { //这里是中介者模式的核心,响应事件
if (event instanceof SpinnerSelectEvent) {
String selectValue = mSpinner.getValue();
mTextView.setText(selectValue);
} else if (event instanceof CleanTextViewEvent) {
mTextView.setText("");
}
}
//模拟页面测试
public static void main(String[] args) {
Dialog dialog = new Dialog();
dialog.mSpinner.select("武汉"); //选择武汉
System.out.println("TextView显示:" + dialog.mTextView.getText());
dialog.mCleanBtn.click(); //清空按钮点击
System.out.println("TextView显示:" + dialog.mTextView.getText());
}
}
//同事类
public static abstract class Colleague {
private Mediator mediator;
public Colleague(Mediator mediator) {
this.mediator = mediator;
}
public Mediator getMediator() {
return mediator;
}
}
//下拉选框选中事件
public static class SpinnerSelectEvent{}
//下拉选框
public static class Spinner extends Colleague{
private String value;
public Spinner(Mediator mediator) {
super(mediator);
}
public String getValue() {
return value;
}
//模拟页面选中
public void select(String value) {
this.value = value;
getMediator().changeEvent(new SpinnerSelectEvent()); //向总线发送事件
}
}
//清空文本框事件
public static class CleanTextViewEvent{}
//清空按钮
public static class Button extends Colleague {
public Button(Mediator mediator) {
super(mediator);
}
//模拟页面点击
public void click() {
getMediator().changeEvent(new CleanTextViewEvent());//向总线发送事件
}
}
//文本
public static class TextView extends Colleague {
private String text = "";
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public TextView(Mediator mediator) {
super(mediator);
}
}
输出:
TextView显示:武汉
TextView显示:
Memento备忘录
在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。 这样以后就可将该对象恢复到原先保存的状态。
GOF依然以客户端程序为例(稍有改动),当一个图形从A点移动到B点,此时撤销,图形则回到A点,当需要先保存,再还原到原始状态的场景,可以使用备忘录模式。
1、备忘录模式结合命令模式,实现客户端操作:
(1)当发出移动命令MoveCommand,图形控件View开始移动,先保存原始位置point到备忘录Memento,再改变位置到targetPoint。
(2)当发出撤销命令,图形控件View从备忘录中获取原始位置point,然后改变位置。
public static class Point {
private int x = 0;
private int y = 0;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "("+x + ","+ y + ")";
}
}
//备忘录
public static class Memento {
private Point status;
public Point getStatus() {
return status;
}
public void setStatus(Point status) {
this.status = status;
}
}
//图形,作为原发器Originator
public static class View {
private Point point;
public void setPoint(Point point) {
this.point = point;
}
//模拟图形渲染
public void render() {
System.out.println("图形位置" + point);
}
//备忘录保存当前状态
public Memento createMemento() {
Memento memento = new Memento();
memento.setStatus(point);
return memento;
}
//从备忘录当中恢复状态
public void setMemento(Memento memento) {
point = memento.getStatus();
}
}
//移动命令,作为负责人Caretaker
public static class MoveCommand {
private View view;
private Point targetPoint;
private Memento memento;
public MoveCommand(View view, Point targetPoint) {
this.view = view;
this.targetPoint = targetPoint;
}
public void execute() {
memento = view.createMemento(); //移动之前存储位置
view.setPoint(targetPoint);//移动到指定位置
view.render(); //渲染图形
}
public void unexecute() {
view.setMemento(memento); //恢复状态
view.render(); //重新渲染;
}
}
public static void main(String[] args) {
View view = new View();
view.setPoint(new Point(50, 50)); //图形初始位置50, 50
//模拟用户移动图形到100, 100
MoveCommand moveCommand2 = new MoveCommand(view, new Point(100, 100));
moveCommand2.execute();
//模拟用户撤销回到上一步50, 50
moveCommand2.unexecute();
}
输出:
图形位置(100,100)
图形位置(50,50)
2、理解示例后,对比理解备忘录模式结构图
(1)备忘录Memento用来保存状态.
(2)当一个对象需要在业务操作中需要保存状态,则在原有的的属性方法上加入创建和还原备忘录的方法,就变成了原发器Originator,如View。
(3)保存出来的状态存放的位置就是负责人Caretaker,如MoveCommand。这个角色很灵活,可以用客户端代替。
(4)当需要还原的不止一步时,Caretaker中就需要使用memento集合保存状态列表。上例中MoveCommand不适用,一个命令只能改变一次状态。
未完待续