24.2 备忘录模式的定义

备忘录模式(Memento Pattern)提供了一种弥补真实世界缺陷的方法,让“后悔药”在程序的世界中真实可行,其定义如下:

Without violating encapsulation,capture and externalize an object’s internal state so that the object can be restored to this state later.(在不破坏封装性的前提下,捕获一个对象的内部状 态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。)

通俗地说,备忘录模式就是一个对象的备份模式,提供了一种程序数据的备份方法,其通用类图如图24-4所示。

image-20210929235311072

图24-4 备忘录模式的通用类图

我们来看看类图中的三个角色。

  • Originator发起人角色

记录当前时刻的内部状态,负责定义哪些属于备份范围的状态,负责创建和恢复备忘录数据。

  • Memento备忘录角色

负责存储Originator发起人对象的内部状态,在需要的时候提供发起人需要的内部状态。

  • Caretaker备忘录管理员角色

对备忘录进行管理、保存和提供备忘录。

备忘录模式的通用代码也非常简单,我们先看发起人角色,如代码清单24-8所示。

代码清单24-8 发起人角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Originator {
//内部状态
private String state = "";
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
//创建一个备忘录
public Memento createMemento(){
return new Memento(this.state);
}
//恢复一个备忘录
public void restoreMemento(Memento _memento){
this.setState(_memento.getState());
}
}

我相信你心里此刻有很多疑问,比如状态是多个怎么办?需要有多份备份怎么办?如果你很着急的话,请看24.4节,但我建议你还是跟随我一步一步地走,我们再来看备忘录角色,如代码清单24-9所示。

代码清单24-9 备忘录角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Memento {
//发起人的内部状态
private String state = "";
//构造函数传递参数
public Memento(String _state){
this.state = _state;
}
public String getState() {
return state;
}
public void setState(String state) {
this.state = state;
}
}

这是一个简单的JavaBean,备忘录管理者也是一个简单的JavaBean,如代码清单24-10所示。

代码清单24-10 备忘录管理员角色

1
2
3
4
5
6
7
8
9
10
public class Caretaker {
//备忘录对象
private Memento memento;
public Memento getMemento() {
return memento;
}
public void setMemento(Memento memento) {
this.memento = memento;
}
}

这3个主要角色都很简单,我们来看场景类如何调用,如代码清单24-11所示。

代码清单24-11 场景类

1
2
3
4
5
6
7
8
9
10
11
12
public class Client {
public static void main(String[] args) {
//定义出发起人
Originator originator = new Originator();
//定义出备忘录管理员
Caretaker caretaker = new Caretaker();
//创建一个备忘录
caretaker.setMemento(originator.createMemento());
//恢复一个备忘录
originator.restoreMemento(caretaker.getMemento());
}
}

备忘录模式就是这么简单,真正使用备忘录模式的时候可比这复杂得多。

25.2 访问者模式的定义

访问者模式(Visitor Pattern)是一个相对简单的模式,其定义如下:

Represent an operation to be performed on the elements of an object structure.Visitor lets you define a new operation without changing the classes of the elements on which it operates.(封装一些作用于某种数据结构中的各元素的操作,它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。)

访问者模式的通用类图如图25-5所示。

image-20210930102720082

图25-5 访问者模式的通用类图

看了这个通用类图,大家可能要犯迷糊了,这里怎么有一个ObjectStruture类呢?你刚刚举的例子怎么就没有呢?真没有吗?我们不是定义了一个List了吗?它中间的元素是我们一个一个手动增加上去的,这就是一个ObjectStruture,我们来看这几个角色的职责。

  • Visitor——抽象访问者

抽象类或者接口,声明访问者可以访问哪些元素,具体到程序中就是visit方法的参数定义哪些对象是可以被访问的。

  • ConcreteVisitor——具体访问者

它影响访问者访问到一个类后该怎么干,要做什么事情。

  • Element——抽象元素

接口或者抽象类,声明接受哪一类访问者访问,程序上是通过accept方法中的参数来定义的。

  • ConcreteElement——具体元素

实现accept方法,通常是visitor.visit(this),基本上都形成了一种模式了。

  • ObjectStruture——结构对象

元素产生者,一般容纳在多个不同类、不同接口的容器,如List、Set、Map等,在项目中,一般很少抽象出这个角色。

大家可以这样理解访问者模式,我作为一个访客(Visitor)到朋友家(Visited Class)去拜访,朋友之间聊聊天,喝喝酒,再相互吹捧吹捧,炫耀炫耀,这都正常。聊天的时候,朋友告诉我,他今年加官晋爵了,工资也涨了30%,准备再买套房子,那我就在心里盘算 (Visitor-self-method)“你这么有钱,我去年要借10万你都不借”,我根据朋友的信息,执行了自己的一个方法。

我们来看看访问者模式的通用源码,先看抽象元素,如代码清单25-11所示。

代码清单25-11 抽象元素

1
2
3
4
5
6
public abstract class Element {
//定义业务逻辑
public abstract void doSomething();
//允许谁来访问
public abstract void accept(IVisitor visitor);
}

抽象元素有两类方法:一是本身的业务逻辑,也就是元素作为一个业务处理单元必须完成的职责;另外一个是允许哪一个访问者来访问。我们来看具体元素,如代码清单25-12所示。

代码清单25-12 具体元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ConcreteElement1 extends Element{
//完善业务逻辑
public void doSomething(){
//业务处理
}
//允许那个访问者访问
public void accept(IVisitor visitor){
visitor.visit(this);
}
}
public class ConcreteElement2 extends Element{
//完善业务逻辑
public void doSomething(){
//业务处理
}
//允许那个访问者访问
public void accept(IVisitor visitor){
visitor.visit(this);
}
}

它定义了两个具体元素,我们再来看抽象访问者,一般是有几个具体元素就有几个访问方法,如代码清单25-13所示。

代码清单25-13 抽象访问者

1
2
3
4
5
public interface IVisitor {
//可以访问哪些对象
public void visit(ConcreteElement1 el1);
public void visit(ConcreteElement2 el2);
}

具体访问者如代码清单25-14所示。

代码清单25-14 具体访问者

1
2
3
4
5
6
7
8
9
10
public class Visitor implements IVisitor {
//访问el1元素
public void visit(ConcreteElement1 el1) {
el1.doSomething();
}
//访问el2元素
public void visit(ConcreteElement2 el2) {
el2.doSomething();
}
}

结构对象是产生出不同的元素对象,我们使用工厂方法模式来模拟,如代码清单25-15 所示。

代码清单25-15 结构对象

1
2
3
4
5
6
7
8
9
10
11
12
public class ObjectStruture {
//对象生成器,这里通过一个工厂方法模式模拟
public static Element createElement(){
Random rand = new Random();
if(rand.nextInt(100) > 50){
return new ConcreteElement1();
}
else{
return new ConcreteElement2();
}
}
}

进入了访问者角色后,我们对所有的具体元素的访问就非常简单了,我们通过一个场景类模拟这种情况,如代码清单25-16所示。

代码清单25-16 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
for(int i=0;i<10;i++){
//获得元素对象
Element el = ObjectStruture.createElement();
//接受访问者访问
el.accept(new Visitor());
}
}
}

通过增加访问者,只要是具体元素就非常容易访问,对元素的遍历就更加容易了,甭管它是什么对象,只要它在一个容器中,都可以通过访问者来访问,任务集中化。这就是访问者模式。

26.2 状态模式的定义

上面的例子中多次提到状态,本节讲的就是状态模式,什么是状态模式呢?其定义如下:

Allow an object to alter its behavior when its internal state changes.The object will appear to change its class.(当一个对象内在状态改变时允许其改变行为,这个对象看起来像改变了其类。)

状态模式的核心是封装,状态的变更引起了行为的变更,从外部看起来就好像这个对象对应的类发生了改变一样。状态模式的通用类图如图26-5所示。

image-20210930110452937

图26-5 状态模式通用类图

我们先来看看状态模式中的3个角色。

  • State——抽象状态角色

接口或抽象类,负责对象状态定义,并且封装环境角色以实现状态切换。

  • ConcreteState——具体状态角色

每一个具体状态必须完成两个职责:本状态的行为管理以及趋向状态处理,通俗地说, 就是本状态下要做的事情,以及本状态如何过渡到其他状态。

  • Context——环境角色

定义客户端需要的接口,并且负责具体状态的切换。

状态模式相对来说比较复杂,它提供了一种对物质运动的另一个观察视角,通过状态变更促使行为的变化,就类似水的状态变更一样,一碗水的初始状态是液态,通过加热转变为气态,状态的改变同时也引起体积的扩大,然后就产生了一个新的行为:鸣笛或顶起壶盖, 瓦特就是这么发明蒸汽机的。我们再来看看状态模式的通用源代码,首先来看抽象环境角色,如代码清单26-14所示。

代码清单26-14 抽象环境角色

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class State {
//定义一个环境角色,提供子类访问
protected Context context;
//设置环境角色
public void setContext(Context _context){
this.context = _context;
}
//行为1
public abstract void handle1();
//行为2
public abstract void handle2();
}

抽象环境中声明一个环境角色,提供各个状态类自行访问,并且提供所有状态的抽象行为,由各个实现类实现。具体环境角色如代码清单26-15所示。

代码清单26-15 环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class ConcreteState1 extends State {

@Override
public void handle1() {
//本状态下必须处理的逻辑
}
@Override
public void handle2() {
//设置当前状态为stat2
super.context.setCurrentState(Context.STATE2);
//过渡到state2状态,由Context实现
super.context.handle2();
}
}
public class ConcreteState2 extends State {
@Override
public void handle1() {
//设置当前状态为state1
super.context.setCurrentState(Context.STATE1);
//过渡到state1状态,由Context实现
super.context.handle1();
}
@Override
public void handle2() {
//本状态下必须处理的逻辑
}
}

具体环境角色有两个职责:处理本状态必须完成的任务,决定是否可以过渡到其他状态。我们再来看环境角色,如代码清单26-16所示。

代码清单26-16 具体环境角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Context {
//定义状态
public final static State STATE1 = new ConcreteState1();
public final static State STATE2 = new ConcreteState2();
//当前状态
private State CurrentState;
//获得当前状态
public State getCurrentState() {
return CurrentState;
}
//设置当前状态
public void setCurrentState(State currentState) {
this.CurrentState = currentState;
//切换状态
this.CurrentState.setContext(this);
}
//行为委托
public void handle1(){
this.CurrentState.handle1();
}
public void handle2(){
this.CurrentState.handle2();
}
}

环境角色有两个不成文的约束:

  • 把状态对象声明为静态常量,有几个状态对象就声明几个静态常量。
  • 环境角色具有状态抽象角色定义的所有行为,具体执行使用委托方式。

我们再来看场景类如何执行,如代码清单26-17所示。

代码清单26-17 具体环境角色

1
2
3
4
5
6
7
8
9
10
11
public class Client {
public static void main(String[] args) {
//定义环境角色
Context context = new Context();
//初始化状态
context.setCurrentState(new ConcreteState1());
//行为执行
context.handle1();
context.handle2();
}
}

看到没?我们已经隐藏了状态的变化过程,它的切换引起了行为的变化。对外来说,我们只看到行为的发生改变,而不用知道是状态变化引起的。

第27章 解释器模式

27.1 四则运算你会吗

在银行、证券类项目中,经常会有一些模型运算,通过对现有数据的统计、分析而预测不可知或未来可能发生的商业行为。模型运算大部分是针对海量数据的,例如建立一个模型公式,分析一个城市的消费倾向,进而影响银行的营销和业务扩张方向。一般的模型运算都有一个或多个运算公式,通常是加、减、乘、除四则运算,偶尔也有指数、开方等复杂运算。具体到一个金融业务中,模型公式是非常复杂的,虽然只有加、减、乘、除四则运算, 但是公式有可能有十多个参数,而且上百个业务品各有不同的取参路径,同时相关表的数据量都在百万级。呵呵,复杂了吧,不复杂那就不叫金融业务,我们来讲讲运算的核心——模型公式及其如何实现。

业务需求:输入一个模型公式(加、减运算),然后输入模型中的参数,运算出结果。
设计要求

  • 公式可以运行时编辑,并且符合正常算术书写方式,例如a+b-c。
  • 高扩展性,未来增加指数、开方、极限、求导等运算符号时较少改动。
  • 效率可以不用考虑,晚间批量运算。

需求不复杂,若仅仅对数字采用四则运算,每个程序员都可以写出来。但是增加了增加模型公式就复杂了。先解释一下为什么需要公式,而不采用直接计算的方法,例如有如下3 个公式:

  • 业务种类1的公式:a+b+c-d。
  • 业务种类2的公式:a+b+e-d。
  • 业务种类3的公式:a-f。

其中,a、b、c、d、e、f参数的值都可以取得,如果使用直接计算数值的方法需要为每个品种写一个算法,目前仅仅是3个业务种类,那上百个品种呢?歇菜了吧!建立公式,然后通过公式运算才是王道。

我们以实现加、减算法(由于篇幅所限,乘、除法的运算读者可以自行扩展)的公式为例,讲解如何解析一个固定语法逻辑。由于使用语法解析的场景比较少,而且一些商业公司 (如SAS、SPSS等统计分析软件)都支持类似的规则运算,亲自编写语法解析的工作已经非常少,以下例程采用逐步分析方法,带领大家了解这一实现过程。

想想公式中有什么?仅有两类元素:运算元素和运算符号,运算元素就是指a、b、c等符号,需要具体赋值的对象,也叫做终结符号,为什么叫终结符号呢?因为这些元素除了需要赋值外,不需要做任何处理,所有运算元素都对应一个具体的业务参数,这是语法中最小的单元逻辑,不可再拆分;运算符号就是加减符号,需要我们编写算法进行处理,每个运算符号都要对应处理单元,否则公式无法运行,运算符号也叫做非终结符号。两类元素的共同点是都要被解析,不同点是所有的运算元素具有相同的功能,可以用一个类表示,而运算符号则是需要分别进行解释,加法需要加法解析器,减法需要减法解析器。分析到这里,我们就可以先画一个简单的类图,如图27-1所示。

image-20210930111518583

图27-1 初步分析加减法类图

这是一个很简单的类图,VarExpression用来解析运算元素,各个公式能运算元素的数量是不同的,但每个运算元素都对应一个VarExpression对象。SybmolExpression负责解析符号,由两个子类AddExpression(负责加法运算)和SubExpression(负责减法运算)来实现。 解析的工作完成了,我们还需要把安排运行的先后顺序(加减法不用考虑,但是乘除法呢? 注意扩展性),并且还要返回结果,因此我们需要增加一个封装类来进行封装处理,由于我们只做运算,暂时还不与业务有关联,定义为Calculator类。分析到这里,思路就比较清晰了,优化后加减法类图如图27-2所示。

Calculator的作用是封装,根据迪米特法则,Client只与直接的朋友Calculator交流,与其 他类没关系。整个类图的结构比较清晰,下面填充类图中的方法,完整类图如图27-3所示。

类图已经完成,下面来看代码实现。Expression抽象类如代码清单27-1所示。

代码清单27-1 抽象表达式类

1
2
3
4
public abstract class Expression {
//解析公式和数值,其中var中的key值是公式中的参数,value值是具体的数字
public abstract int interpreter(HashMap<String,Integer> var);
}

image-20210930111633860

图27-2 优化后加减法类图

image-20210930111653467

图27-3 完整加减法类图

抽象类非常简单,仅一个方法interpreter负责对传递进来的参数和值进行解析和匹配,其中输入参数为HashMap类型,key值为模型中的参数,如a、b、c等,value为运算时取得的具体数字。

变量解析器如代码清单27-2所示。

代码清单27-2 变量解析器

1
2
3
4
5
6
7
8
9
10
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);
}
}

抽象运算符号解析器如代码清单27-3所示。

代码清单27-3 抽象运算符号解析器

1
2
3
4
5
6
7
8
9
public abstract class SymbolExpression extends Expression {
protected Expression left;
protected Expression right;
//所有的解析公式都应只关心自己左右两个表达式的结果
public SymbolExpression(Expression _left,Expression _right){
this.left = _left;
this.right = _right;
}
}

这个解析过程还是比较有意思的,每个运算符号都只和自己左右两个数字有关系,但左右两个数字有可能也是一个解析的结果,无论何种类型,都是Expression的实现类,于是在对运算符解析的子类中增加了一个构造函数,传递左右两个表达式。具体的加、减法解析器如代码清单27-4、代码清单27-5所示。

代码清单27-4 加法解析器

1
2
3
4
5
6
7
8
9
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);
}
}

代码清单27-5 减法解析器

1
2
3
4
5
6
7
8
9
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如代码清单27-6所示。

代码清单27-6 解析器封装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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);
}
}

方法比较长,我们来分析一下,Calculator构造函数接收一个表达式,然后把表达式转化为char数组,并判断运算符号,如果是“+”则进行加法运算,把左边的数(left变量)和右边的数(right变量)加起来就可以了,那左边的数为什么是在栈中呢?例如这个公式:a+b-c, 根据for循环,首先被压入栈中的应该是有a元素生成的VarExpression对象,然后判断到加号时,把a元素的对象VarExpression从栈中弹出,与右边的数组b进行相加,b又是怎么得来的呢?当前的数组游标下移一个单元格即可,同时为了防止该元素再次被遍历,则通过++i的方式跳过下一个遍历——于是一个加法的运行结束。减法也采用相同的运行原理。

为了满足业务要求,我们设置了一个Client类来模拟用户情况,用户要求可以扩展,可以修改公式,那就通过接收键盘事件来处理,Client类如代码清单27-7所示。

代码清单27-7 客户模拟类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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))){
String in = (new BufferedReader(new InputStreamReader (System.in))).readLine();
map.put(String.valueOf(ch),Integer.valueOf(in));
}
}
}
return map;
}
}

其中,getExpStr是从键盘事件中获得的表达式,getValue方法是从键盘事件中获得表达式中的元素映射值,运行过程如下。

  • 首先,要求输入公式。
1
请输入表达式:a+b-c
  • 其次,要求输入公式中的参数。
1
2
3
请输入a的值:100
请输入b的值:20
请输入c的值:40
  • 最后,运行出结果。
1
运算结果为:a+b-c=80

看,要求输入一个公式,然后输入参数,运行结果出来了!那我们是不是可以修改公式?当然可以,我们只要输入公式,然后输入相应的值就可以了,公式是在运行时定义的, 而不是在运行前就制定好的,是不是类似于初中学过的“代数”这门课?先公式,然后赋值, 运算出结果。

需求已经开发完毕,公式可以自由定义,只要符合规则(有变量有运算符合)就可以运算出结果;若需要扩展也非常容易,只要增加SymbolExpression的子类就可以了,这就是解释器模式。

27.2 解释器模式的定义

解释器模式(Interpreter Pattern)是一种按照规定语法进行解析的方案,在现在项目中使用较少,其定义如下:

Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.(给定一门语言,定义它的文法的一种表示,并定义一个解释器,该解释器使用该表示来解释语言中的句子。)

解释器模式的通用类图如图27-4所示。

image-20210930112248422

图27-4 解释器模式通用类图
  • AbstractExpression——抽象解释器

具体的解释任务由各个实现类完成,具体的解释器分别由TerminalExpression和Non-terminalExpression完成。

  • TerminalExpression——终结符表达式

实现与文法中的元素相关联的解释操作,通常一个解释器模式中只有一个终结符表达式,但有多个实例,对应不同的终结符。具体到我们例子就是VarExpression类,表达式中的每个终结符都在栈中产生了一个VarExpression对象。

  • NonterminalExpression——非终结符表达式

文法中的每条规则对应于一个非终结表达式,具体到我们的例子就是加减法规则分别对应到AddExpression和SubExpression两个类。非终结符表达式根据逻辑的复杂程度而增加,原则上每个文法规则都对应一个非终结符表达式。

  • Context——环境角色

具体到我们的例子中是采用HashMap代替。

解释器是一个比较少用的模式,以下为其通用源码,可以作为参考。抽象表达式通常只有一个方法,如代码清单27-8所示。

代码清单27-8 抽象表达式

1
2
3
4
public abstract class Expression {
//每个表达式必须有一个解析任务
public abstract Object interpreter(Context ctx);
}

抽象表达式是生成语法集合(也叫做语法树)的关键,每个语法集合完成指定语法解析任务,它是通过递归调用的方式,最终由最小的语法单元进行解析完成。终结符表达式如代码清单27-9所示。

代码清单27-9 终结符表达式

1
2
3
4
5
6
public class TerminalExpression extends Expression {
//通常终结符表达式只有一个,但是有多个对象
public Object interpreter(Context ctx) {
return null;
}
}

通常,终结符表达式比较简单,主要是处理场景元素和数据的转换。

非终结符表达式如代码清单27-10所示。

代码清单27-10 非终结符表达式

1
2
3
4
5
6
public class TerminalExpression extends Expression {
//通常终结符表达式只有一个,但是有多个对象
public Object interpreter(Context ctx) {
return null;
}
}

每个非终结符表达式都代表了一个文法规则,并且每个文法规则都只关心自己周边的文法规则的结果(注意是结果),因此这就产生了每个非终结符表达式调用自己周边的非终结符表达式,然后最终、最小的文法规则就是终结符表达式,终结符表达式的概念就是如此, 不能够再参与比自己更小的文法运算了。

客户类如代码清单27-11所示。

代码清单27-11 客户类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client {
public static void main(String[] args) {
Context ctx = new Context();
//通常定一个语法容器,容纳一个具体的表达式,通常为ListArray、LinkedList、Stack等类型
Stack&Expression> stack = null;
for(;;){
//进行语法判断,并产生递归调用
}
//产生一个完整的语法树,由各个具体的语法分析进行解析
Expression exp = stack.pop();
//具体元素进入场景
exp.interpreter(ctx);
}
}

通常Client是一个封装类,封装的结果就是传递进来一个规范语法文件,解析器分析后产生结果并返回,避免了调用者与语法解析器的耦合关系。

28.1 内存溢出,司空见惯

下午,我正在开会中,老大推门进来。

“三儿,出来一下。”

我刚出会议室门口,老大就发话了。

“郎当(姓朗,顺口就叫郎当)的那个报考系统又crash了一台机器,两天已经宕了4次 了,你这边还有紧急的事情没有?……没有,那赶快过去顶一下,就运行三天的程序,两天 宕了4次,还怎么玩?!”

我马上收拾东西,冲到马路上拦了出租车,同时打电话给郎当。

“三哥,厂商人员已经定位出了,OutOfMemory内存溢出,没查到有内存泄漏的情况,现 在还在跟踪……是突然暴涨的,都是在繁忙期出现问题的……”

内存溢出对Java应用来说实在是太平常了,有以下两种可能。

  • 内存泄漏

无意识的代码缺陷,导致内存泄漏,JVM不能获得连续的内存空间。

  • 对象太多

代码写得很烂,产生的对象太多,内存被耗尽。现在的情况是没有内存泄漏,那只有一种原因——代码太差把内存耗尽。

到现场后,郎当给我介绍了一下系统情况。该系统是一个报考系统,其中有一个模块负责社会人员报名,该模块对全国的考试人员只开放3天,并且限制报考人员数量。第一天9点开始报考,系统慢得像蜗牛,基本上都不能访问,后来设置了HTTP Server的并发数量,稍有缓解,40分钟后宕了一台机器,10分钟后,又挂了一台,下午3点又挂了一台,看样子晚上要让郎当去寺庙烧烧香了。

该系统一共有8台应用服务器,基本上CPU繁忙程度都在60%以上,HTTP的最大并发是2000,平均分配到每台应用服务器上没有太大的压力,于是怀疑是代码问题,然后详细了解了一下业务和数据流逻辑,基本的业务操作过程清楚了,先登录(没有账号的,则要先注册),登录后,需要填写以下信息:

  • 考试科目,选择框。
  • 考试地点,选择框,根据科目不同,列表不同。
  • 准考证邮寄地址,输入框。

还有其他一堆信息,我们以这三者作为代表来讲解。信息填写完毕后,点击确认,报名就结束了。简单程序的业务逻辑也确实是这样,为什么出现Crash情况呢?那肯定是和压力有关系!

我们先把这个过程的静态类图画出来,如图28-1所示。

image-20210930113627220

图28-1 报考系统类图

很简单的工厂方法模式,表现层通过工厂方法模式创建对象,然后传递给业务层和持久层,最终保存到数据库中,为什么要使用工厂方法模式而不用直接new一个对象呢?因为是在框架下编程,必须有一个对象工厂(ObjectFactory,Spring也有对象工厂)。我们先来看报考信息,如代码清单28-1所示。

代码清单28-1 报考信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class SignInfo {
//报名人员的ID
private String id;
//考试地点
private String location;
//考试科目
private String subject;
//邮寄地址
private String postAddress;
public String getId() {
return id;
}
public void setId(String id) {

this.id = id;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getPostAddress() {
return postAddress;
}
public void setPostAddress(String postAddress) {
this.postAddress = postAddress;
}
}

它是一个很简单的POJO对象(Plain Ordinary Java Object,简单Java对象)。我们再来看工厂类,如代码清单28-2所示。

代码清单28-2 报考信息工厂

1
2
3
4
5
6
public class SignInfoFactory {
//报名信息的对象工厂
public static SignInfo getSignInfo(){
return new SignInfo();
}
}

工厂类就这么简单?非也,这是我们的教学代码,真实的ObjectFactory要复杂得多,主要是注入了部分Handler的管理。表现层是如何创建对象的,如代码清单28-3所示。

代码清单28-3 场景类

1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) {
//从工厂中获得一个对象
SignInfo signInfo = SignInfoFactory.getSignInfo();
//进行其他业务处理
}
}

就这么简单,但是简单为什么会出现问题呢?而且这样写也没有问题呀,很标准的工厂方法模式,应该不会有大问题,然后又看了看系统厂商提供的分析报告,报告中指出:内存突然由800MB飙升到1.4GB,新的对象申请不到内存空间,于是出现OutOfMemory,同时报告中还列出宕机时刻内存中的对象,其中SignInfo类的对象就有400MB,疯子,绝对是疯子!报告都没有看嘛!

问题找到了,我拉郎当过来谈话,“厂商不是分析出原因了嘛,人家已经指出SignInfo类的对象占用了400MB多的内存,这是怎么回事?”

“三哥,这是很正常的,这么大的访问量,产生出这么多的SignInfo对象也是应该的,内 存中有这么多对象并不表示这些对象正在被使用呀,估计很大一部分还没有被回收而已,垃 圾回收器什么时候回收内存中的对象这是不确定的。你看,并发200多个,这可是并发数 量……”

我想了想,也确实是这么回事。既然已经定位是内存中对象太多,那就应该想到使用一种共享的技术减少对象数量,那怎么共享呢?

大家知道,对象池(Object Pool)的实现有很多开源工具,比如Apache的commons-pool 就是一个非常不错的池工具,我们暂时还用不到这种重量级的工具,我们自己来设计一个共享对象池,需要实现如下两个功能。

  • 容器定义

我们要定义一个池容器,在这个容器中容纳哪些对象。

  • 提供客户端访问的接口

我们要提供一个接口供客户端访问,池中有可用对象时,可以直接从池中获得,否则建立一个新的对象,并放置到池中。

设计思路有了,那我们池中对象的标准是什么呢?你想想看,如果你把所有的对象都放到池中,那还有什么意义?内存早就给你撑爆了!这么多对象,必然有一些相同的属性值,如几十万SignInfo对象中,考试科目就4个,考试地点也就是30多个,其他的属性则是每个对象都不相同的,我们把对象的相同属性提取出来,不同的属性在系统内进行赋值处理,是不是就可以建立一个池了?话无须多说,我们以类图来表示,如图28-2所示。

image-20210930113940991

图28-2 增加对象池的类图
做一个很小的改动,增加了一个子类,实现带缓冲池的对象建立,同时在工厂类上增加了一个容器对象HashMap,保存池中的所有对象。我们先来看产品子类,如代码清单28-4所示。

代码清单28-4 带对象池的报考信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SignInfo4Pool extends SignInfo {
//定义一个对象池提取的KEY值
private String key;
//构造函数获得相同标志
public SignInfo4Pool(String _key){
this.key = _key;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
}

很简单,就是增加了一个key值,为什么要增加key值?为什么要使用子类,而不在SignInfo类上做修改?好,我来给你解释为什么要这样做,我们刚刚已经分析了所有的SignInfo对象都有一些共同的属性:考试科目和考试地点,我们把这些共性提取出来作为所有对象的外部状态,在这个对象池中一个具体的外部状态只有一个对象。按照这个设计,我们定义key值的标准为:考试科目+考试地点的复合字符串作为唯一的池对象标准,也就是说在对象池中,一个key值唯一对应一个对象。


注意 在对象池中,对象一旦产生,必然有一个唯一的、可访问的状态标志该对象,而且池中的对象声明周期是由池容器决定,而不是由使用者决定的。


你可能马上就要提出了,为什么不建立一个新的类,包含subject和location两个属性作为外部状态呢?嗯,这是一个办法,但不是最好的办法,有两个原因:

  • 修改的工作量太大,增加的这个类由谁来创建呢?同时,SignInfo类是否也要修改 呢?你不可能让两段相同的POJO程序同时出现在同一模块中吧!
  • 性能问题,我们会在扩展模块中讲解。

说了这么多,我们还是继续来看程序,工厂类如代码清单28-5所示。

代码清单28-5 带对象池的工厂类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class SignInfoFactory {
//池容器
private static HashMap<String,SignInfo> pool = new HashMap<String,SignInfo>();
//报名信息的对象工厂
@Deprecated
public static SignInfo(){
return new SignInfo();
}
//从池中获得对象
public static SignInfo getSignInfo(String key){
//设置返回对象
SignInfo result = null;
//池中没有该对象,则建立,并放入池中
if(!pool.containsKey(key)){
System.out.println(key + "----建立对象,并放置到池中");
result = new SignInfo4Pool(key);
pool.put(key, result);
}
else{
result = pool.get(key);
System.out.println(key +"---直接从池中取得");
}
return result;
}
}

方法都很简单,不多解释。读者需要注意一点的是@Deprecated注解,不要有删除投产中代码的念头,如果方法或类确实不再使用了,增加该注解,表示该方法或类已经过时,尽量不要再使用了,我们应该保持历史原貌,同时也有助于版本向下兼容,特别是在产品级研发中。

我们再来看看客户端是如何调用的,如代码清单28-6所示。

代码清单28-6 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
//初始化对象池
for(int i=0;i<4;i++){
String subject = "科目" + i;
//初始化地址
for(int j=0;j<30;j++){
String key = subject + "考试地点"+j;
SignInfoFactory.getSignInfo(key);
}
}
SignInfo signInfo = SignInfoFactory.getSignInfo("科目1考试地点1");

}
}

运行结果如下所示:

1
2
3
4
5
6
科目3考试地点25----建立对象,并放置到池中 
科目3考试地点26----建立对象,并放置到池中
科目3考试地点27----建立对象,并放置到池中
科目3考试地点28----建立对象,并放置到池中
科目3考试地点29----建立对象,并放置到池中
科目1考试地点1---直接从池中取得

前面还有很多的对象创建提示语句,不再复制。通过这样的改造后,我们想想内存中有多少个SignInfo对象?是的,最多120个对象,相比之前几万个SignInfo对象优化了非常多。细心的读者可能注意到了SignInfo4Pool类基本上没有跑出我们的视线范围,仅仅在工厂方法中使用到了,尽量缩小变更引起的风险,想想看我们的改动是不是很小,只要在展示层中拼一个字符串,然后传递到工厂方法中就可以了。

通过这样的改造后,第三天系统运行得非常稳定,CPU占用率也下降了,而且以后再也没有出现类似问题,这就是享元模式的功劳。

image-20210930114537654

28.2 享元模式的定义

享元模式(Flyweight Pattern)是池技术的重要实现方式,其定义如下:Use sharing to support large numbers of fine-grained objects efficiently.(使用共享对象可有效地支持大量的细粒度的对象。)

享元模式的定义为我们提出了两个要求:细粒度的对象和共享对象。我们知道分配太多的对象到应用程序中将有损程序的性能,同时还容易造成内存溢出,那怎么避免呢?就是享元模式提到的共享技术。我们先来了解一下对象的内部状态和外部状态。

要求细粒度对象,那么不可避免地使得对象数量多且性质相近,那我们就将这些对象的信息分为两个部分:内部状态(intrinsic)与外部状态(extrinsic)。

  • 内部状态

内部状态是对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变, 如我们例子中的id、postAddress等,它们可以作为一个对象的动态附加信息,不必直接储存在具体某个对象中,属于可以共享的部分。

  • 外部状态

外部状态是对象得以依赖的一个标记,是随环境改变而改变的、不可以共享的状态,如我们例子中的考试科目+考试地点复合字符串,它是一批对象的统一标识,是唯一的一个索引值。

有了对象的两个状态,我们就可以来看享元模式的通用类图,如图28-3所示。

图28-3 享元模式的通用类图

类图也很简单,我们先来看我们享元模式角色名称。

  • Flyweight——抽象享元角色

它简单地说就是一个产品的抽象类,同时定义出对象的外部状态和内部状态的接口或实现。

  • ConcreteFlyweight——具体享元角色

具体的一个产品类,实现抽象角色定义的业务。该角色中需要注意的是内部状态处理应该与环境无关,不应该出现一个操作改变了内部状态,同时修改了外部状态,这是绝对不允许的。

  • unsharedConcreteFlyweight——不可共享的享元角色

不存在外部状态或者安全要求(如线程安全)不能够使用共享技术的对象,该对象一般不会出现在享元工厂中。

  • FlyweightFactory——享元工厂

职责非常简单,就是构造一个池容器,同时提供从池中获得对象的方法。

享元模式的目的在于运用共享技术,使得一些细粒度的对象可以共享,我们的设计确实也应该这样,多使用细粒度的对象,便于重用或重构。我来看享元模式的通用代码,先看抽象享元角色,如代码清单28-7所示。

代码清单28-7 抽象享元角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class Flyweight {
//内部状态
private String intrinsic;
//外部状态
protected final String Extrinsic;
//要求享元角色必须接受外部状态
public Flyweight(String _Extrinsic){
this.Extrinsic = _Extrinsic;
}
//定义业务操作
public abstract void operate();
//内部状态的getter/setter
public String getIntrinsic() {
return intrinsic;
}
public void setIntrinsic(String intrinsic) {
this.intrinsic = intrinsic;
}
}

抽象享元角色一般为抽象类,在实际项目中,一般是一个实现类,它是描述一类事物的方法。在抽象角色中,一般需要把外部状态和内部状态(当然了,可以没有内部状态,只有行为也是可以的)定义出来,避免子类的随意扩展。我们再来看具体的享元角色,如代码清单28-8所示。

代码清单28-8 具体享元角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ConcreteFlyweight1 extends Flyweight{
//接受外部状态
public ConcreteFlyweight1(String _Extrinsic){

super(_Extrinsic);
}
//根据外部状态进行逻辑处理
public void operate(){
//业务逻辑
}
}
public class ConcreteFlyweight2 extends Flyweight{
//接受外部状态
public ConcreteFlyweight2(String _Extrinsic){
super(_Extrinsic);
}
//根据外部状态进行逻辑处理
public void operate(){
//业务逻辑
}
}

这很简单,实现自己的业务逻辑,然后接收外部状态,以便内部业务逻辑对外部状态的依赖。注意,我们在抽象享元中对外部状态加上了final关键字,防止意外产生,什么意外? 获得了一个外部状态,然后无意修改了一下,池就混乱了!


注意 在程序开发中,确认只需要一次赋值的属性则设置为final类型,避免无意修改导致逻辑混乱,特别是Session级的常量或变量。


我们继续看享元工厂,如代码清单28-9所示。

代码清单28-9 享元工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class FlyweightFactory {
//定义一个池容器
private static HashMap<String,Flyweight> pool= new HashMap<String,Flyweight>();
//享元工厂
public static Flyweight getFlyweight(String Extrinsic){
//需要返回的对象
Flyweight flyweight = null;
//在池中没有该对象
if(pool.containsKey(Extrinsic)){
flyweight = pool.get(Extrinsic);
}
else{
//根据外部状态创建享元对象
flyweight = new ConcreteFlyweight1(Extrinsic);
//放置到池中
pool.put(Extrinsic, flyweight);
}
return flyweight;

}
}

29.1 我有一个梦想……

我们每个人都有理想,但不要只是空想,理想是要靠今天的拼搏来实现的。今天咱们就来谈谈自己的理想,如希望成为一个富翁,身价过亿,有两家大公司,一家是房地产公司, 另一家是服装制造公司。这两家公司都很赚钱,天天帮你累积财富。其实你并不关心公司的类型,你关心的是它们是不是在赚钱,赚了多少,这才是你关注的。商人嘛,唯利是图是其本性,偷税漏税是方法,欺上瞒下、压榨员工血汗是常用的手段,先用类图表示一下这两个公司,如图29-1所示。

image-20210930145552835

图29-1 盈利模式的类图
类图很简单,声明了一个Corp抽象类,定义一个公司的抽象模型,公司首要是赚钱的,做义务或善举那也是有背后利益支撑的,还是赞成这句话“天下熙熙,皆为利来;天下攘攘,皆为利往”。我们先看Corp类的源代码,如代码清单29-1所示。

代码清单29-1 抽象公司

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class Corp {
/*
* 如果是公司就应该有生产,不管是软件公司还是制造业公司
* 每家公司生产的东西都不一样,所以由实现类来完成
*/
protected abstract void produce();
/** 有产品了,那肯定要销售啊,不销售公司怎么生存 */
protected abstract void sell();
//公司是干什么的?赚钱的
public void makeMoney(){
//每个公司都是一样,先生产
this.produce();
//然后销售
this.sell();
}
}

怎么这是模板方法模式啊?是的,这是个引子,请继续往下看。合适的方法存在合适的类中,这个基本上是每本Java基础书上都会讲的,但是到实际的项目中应用的时候就不是这么回事儿了。我们继续看两个实现类是如何实现的,先看HouseCorp类,这是最赚钱的公司,如代码清单29-2所示。

代码清单29-2 房地产公司

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class HouseCorp extends Corp {
//房地产公司盖房子
protected void produce() {
System.out.println("房地产公司盖房子...");
}
//房地产公司卖房子,自己住那可不赚钱
protected void sell() {
System.out.println("房地产公司出售房子...");
}
//房地产公司很High了,赚钱,计算利润
public void makeMoney(){
super.makeMoney();
System.out.println("房地产公司赚大钱了...");
}
}

房地产公司按照正规翻译来说应该是realty corp,这个是比较准确的翻译,但是我问你把房地产公司翻译成英文,你的第一反应是什么?house corp!这是中式英语。我们再来看服装公司,虽然不景气,但好歹也是赚钱的,如代码清单29-3所示。

代码清单29-3 服装公司

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ClothesCorp extends Corp {
//服装公司生产的就是衣服了
protected void produce() {
System.out.println("服装公司生产衣服...");
}
//服装公司卖服装,可只卖服装,不卖穿衣服的模特
protected void sell() {
System.out.println("服装公司出售衣服...");
}
//服装公司不景气,但怎么说也是赚钱行业
public void makeMoney(){
super.makeMoney();
System.out.println("服装公司赚小钱...");
}
}

两个公司都有了,那肯定有人会关心两个公司的运营情况。你也要知道它是生产什么的,以及赚多少钱吧。通过场景类来进行模拟,如代码清单29-4所示。

代码清单29-4 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
System.out.println("-------房地产公司是这样运行的-------");
//先找到我的公司
HouseCorp houseCorp =new HouseCorp();
//看我怎么挣钱
houseCorp.makeMoney();
System.out.println("\n");
System.out.println("-------服装公司是这样运行的-------");
ClothesCorp clothesCorp = new ClothesCorp();
clothesCorp.makeMoney();
}
}

这段代码很简单,运行结果如下所示:

1
2
3
4
5
6
7
8
-------房地产公司是这样运行的------- 
房地产公司盖房子...
房地产公司出售房子...
房地产公司赚大钱了...
-------服装公司是这样运行的-------
服装公司生产衣服...
服装公司出售衣服...
服装公司赚小钱...

上述代码完全可以描述我现在的公司,但是你要知道万物都是运动的,你要用运动的眼光看问题,公司才会发展……终于有一天你觉得赚钱速度太慢,于是你上下疏通,左右打关系,终于开辟了一条赚钱的“康庄大道”:生产山寨产品!什么产品呢?即市场上什么牌子的东西火爆我生产什么牌子的东西,不管是打火机还是电脑,只要它火爆,我就生产,赚过了高峰期就换个产品,打一枪换一个牌子,不承担售后成本、也不担心销路问题,我只要正品的十分之一的价格,你买不买?哈哈,赚钱啊!

企业的方向定下来了,通过调查,苹果公司的iPod系列产品比较火爆,那咱就生产这个,把服装厂改成iPod生产厂,看类图的变化,如图29-2所示。

image-20210930145947637

图29-2 服装公司改头换面后的类图
好,我的企业改头换面了,开始生产iPod产品了,看我IPodCorp类的实现,如代码清单29-5所示。

代码清单29-5 iPod山寨公司

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IPodCorp extends Corp {
//我开始生产iPod了
protected void produce() {
System.out.println("我生产iPod...");
}
//山寨的iPod很畅销,便宜嘛
protected void sell() {
System.out.println("iPod畅销...");
}
//狂赚钱
public void makeMoney(){
super.makeMoney();
System.out.println("我赚钱呀...");
}
}

服装工厂改成了电子工厂,你这个董事长还是要去看看到底生产什么的,场景类如代码清单29-6所示。

代码清单29-6 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Client {
public static void main(String[] args) {
System.out.println("-------房地产公司是按这样运行的-------");
//先找到我的公司
HouseCorp houseCorp =new HouseCorp();
//看我怎么挣钱
houseCorp.makeMoney();
System.out.println("\n");
System.out.println("-------山寨公司是按这样运行的-------");
IPodCorp iPodCorp = new IPodCorp();
iPodCorp.makeMoney();
}
}

确实,只用修改了黑色字体这几句话,服装厂就开始变成山寨iPod生产车间,然后你就看着你的财富在积累。山寨的东西不需要特别的销售渠道(正品到哪里我就到哪里),不需要维修成本(大不了给你换个,你还想怎么样,过了高峰期我就改头换面了,你找谁维修去?投诉?投诉谁呢?),不承担广告成本(正品在打广告,我还需要吗?需要吗?),但是也有犯愁的时候,这是一个山寨工厂,要及时地生产出市场上流行的产品,转型要快,要灵活,今天从生产iPod转为生产MP4,明天再转为生产上网本,这都需要灵活的变化,不要限制得太死!那问题来了,每次我的厂房,我的工人,我的设备都在,不可能每次我换个山寨产品厂子就彻底不要了。这不行,成本忒高了点,那怎么办?

Thinking,Thinking…I got an idea!(跳跳虎语),既然产品和工厂绑得太死,那我就给你来 松松,改变设计,如图29-3所示。

image-20210930150200692

图29-3 使用快速变化的类图
公司和产品之间建立关联关系,可以彻底解决以后山寨公司生产产品的问题,工厂想换产品?太容易了!看程序说话,先看Product抽象类,如代码清单29-7所示。

代码清单29-7 抽象产品类

1
2
3
4
5
6
public abstract class Product {
//甭管是什么产品它总要能被生产出来
public abstract void beProducted();
//生产出来的东西,一定要销售出去,否则亏本
public abstract void beSelled();
}

简单!忒简单了!House产品类如代码清单29-8所示。

代码清单29-8 房子

1
2
3
4
5
6
7
8
9
10
public class House extends Product {
//豆腐渣就豆腐渣呗,好歹也是房子
public void beProducted() {
System.out.println("生产出的房子是这样的...");
}
//虽然是豆腐渣,也是能够销售出去的
public void beSelled() {
System.out.println("生产出的房子卖出去了...");
}
}

既然是产品类,那肯定有两种行为要存在:被生产和被销售,否则就不能称为产品了。我们再来看iPod产品类,如代码清单29-9所示。

代码清单29-9 iPod产品

1
2
3
4
5
6
7
8
public class IPod extends Product {
public void beProducted() {
System.out.println("生产出的iPod是这样的...");
}
public void beSelled() {
System.out.println("生产出的iPod卖出去了...");
}
}

产品是由公司生产出来的,我们来看公司Corp抽象类,如代码清单29-10所示。

代码清单29-10 抽象公司类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Corp {
//定义一个抽象的产品对象,不知道具体是什么产品
private Product product;
//构造函数,由子类定义传递具体的产品进来
public Corp(Product product){
this.product = product;
}
//公司是干什么的?赚钱的!
public void makeMoney(){
//每家公司都是一样,先生产
this.product.beProducted();
//然后销售
this.product.beSelled();
}
}

这里多了个有参构造,其目的是要继承的子类都必选重写自己的有参构造函数,把产品类传递进来,再看子类HouseCorp的实现,如代码清单29-11所示。

代码清单29-11 房地产公司

1
2
3
4
5
6
7
8
9
10
11
public class HouseCorp extends Corp {
//定义传递一个House产品进来
public HouseCorp(House house){
super(house);
}
//房地产公司很High了,赚钱,计算利润
public void makeMoney(){
super.makeMoney();
System.out.println("房地产公司赚大钱了...");
}
}

理解上没有多少难度,不多说,继续看山寨公司的实现,如代码清单29-12所示。

代码清单29-12 山寨公司

1
2
3
4
5
6
7
8
9
10
11
public class ShanZhaiCorp extends Corp {
//产什么产品,不知道,等被调用的才知道
public ShanZhaiCorp(Product product){
super(product);
}
//狂赚钱
public void makeMoney(){
super.makeMoney();
System.out.println("我赚钱呀...");
}
}

HouseCorp类和ShanZhaiCorp类的区别是在有参构造的参数类型上,HouseCorp类比较明 确,我就是只要House类,所以直接定义传递进来的必须是House类, 一个类尽可能少地承 担职责,那方法也一样,既然HouseCorp类已经非常明确地只生产House产品,那为什么不定 义成House类型呢?ShanZhaiCorp就不同了,它确定不了生产什么类型。

好了,两大对应的阵营都已经产生了。我们再看Client程序,如代码清单29-13所示。

代码清单29-13 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
House house = new House();
System.out.println("-------房地产公司是这样运行的-------");
//先找到房地产公司
HouseCorp houseCorp =new HouseCorp(house);
//看我怎么挣钱
houseCorp.makeMoney();
System.out.println("\n");
//山寨公司生产的产品很多,不过我只要指定产品就成了
System.out.println("-------山寨公司是这样运行的-------");
ShanZhaiCorp shanZhaiCorp = new ShanZhaiCorp(new IPod());
shanZhaiCorp.makeMoney();
}
}

运行结果如下所示:

1
2
3
4
5
6
7
8
-------房地产公司是这样运行的------- 
生产出的房子是这样的...
生产出的房子卖出去了...
房地产公司赚大钱了...
-------山寨公司是这样运行的-------
生产出的iPod是这个样子的...
生产出的iPod卖出去了...
我赚钱呀...

突然有一天,老板良心发现了,不准备生产这种“三无”产品了,那我们程序该怎么修改呢?如果仍重操旧业,生产衣服,那该如何处理呢?很容易处理,增加一个产品类,然后稍稍修改一下场景就可以了,我们来看衣服产品类,如代码清单29-14所示。

代码清单29-14 服装

1
2
3
4
5
6
7
8
public class Clothes extends Product {
public void beProducted() {
System.out.println("生产出的衣服是这样的...");
}
public void beSelled() {
System.out.println("生产出的衣服卖出去了...");
}
}

然后再稍稍修改一下场景类,如代码清单29-15所示。

代码清单29-15 场景类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Client {
public static void main(String[] args) {
House house = new House();
System.out.println("-------房地产公司是这样运行的-------");
//先找到房地产公司
HouseCorp houseCorp =new HouseCorp(house);
//看我怎么挣钱
houseCorp.makeMoney();
System.out.println("\n");
//山寨公司生产的产品很多,不过我只要指定产品就成了
System.out.println("-------山寨公司是这样运行的-------");
ShanZhaiCorp shanZhaiCorp = new ShanZhaiCorp(new Clothes());
shanZhaiCorp.makeMoney();
}
}

修改后的运行结果如下所示:

1
2
3
4
5
6
7
8
-------房地产公司是这样运行的------- 
生产出的房子是这样的...
生产出的房子卖出去了...
房地产公司赚大钱了...
-------山寨公司是这样运行的-------
生产出的衣服是这样的...
生产出的衣服卖出去了...
我赚钱呀...

看代码中的黑体部分,就修改了这一条语句就完成了生产产品的转换。那我们深入思考一下,既然万物都是运动的,我现在只有房地产公司和山寨公司,那以后我会不会增加一些其他的公司呢?或者房地产公司会不会对业务进行细化,如分为公寓房公司、别墅公司,以及商业房公司等呢?那我告诉你,会的!绝对会的!但是你发觉没有,这种变化对我们上面的类图来说不会做大的修改,充其量只是扩展:

  • 增加公司,要么继承Corp类,要么继承HouseCorp或ShanZhaiCorp,不用再修改原有的 类了。
  • 增加产品,继承Product类,或者继承House类,你要把房子分为公寓房、别墅、商业 用房等。

你唯一要修改的就是Client类。类都增加了,高层模块也需要修改,也就是说Corp类和Product类都可以自由地扩展,而不会对整个应用产生太大的变更,这就是桥梁模式。

29.2 桥梁模式的定义

桥梁模式(Bridge Pattern)也叫做桥接模式,是一个比较简单的模式,其定义如下:

Decouple an abstraction from its implementation so that the two can vary independently.(将抽象和实现解耦,使得两者可以独立地变化。)

桥梁模式的重点是在“解耦”上,如何让它们两者解耦是我们要了解的重点,我们先来看桥梁模式的通用类,如图29-4所示。

image-20210930150840802

图29-4 桥梁模式通用类图

我们先来看桥梁模式中的4个角色。

  • Abstraction——抽象化角色

它的主要职责是定义出该角色的行为,同时保存一个对实现化角色的引用,该角色一般是抽象类。

  • Implementor——实现化角色

它是接口或者抽象类,定义角色必需的行为和属性。

  • RefinedAbstraction——修正抽象化角色

它引用实现化角色对抽象化角色进行修正。

  • ConcreteImplementor——具体实现化角色

它实现接口或抽象类定义的方法和属性。

桥梁模式中的几个名词比较拗口,大家只要记住一句话就成:抽象角色引用实现角色, 或者说抽象角色的部分实现是由实现角色完成的。我们来看其通用源码,先看实现化角色, 如代码清单29-16所示。

代码清单29-16 实现化角色

1
2
3
4
5
public interface Implementor {
//基本方法
public void doSomething();
public void doAnything();
}

它没有任何特殊的地方,就是一个一般的接口,定义要实现的方法。其实现类如代码清单29-17所示。

代码清单29-17 具体实现化角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ConcreteImplementor1 implements Implementor{
public void doSomething(){
//业务逻辑处理
}
public void doAnything(){
//业务逻辑处理
}
}
public class ConcreteImplementor2 implements Implementor{
public void doSomething(){
//业务逻辑处理
}
public void doAnything(){
//业务逻辑处理
}
}

上面定义了两个具体实现化角色——代表两个不同的业务逻辑。我们再来看抽象化角色,如代码清单29-18所示。

代码清单29-18 抽象化角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public abstract class Abstraction {
//定义对实现化角色的引用
private Implementor imp;
//约束子类必须实现该构造函数
public Abstraction(Implementor _imp){
this.imp = _imp;
}
//自身的行为和属性
public void request(){
this.imp.doSomething();
}
//获得实现化角色
public Implementor getImp(){
return imp;
}
}

各位可能要问,为什么要增加一个构造函数?答案是为了提醒子类,你必须做这项工作,指定实现者,特别是已经明确了实现者,则尽量清晰明确地定义出来。我们来看具体的抽象化角色,如代码清单29-19所示。

代码清单29-19 具体抽象化角色

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RefinedAbstraction extends Abstraction {
//覆写构造函数
public RefinedAbstraction(Implementor _imp){
super(_imp);
}
//修正父类的行为
@Override
public void request(){
/*
* 业务处理...
*/
super.request();
super.getImp().doAnything();
}
}

想想看,如果我们的实现化角色有很多的子接口,然后是一堆的子实现。如果在构造函数中不传递一个尽量明确的实现者,代码就很不清晰。我们来看场景类如何模拟,如代码清单29-20所示。

代码清单29-20 场景类

1
2
3
4
5
6
7
8
9
10
public class Client {
public static void main(String[] args) {
//定义一个实现化角色
Implementor imp = new ConcreteImplementor1();
//定义一个抽象化角色
Abstraction abs = new RefinedAbstraction(imp);
//执行行文
abs.request();
}
}

桥梁模式是一个非常简单的模式,它只是使用了类间的聚合关系、继承、覆写等常用功能,但是它却提供了一个非常清晰、稳定的架构。

30.1 工厂方法模式VS建造者模式

工厂方法模式注重的是整体对象的创建方法,而建造者模式注重的是部件构建的过程, 旨在通过一步一步地精确构造创建出一个复杂的对象。我们举个简单例子来说明两者的差异,如要制造一个超人,如果使用工厂方法模式,直接产生出来的就是一个力大无穷、能够飞翔、内裤外穿的超人;而如果使用建造者模式,则需要组装手、头、脚、躯干等部分,然后再把内裤外穿,于是一个超人就诞生了。纯粹使用文字来描述比较枯燥,我们还是通过程序来更加清晰地认识两者的差别。

30.1.1 按工厂方法建造超人

首先,按照工厂方法模式创建出一个超人,类图如图30-1所示。

image-20210930154828157

图30-1 按工厂方法建造超人

类图中我们按照年龄段把超人分为两种类型:成年超人(如克拉克、超能先生)和未成年超人(如Dash、Jack)。这是一个非常正宗的工厂方法模式,定义一个产品的接口,然后再定义两个实现,通过超人制造工厂制造超人。想想看我们对超人最大印象是什么?当然是他的超能力,我们以specialTalent(特殊天赋)方法来代表,先看抽象产品类,如代码清单30-1所示。

代码清单30-1 超人接口

1
2
3
4
public interface ISuperMan {
//每个超人都有特殊技能
public void specialTalent();
}

产品的接口定义好了,我们再来看具体的产品。先看成年超人,很简单,如代码清单30-2所示。

代码清单30-2 成年超人

1
2
3
4
5
6
public class AdultSuperMan implements ISuperMan {
//超能先生
public void specialTalent() {
System.out.println("超人力大无穷");
}
}

未成年超人的代码如代码清单30-3所示。

代码清单30-3 未成年超人

1
2
3
4
5
6
public class ChildSuperMan implements ISuperMan {
//超能先生的三个孩子
public void specialTalent() {
System.out.println("小超人的能力是刀枪不入、快速运动");
}
}

产品都具备,那我们编写一个工厂类,其意图就是生产超人,具体是成年超人还是未成年超人,则由客户端决定,如代码清单30-4所示。

代码清单30-4 超人制造工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SuperManFactory {
//定义一个生产超人的工厂
public static ISuperMan createSuperMan(String type){
//根据输入参数产生不同的超人
if(type.equalsIgnoreCase("adult")){
//生产成人超人
return new AdultSuperMan();
}
else if(type.equalsIgnoreCase("child")){
//生产未成年超人
return new ChildSuperMan();
}
else{
return null;
}
}
}

产品有了,工厂类也有了,剩下的工作就是开始生产超人。这也非常简单,如代码清单30-5所示。

代码清单30-5 场景类

1
2
3
4
5
6
7
8
9
public class Client {
//模拟生产超人
public static void main(String[] args) {
//生产一个成年超人
ISuperMan adultSuperMan = SuperManFactory.createSuperMan("adult");
//展示一下超人的技能
adultSuperMan.specialTalent();
}
}

建立了一个超人生产工厂,年复一年地生产超人,对于具体生产出的产品,不管是成年超人还是未成年超人,都是一个模样:深蓝色紧身衣、胸前S标记、内裤外穿,没有特殊的地方。但是我们的目的达到了——生产出超人,拯救全人类,这就是我们的意图。具体怎么生产、怎么组装,这不是工厂方法模式要考虑的,也就是说,工厂模式关注的是一个产品整体,生产出的产品应该具有相似的功能和架构。


注意 通过工厂方法模式生产出对象,然后由客户端进行对象的其他操作,但是并不代 表所有生产出的对象都必须具有相同的状态和行为,它是由产品所决定。


30.1.2 按建造者模式建造超人

我们再来看看建造者模式是如何生产超人的,如图30-2所示。

image-20210930155136413

图30-2 按建造者模式生产超人
又是一个典型的建造者模式!哎,不对呀!通用模式上抽象建造者与产品类没有关系呀!是的,我们当然可以加强了,我们在抽象建造者上使用了模板方法模式,每一个建造者都必须返回一个产品,但是产品是如何制造的,则由各个建造者自己负责。我们来看看程序,先看产品类,如代码清单30-6所示。

代码清单30-6 超人产品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SuperMan {
//超人的躯体
private String body;
//超人的特殊技能
private String specialTalent;
//超人的标志
private String specialSymbol;
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public String getSpecialTalent() {
return specialTalent;
}
public void setSpecialTalent(String specialTalent) {
this.specialTalent = specialTalent;
}
public String getSpecialSymbol() {
return specialSymbol;
}
public void setSpecialSymbol(String specialSymbol) {
this.specialSymbol = specialSymbol;
}
}

超人这个产品是由三部分组成:躯体、特殊技能、身份标记,这就类似于电子产品,首先生产出一个固件,然后再安装一个灵魂(软件驱动),最后再打上产品标签。完事了!一个崭新的产品就诞生了!我们的超人也是这样生产的,先生产一个普通的躯体,然后注入特殊技能,最后打上S标签,一个超人生产完毕。我们再来看一下建造者的抽象定义,如代码清单30-7所示。

代码清单30-7 抽象建造者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public abstract class Builder {
//定义一个超人的应用
protected final SuperMan superMan = new SuperMan();
//构建出超人的躯体
public void setBody(String body){
this.superMan.setBody(body);
}
//构建出超人的特殊技能
public void setSpecialTalent(String st){
this.superMan.setSpecialTalent(st);
}
//构建出超人的特殊标记
public void setSpecialSymbol(String ss){
this.superMan.setSpecialSymbol(ss);
}
//构建出一个完整的超人
public abstract SuperMan getSuperMan();
}

一个典型的模板方法模式,超人的各个部件(躯体、灵魂、标志)都准备好了,具体怎么组装则是由实现类来决定。我们先来看成年超人,如代码清单30-8所示。

代码清单30-8 成年超人建造者

1
2
3
4
5
6
7
8
9
public class AdultSuperManBuilder extends Builder {
@Override
public SuperMan getSuperMan() {
super.setBody("强壮的躯体");
super.setSpecialTalent("会飞行");
super.setSpecialSymbol("胸前带S标记");
return super.superMan;
}
}

怎么回事?在第11章中讲解建造者模式的时候在产品中使用了模板方法模式,在这里怎么把模板方法模式迁移到建造者了?怎么会这样?你是不是在发出这样的疑问?别疑问了! 设计模式只是提供了一个解决问题的意图:复杂对象的构建与它的表示分离,而没有具体定出一个设计模式必须是这样的实现,必须是这样的代码,灵活运用模式才是其根本,别学死板了。

我们继续看未成年超人的建造者,如代码清单30-9所示。

代码清单30-9 未成年超人建造者

1
2
3
4
5
6
7
8
9
public class ChildSuperManBuilder extends Builder {
@Override
public SuperMan getSuperMan() {
super.setBody("强壮的躯体");
super.setSpecialTalent("刀枪不入");
super.setSpecialSymbol("胸前带小S标记");
return super.superMan;
}
}

大家注意看我们这两个具体的建造者,它们都关注了产品的各个部分,在某些应用场景下甚至会关心产品的构建顺序,即使是相同的部件,装配顺序不同,产生的结果也不同,这也正是建造者模式的意图:通过不同的部件、不同装配产生不同的复杂对象。我们再来看导演类,如代码清单30-10所示。

代码清单30-10 导演类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Director {
//两个建造者的应用
private static Builder adultBuilder = new AdultSuperManBuilder();
//未成年超人的建造者
private static Builder childBuilder = new ChildSuperManBuilder();
//建造一个成年、会飞行的超人
public static SuperMan getAdultSuperMan(){
return adultBuilder.getSuperMan();
}
//建造一个未成年、刀枪不入的超人
public static SuperMan getChildSuperMan(){
return childBuilder.getSuperMan();
}
}

这很简单,不多说了!看看场景类是如何调用的,如代码清单30-11所示。

代码清单30-11 场景类

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) {
//建造一个成年超人
SuperMan adultSuperMan = Director.getAdultSuperMan();
//展示一下超人的信息
adultSuperMan.getSpecialTalent();
}
}

这个场景类的写法与工厂方法模式是相同的,但是你可以看到,在建立超人的过程中, 建造者必须关注超人的各个部件,而工厂方法模式则只关注超人的整体,这就是两者的区别。

30.1.3 最佳实践

工厂方法模式和建造者模式都属于对象创建类模式,都用来创建类的对象。但它们之间的区别还是比较明显的。

  • 意图不同

在工厂方法模式里,我们关注的是一个产品整体,如超人整体,无须关心产品的各部分是如何创建出来的;但在建造者模式中,一个具体产品的产生是依赖各个部件的产生以及装配顺序,它关注的是“由零件一步一步地组装出产品对象”。简单地说,工厂模式是一个对象创建的粗线条应用,建造者模式则是通过细线条勾勒出一个复杂对象,关注的是产品组成部分的创建过程。

  • 产品的复杂度不同

工厂方法模式创建的产品一般都是单一性质产品,如成年超人,都是一个模样,而建造者模式创建的则是一个复合产品,它由各个部件复合而成,部件不同产品对象当然不同。这不是说工厂方法模式创建的对象简单,而是指它们的粒度大小不同。一般来说,工厂方法模式的对象粒度比较粗,建造者模式的产品对象粒度比较细。

两者的区别有了,那在具体的应用中,我们该如何选择呢?是用工厂方法模式来创建对象,还是用建造者模式来创建对象,这完全取决于我们在做系统设计时的意图,如果需要详细关注一个产品部件的生产、安装步骤,则选择建造者,否则选择工厂方法模式。