架构师内功心法,熟悉电脑中的操作系统的命令模式详解

早期的黑白电视机要换台那简直是很不容易,需要跑到电视机前面扳动上面那个切换频道的按钮,一顿折腾下来才能完成一次换台。现如今,我们只需要躺在沙发上按一下遥控器的按钮就可以轻松的躺在沙发上完成一次次的换台了。这里就使用了命令模式,将换台命令和换台处理进行了分离。

还有就是餐厅的点菜单,一般是后厨先把所有的原材料组合配置好了,客户需要用餐只需要点菜即可,将需求和处理进行了解耦。

命令模式(Command Pattern)是对命令的封装,每一个命令都是操作:请求一方发出请求要求执行一个操作;接收一方收到请求,并执行操作。命名模式解耦了请求方和接收方,请求方只需要请求执行命令,不用关系命令是怎样被接收,怎样被操作以及是否被执行等等。

一、命令模式的应用场景

当系统的某项操作具备命令语义时,且命令实现不稳定,那么可以通过命令模式解耦请求与实现,利用抽象命令接口使请求方代码架构稳定,封装接收方具体命令实现细节。命令模式适用于以下几个场景:

如果自己开发一个音乐播放器,它的功能有开始播放功能、暂停播放功能、停止播放功能、拖动进度条功能,自己去操作播放器的时候并不是直接调用播放器的方法,而是通过一个控制条传达指令给播放器的内核,那么具体的指令会封装成一个个按钮。那么每一个按钮就相当于是对一条命令的封装。用控制条实现了用户发送指令与播放器内核接收指令的解耦。
首先创建播放器内核Player类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Player {

public void play() {
System.out.println("播放");
}

public void pause() {
System.out.println("暂停");
}

public void stop() {
System.out.println("停止");
}

public void speed() {
System.out.println("拖动进度条");
}

}

创建命令接口ICommand:

1
2
3
4
public interface ICommand {

void execute();
}

然后分别创建操作播放器可以接收的指令,播放指令PlayCommand类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PlayCommand implements ICommand {

private Player player;

public PlayCommand(Player player) {
this.player = player;
}

@Override
public void execute() {
player.play();
}
}

暂停指令PauseCommand类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PauseCommand implements ICommand {

private Player player;

public PauseCommand(Player player) {
this.player = player;
}

@Override
public void execute() {
player.pause();
}
}

停止指令StopCommand类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class PauseCommand implements ICommand {

private Player player;

public PauseCommand(Player player) {
this.player = player;
}

@Override
public void execute() {
player.pause();
}
}

拖动进度条指令SpeedCommand类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SpeedCommand implements ICommand {

private Player player;

public SpeedCommand(Player player) {
this.player = player;
}

@Override
public void execute() {
player.speed();
}
}
```
最后,创建控制条Controller类:

public class Controller {

private List<ICommand> commands = new ArrayList<>();

public void addCommand(ICommand command) {
    commands.add(command);
}

public void execute(ICommand command) {
    command.execute();
}

public void executes() {
    for(ICommand command : commands) {
        command.execute();
    }
    commands.clear();
}

}

1
测试main方法:

public static void main(String[] args) {
Player player = new Player();

Controller controller = new Controller();

controller.addCommand(new PlayCommand(player));
controller.addCommand(new PauseCommand(player));
controller.addCommand(new StopCommand(player));
controller.addCommand(new SpeedCommand(player));

controller.executes();

}

1
2
3
4
5
6
由于控制条已经与播放器内核解耦,以后想扩展新的命令,只需要增加命令即可,无需改动控制条结构。

# 二、命令模式在源码中的体现

## 2.1 Runnable接口
实际上Runnable接口就相当于是命令的抽象,只要是实现了Runnable接口的类都被认为是一个线程。

public interface Runnable {
/**
* When an object implementing interface Runnable is used
* to create a thread, starting the thread causes the object’s
* run method to be called in that separately executing
* thread.
*


* The general contract of the method run is that it may
* take any action whatsoever.
*
* @see java.lang.Thread#run()
*/
public abstract void run();
}

1
2
3
4
5
**实际上调用的线程的start方法后,就有资格去抢CPU的资源了,而不需要我们编写获得CPU资源的逻辑。而线程抢到CPU资源后,就会去执行run()方法中的内容,用Runnable接口把用户请求和CPU执行进行了解耦。**

## 2.2 junit.framework.Test接口

先来看接口源码:

package junit.framework;

public interface Test {
int countTestCases();

void run(TestResult var1);

}

1
上面Test接口中有两个方法,第一个countTestCases()方法用来统计当前需要执行的测试用例总数。第二个run()方法用来执行具体的测试逻辑,其参数TestResult用来返回测试结果的。实际上我们平时在编写测试用例的时候,只需要实现Test接口即便认为就是一个测试用例,那么在执行的时候就自动识别了。平时的通常做法就是继承TestCase类,来看下它的源码:

public TestResult run() {
TestResult result = this.createResult();
this.run(result);
return result;
}

public void run(TestResult result) {
result.run(this);
}


实际上TestCase类它也实现了Test接口。我们继承了TestCase类也相当于实现了Test接口,自然也会被扫描成一个测试用例。

# 三、命令模式的优缺点

优点:


* 通过引入中间件(抽象接囗),解耦了命令请求与实现;
* 扩展生良好,可以很容另地增加新命令; 
* 支持组合命令,支持命令队列;
* 可以在现有命令的基础上,增加额外功能(比如日志记录等,结合装饰器模式更酸爽)。

缺点:

* 具体命令类可能过多;
* 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构,解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接囗),**增加了理解上的困难**(不过这也是设计模式带来的一个通病,抽象必然会引入额外类型;抽象肯定比紧密难理解)。



架构师内功心法,非常熟知但并不知其所以然的迭代器模式详解

迭代器模式(Iterator Pattern)又称为游标模式(Cursor Pattern),它提供一种顺序访问的集合或容器对象元素的方法,而无需暴露集合内部表示。迭代器模式可以为不同的容器提供一种遍历行为,而不用关心容器内元素组成结构。迭代器模式的本质是抽离集合对象迭代行为到迭代器中,提供一致访问接口。

一、迭代器模式的应用场景

迭代器模式在我们生活中应用的得也比较广泛,比如物流系统中的传送带,不管传送的是什么物品,都被打包成一个一个的箱子并且有一个统一的二维码。这样我们不需要关心箱子里面是啥,我们在分发时只需要一个一个检发送的目的地即可。再比如,我们平时乘坐交通工具都是统一刷卡或者刷脸进站,而不需要关心是男生还是女性、是残疾人还是正常人等个性化的信息。



我们把多个对象聚在一起形成的总体称之为集合(Aggregate),集合对象是能够包容一组对象的容器对象。不同集合其内部元素的聚和结构可能不同,而迭代器模式屏蔽了内部元素获取细节,为外部提供一致的元素访问行为,解耦了元素迭代与集合对象间的耦合,并且提供不同的迭代器,可以为同一个对象提供不同顺序的元素访问行为,扩展了集合对元素迭代功能,符合开闭原则

迭代器模式使用于以下几个场景:

  • 访问一个集合对象的内容而无需暴露它的内部表示;
  • 为遍历不同的集合结构提供一个统一的访问接口。

迭代器模式主要包括四种角色:

  • 抽象迭代器(Iterator):负责定义访问和遍历元素的接口;
  • 具体迭代器(ConcreteIterator):提供具体的元素遍历行为;
  • 抽象容器(Aggregate):负责定义提供具体迭代器的接口;
  • 具体容器(ConcreteAggregate):创建具体迭代器。

1.1 自定义的迭代器

来以课程为例,创建一个课程的集合,集合中的每一个元素都是课程对象,然后自定义一个迭代器,将集合中的元素每一个课程对象信息读取出来。首先创建集合元素课程对象Course 类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Course {

private String name;

public Course(String name) {
this.name = name;
}

public String getName() {
return name;
}
}

然后创建自定义接口Iterator:

1
2
3
4
5
6
7
public interface Iterator<E> {

E next();

boolean hasNext();

}

接着创建课程集合CourseAggregate接口:

1
2
3
4
5
6
7
8
9
public interface CourseAggregate {

void add(Course course);

void remove(Course course);

Iterator<Course> iterator();

}

然后实现迭代器接口和课程结合接口:

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
public class IteratorImpl<E> implements Iterator<E> {

private List<E> list;

private int cursor;

private E element;

public IteratorImpl(List<E> list) {
this.list = list;
}

@Override
public E next() {
System.out.println("当前位置是:" + cursor);
element = list.get(cursor);
cursor ++ ;
return element;
}

@Override
public boolean hasNext() {
if(cursor > list.size() - 1) {
return false;
}
return true;
}
}

课程集合实现类CourseAggregateImpl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class CourseAggregateImpl implements CourseAggregate {

private List list;

public CourseAggregateImpl(List list) {
this.list = new ArrayList();
}

@Override
public void add(Course course) {
list.add(course);
}

@Override
public void remove(Course course) {
list.remove(course);
}

@Override
public Iterator<Course> iterator() {
return new IteratorImpl<Course>(list);
}
}

测试main方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {

Course chinese = new Course("语文");
Course math = new Course("数学");
Course english = new Course("英语");

CourseAggregate courseAggregate = new CourseAggregateImpl();
courseAggregate.add(chinese);
courseAggregate.add(math);
courseAggregate.add(english);

courseAggregate.remove(math);

Iterator<Course> courseIterator = courseAggregate.iterator();
while (courseIterator.hasNext()) {
System.out.println(courseIterator.next().getName());
}

}

二、迭代器模式在源码中的体现

2.1 JDK中的迭代器Iterator

Iterator接口源码如下:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public interface Iterator<E> {
/**
* Returns {@code true} if the iteration has more elements.
* (In other words, returns {@code true} if {@link #next} would
* return an element rather than throwing an exception.)
*
* @return {@code true} if the iteration has more elements
*/
boolean hasNext();

/**
* Returns the next element in the iteration.
*
* @return the next element in the iteration
* @throws NoSuchElementException if the iteration has no more elements
*/
E next();

/**
* Removes from the underlying collection the last element returned
* by this iterator (optional operation). This method can be called
* only once per call to {@link #next}. The behavior of an iterator
* is unspecified if the underlying collection is modified while the
* iteration is in progress in any way other than by calling this
* method.
*
* @implSpec
* The default implementation throws an instance of
* {@link UnsupportedOperationException} and performs no other action.
*
* @throws UnsupportedOperationException if the {@code remove}
* operation is not supported by this iterator
*
* @throws IllegalStateException if the {@code next} method has not
* yet been called, or the {@code remove} method has already
* been called after the last call to the {@code next}
* method
*/
default void remove() {
throw new UnsupportedOperationException("remove");
}

/**
* Performs the given action for each remaining element until all elements
* have been processed or the action throws an exception. Actions are
* performed in the order of iteration, if that order is specified.
* Exceptions thrown by the action are relayed to the caller.
*
* @implSpec
* <p>The default implementation behaves as if:
* <pre>{@code
* while (hasNext())
* action.accept(next());
* }</pre>
*
* @param action The action to be performed for each element
* @throws NullPointerException if the specified action is null
* @since 1.8
*/
default void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (hasNext())
action.accept(next());
}
}

从上面代码中,我们看到两个主要的方法定义hasNext()和next()方法,和我们自己写的完全一致。另外,从上面的代码中,我们看到remove()方法实现似曾相识。其实是在组合模式中我们见到过。迭代器模式和组合模式,两者似乎存在一定的相似性。组合模式解决的是统一树形结构各层次访问接囗,迭代器模式解决的是统一各集合对象元素遍历接囗。虽然他们的适配场景不同,但核心理念是相通的。

Iterator接口的实现类,在ArrayList类中的内部实现类Itr,它实现了Iterator接口:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;

public boolean hasNext() {
return cursor != size;
}

@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}

public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}

final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}

其中的next()和hasNext()方法的实现都很简单,ArrayList类中还有几个迭代器对Itr进行了进一步的扩展,来看下ListItr类的源码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}

public boolean hasPrevious() {
return cursor != 0;
}

public int nextIndex() {
return cursor;
}

public int previousIndex() {
return cursor - 1;
}

@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}

public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();

try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}

public void add(E e) {
checkForComodification();

try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}

三、迭代器模式的优缺点

优点:

  • 多态迭代:为不同的聚合结构提供一致的遍历接囗,即一个迭代接口可以访问不同的集合对象;
  • 简化集合对象接囗:迭代器模式将集合对象本身应该提供的元素迭代接囗抽取到了迭代器中使集合对象无须关心具体迭代行为;
  • 元素迭代功能多样化:每个集合对象都可以提供一个或多个不同的迭代器,使的同种元素聚合结构可以有不同的迭代行为;
  • 解耦迭代与集合:迭代器模式封装了具体的迭代算法,迭代算法的变化,不会影响到集合对象的架构。

缺点:

  • 对于比较简单的遍历(数组或者有序列表),使用迭代器方式遍历较为繁琐。

架构师内功心法,必须完全掌握吃透的踢皮球方式的责任链模式详解


title: 架构师内功心法,必须完全掌握吃透的踢皮球方式的责任链模式详解
date: 2020-03-16 17:13:47
tags:

在日常生活中责任链模式还是挺常见的,我们平时工作处理一些事务,往往都是各个部门协同合作完成某一个项目任务。而每个部门都有自己的职责,所以很多时候事情完成了一部分,便会交给下一个部门,直到所有的部门全部完成所有的工作之后,那么这个项目任务才算最终完成。还有平时说的过五关斩六将其实也是一种责任链的模式。


一、责任链模式的应用场景

责任链模式(Chain of Responsibility Pattern)是将链中的每一个节点看作是一个对象,每个节点处理的请求均不同,且内部自动维护下一个节点对象。当一个请求从链式的首端发出时,会沿着链的路径依次传递给每一个节点对象,直到有对象处理这个请求为止。责任模式主要是解耦请求与处理,客户只要将请求发送到对应的链上即可,无需关心请求的具体内容和处理细节,请求会自动进行传递直至有节点的对象进行处理。

责任链模式适用于以下几个场景:

  • 多个对象可以处理同一请求,但是具体由哪个对象处理则在运行时动态决定的;
  • 在不明确指定接收者的情况下,向多个对象中的一个提交一个请求;
  • 可动态指定一组对象处理请求。

责任链模式主要包含两种角色:

  • 抽象处理者(Handler):定义一个请求处理的方法,并维护下一个处理节点Handler对象的引用;
  • 具体处理者(ConcreteHandler):对请求进行处理,如果不感兴趣,则进行转发。

责任链模式的本质是解耦请求与处理,让请求在处理链中能进行传递与被处理;理解责任链模式应当理解的是其模式(道)而不是其具体实现(术),它的独到之处是其将节点处理者组成了链式结构,并允许节点自身决定是否进行请求处理或转发,相当于让请求流动起来。

1.1 数据校验拦截使用责任链模式

首先创建一个实体类 User 对象:

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
37
38
39
40
41
42
43
44
public class User {

private String username;
private String password;
private String roleName;

public User(String username, String password) {
this.username = username;
this.password = password;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public String getRoleName() {
return roleName;
}

public void setRoleName(String roleName) {
this.roleName = roleName;
}

@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", roleName='" + roleName + '\'' +
'}';
}
}

写一个简单的用户登录权限 UserService 类:

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
37
public class UserService {

private final String ROLE_NAME = "administrator";

public void login(String username, String password) {
if(StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
System.out.println("用户和密码校验成功了,可以继续往下执行了!");
return;
}
System.out.println("用户和密码不能为空,可以继续往下执行了!");

User user = check(username, password);
if(null == user) {
System.out.println("用户不存在!");
return;
}

System.out.println("恭喜,登录成功了!");

if(!ROLE_NAME.equals(user.getRoleName())) {
System.out.println("不是管理员,所以没有操作权限!");
}

System.out.println("允许操作!");
}

public User check(String username, String password) {
User user = new User(username, password);
user.setRoleName(ROLE_NAME);
return user;
}

public static void main(String[] args) {
UserService userService = new UserService();
userService.login("kevin", "123456");
}
}

上面的代码主要功能做了登录前的数据验证,判断逻辑是有先后顺序的。首先判断非空,然后判断用户名和密码,最后根据用户名密码获得用户角色。如果有角色的话就可以获得用户得操作权限。这样的代码在业务中实现显得非常的臃肿,我们来进行改造,可以使用责任链模式,将这些检查步骤串联起来,这样可以使得我们在编码的时候更加关注某个具体业务逻辑的实现处理。

首先创建一个抽象的 Handler 类:

1
2
3
4
5
6
7
8
9
10
11
public abstract class Handler {

protected Handler chain;

public void next(Handler handler) {
this.chain = handler;
}

public abstract void doHandle(User user);

}

然后分别创建校验ValidateHandler类,登录验证LoginHandler类,还有权限验证AuthHandler类:

ValidateHandler类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ValidateHandler extends Handler {

@Override
public void doHandle(User user) {
if(StringUtils.isEmpty(user.getUsername()) ||
StringUtils.isEmpty(user.getPassword())) {
System.out.println("用户和密码校验成功了,可以继续往下执行了!");
return;
}
System.out.println("用户和密码不能为空,可以继续往下执行了!");
chain.doHandle(user);
}
}

LoginHandler类:

1
2
3
4
5
6
7
8
public class LoginHandler extends Handler {
@Override
public void doHandle(User user) {
System.out.println("恭喜,登录成功了!");
user.setRoleName(ROLE_NAME);
chain.doHandle(user);
}
}

AuthHandler类:

1
2
3
4
5
6
7
8
9
10
public class AuthHandler extends Handler {
@Override
public void doHandle(User user) {
if(!ROLE_NAME.equals(user.getRoleName())) {
System.out.println("不是管理员,所以没有操作权限!");
}

System.out.println("允许操作!");
}
}

接下来改造 UserService 类,使得前面定义的几个Handler串联起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserService {

public void login(String username, String password) {
Handler validateHandler = new ValidateHandler();
Handler loginHandler = new LoginHandler();
Handler authHandler = new AuthHandler();

validateHandler.next(loginHandler);
loginHandler.next(authHandler);
validateHandler.doHandle(new User(username, password));
}


public static void main(String[] args) {
UserService userService = new UserService();
userService.login("kevin", "123456");
}
}

其实我们平时使用的很多验证框架的运用这样的一个原理,将各个维度的权限处理后解耦之后再进行串联,各自只负责各自相关的职责即可。如果职责与自己不相关则抛给链上的下一个Handler,俗称踢皮球

1.2 责任链模式和建造者模式结合使用

我们看到前面的代码在UserService类中,当链式结构比较长的话,那么其代码也是很臃肿的,如果在后面修改业务逻辑的话,都需要在UserService类中去进行修改,不符合开闭原则。产生这些问题的原因就是链式结构的组装过于复杂,而且对于结构的创建我们很容易就想到建造者模式,使用建造者模式,完成可以对UserService指定的处理节点对象进行自动链式组装,只需要指定处理节点对象,其它任何事情无需关心,并且可以指定处理对象节点的顺序不同,构造出来的链式结构也随之不同。来对Handler的代码进行改造:

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
public abstract class Handler<T> {

protected final String ROLE_NAME = "administrator";

protected Handler chain;

public void next(Handler handler) {
this.chain = handler;
}

public abstract void doHandle(User user);

public static class Builer<T> {
private Handler<T> head;
private Handler<T> tail;

public Builer<T> addHandler(Handler<T> handler) {
if(this.head == null) {
this.head = this.tail = handler;
return this;
}
this.tail.next(handler);
this.tail = handler;
return this;
}

public Handler<T> build() {
return this.head;
}

}

}

然后修改 UserService类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserService {

public void login(String username, String password) {
Handler validateHandler = new ValidateHandler();
Handler loginHandler = new LoginHandler();
Handler authHandler = new AuthHandler();

Handler.Builer builer = new Handler.Builer();
builer.addHandler(validateHandler)
.addHandler(loginHandler)
.addHandler(authHandler);

builer.build().doHandle(new User(username, password));
}


public static void main(String[] args) {
UserService userService = new UserService();
userService.login("kevin", "123456");
}
}

二、责任链模式在源码中的体现

2.1 Servlet中的Filter类

1
2
3
4
5
6
7
8
9
10
11
12
13
package javax.servlet;

import java.io.IOException;

public interface Filter {
default void init(FilterConfig filterConfig) throws ServletException {
}

void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException;

default void destroy() {
}
}

这个Filter接口定义很简单,相当于责任链模式中Handler抽象角色,那么它是如何形成一条责任链的呢?在doFilter()方法的最后一个参数我们已经看到了FilterChain类,来看下这个类的源码:

1
2
3
public interface FilterChain {
void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException;
}

FilterChain类中也是只定义了一个doFilter()方法,具体的逻辑是由使用者自己去进行实现的。我们来看一个Spring中实现的MockFilterChain类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
Assert.notNull(request, "Request must not be null");
Assert.notNull(response, "Response must not be null");
Assert.state(this.request == null, "This FilterChain has already been called!");
if (this.iterator == null) {
this.iterator = this.filters.iterator();
}

if (this.iterator.hasNext()) {
Filter nextFilter = (Filter)this.iterator.next();
nextFilter.doFilter(request, response, this);
}

this.request = request;
this.response = response;
}

它把链条中所有的Filter放到List中,然后在调用doFilter()方法时循环迭代List,也就是List中的Filter会顺序执行。

2.2 Netty中的Pipeline

Netty中的串行化处理Pipeline采用了责任链模式设计。底层采用双向列表的数据结构,将链式的处理器串联起来。客户端每一个请求过来,Netty认为Pipeline中所有的处理器都有机会处理它。对于入栈的请求全部从头节点开始往后传播,一直传播到尾部节点才会把消息释放掉。 负责处理器的接口ChannelHandler源码:

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
37
38
39
40
41
public interface ChannelHandler {

/**
* Gets called after the {@link ChannelHandler} was added to the actual context and it's ready to handle events.
*/
void handlerAdded(ChannelHandlerContext ctx) throws Exception;

/**
* Gets called after the {@link ChannelHandler} was removed from the actual context and it doesn't handle events
* anymore.
*/
void handlerRemoved(ChannelHandlerContext ctx) throws Exception;

/**
* Gets called if a {@link Throwable} was thrown.
*
* @deprecated is part of {@link ChannelInboundHandler}
*/
@Deprecated
void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception;

/**
* Indicates that the same instance of the annotated {@link ChannelHandler}
* can be added to one or more {@link ChannelPipeline}s multiple times
* without a race condition.
* <p>
* If this annotation is not specified, you have to create a new handler
* instance every time you add it to a pipeline because it has unshared
* state such as member variables.
* <p>
* This annotation is provided for documentation purpose, just like
* <a href="http://www.javaconcurrencyinpractice.com/annotations/doc/">the JCIP annotations</a>.
*/
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Sharable {
// no value
}
}

Netty对负责处理接口做了更细粒度的划分,处理器被分为两种,一种是入栈处理器 ChannelInboundHandler,另外一种是出栈处理器ChannelOutboundHandler,这两个接口都继承ChannelHandler接口。所有的处理都添加在Pipeline上。所以,添加删除责任处理器的接口行为都在ChannelPipeline中进行了规定

在默认的实现类中将所有的Handler都串成了一个链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DefaultChannelPipeline implements ChannelPipeline {

static final InternalLogger logger = InternalLoggerFactory.getInstance(DefaultChannelPipeline.class);

private static final String HEAD_NAME = generateName0(HeadContext.class);
private static final String TAIL_NAME = generateName0(TailContext.class);

private static final FastThreadLocal<Map<Class<?>, String>> nameCaches =
new FastThreadLocal<Map<Class<?>, String>>() {
@Override
protected Map<Class<?>, String> initialValue() throws Exception {
return new WeakHashMap<Class<?>, String>();
}
};

private static final AtomicReferenceFieldUpdater<DefaultChannelPipeline, MessageSizeEstimator.Handle> ESTIMATOR =
AtomicReferenceFieldUpdater.newUpdater(
DefaultChannelPipeline.class, MessageSizeEstimator.Handle.class, "estimatorHandle");
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
......

在Pipeline中的任意一个节点,只要我们不手动往下传播下去,这个事件就会终止传播在当前节点。对于入栈的数据,默认会传递到尾部节点进行回收。如果我们不进行下一步传播,事件就会终止在当前节点。对于出栈的数据把数据写回客户端也意味着事件的终止。

三、责任链模式的优缺点

优点:

  • 将请求与处理解耦;
  • 请求处理者(节点对象)只需要关注自己感兴趣的请求进行处理即可,对于不感兴趣的请求,直接转到下一级节点对象;
  • 具备链式传递处理请求功能,请求发送者无需知晓链路结构,只需等待请求处理结果;
  • 链路结构灵活,可以通过改变链路结构动态的新增或者删除责任;
  • 易于扩展新的请求处理类,符合开闭原则。

缺点:

  • 责任链太长或者处理时间太长,会影响整体性能;
  • 如果节点对象存在循环引用时,会造成死循环,导致系统崩溃。

-

架构师内功心法,连接两个空间维度的桥接模式详解

在现实生活中的桥接模式也随处可见,比如连接两个空间维度的桥,连接虚拟网络与真实网络的连接。

桥接模式(Bridge Pattern)也成为桥梁模式、接口模式或柄体(Handle And Body)模式,是将抽象部分与它的具体实现部分分离,使得它们都可以独立地变化。

一、桥接模式的应用场景

桥接模式主要目的是通过组合的方式建立两个类之间的联系,而不是继承。但又类似于多重继承方案,但是多重继承方案又违背了类的单一职责原则,其复用性比较差,桥接模式是比多重继承更好的替代方案。桥接模式的核心在于解耦抽象和实现。

1.1 桥接模式的角色

接下来我们看下桥接模式通用的UML图:

从UML图中可以看出桥接模式主要包含四种角色:

  • 抽象(Abstraciton):该类持有一个对实现角色的引用,抽象角色中的方法需要实现角色来实现。抽象角色一般就是抽象类(构造函数规定子类要传入一个实现对象);
  • 修正抽象(RefinedAbstraction):抽象的具体实现,对抽象类方法进行扩展和完善。
  • 实现(Implementor):确定实现维度的基本操作,提供给抽象类使用。该类一般为抽象类或接口。
  • 具体实现(ConcreteImplementor):实现类(Implementor)的具体实现。

下面来看具体实现的代码:

首先创建抽象 Abstraction 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Abstraction {

protected IImplementor iImplementor;

public Abstraction(IImplementor iImplementor) {
this.iImplementor = iImplementor;
}

void operation(){
this.iImplementor.operationImpl();
}

}

创建修正抽象 RefinedAbstraction 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class RefinedAbstraction extends Abstraction {

public RefinedAbstraction(IImplementor iImplementor) {
super(iImplementor);
}

@Override
void operation(){
super.operation();
System.out.println("refined operation");
}

}

创建角色实现 IImplementor 接口:

1
2
3
4
public interface IImplementor {

void operationImpl();
}

创建具体实现ConcreteImplementorA、ConcreteImplementorB 类:

1
2
3
4
5
6
public class ConcreteImplementorA implements IImplementor {
@Override
public void operationImpl() {
System.out.println("concreteImplementor A");
}
}
1
2
3
4
5
6
public class ConcreteImplementorB implements IImplementor {
@Override
public void operationImpl() {
System.out.println("concreteImplementor B");
}
}

测试main方法:

1
2
3
4
5
6
7
8
9
10
11
   public static void main(String[] args) {

IImplementor iImplementorA = new ConcreteImplementorA();
IImplementor iImplementorB = new ConcreteImplementorB();

Abstraction absA = new RefinedAbstraction(iImplementorA);
Abstraction absB = new RefinedAbstraction(iImplementorB);

absA.operation();
absB.operation();
}

桥接模式有以下几个应用场景:

  • 在抽象和具体实现之间需要增加更多灵活性的场景;
  • 一个类存在两个(或多个)独立变化的维度,而这两个(或多个)维度都需要独立进行扩展;
  • 不希望使用继承,或因为多层继承导致系统类的个数剧增。

1.2 桥接模式的业务实现案例

我们在平时办公的时候经常需要通过发送邮件消息、钉钉消息或者系统内消息和同事进行沟通。尤其是在使用一些流程审批的时候,我们需要记录这些过程以备查。可以根据消息的类型来进行划分,可以分为邮件消息,钉钉消息和系统内消息。但是,如果根据消息的紧急程度来划分的话,可以分为普通消息、紧急消息和特急消息。显然,整个消息系统可以划分为两个大维度。

如果使用继承的话情况就复杂了,而且不利于扩展。邮件信息可以是普通的,也可以是紧急的;钉钉消息可以是普通的,也可以是紧急的。下面使用桥接模式解决这类问题:

首先创建一个IMessage接口担任桥接的角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 实现消息发送的统一接口
*/
public interface IMessage {

/**
* 发送消息
* @param message
* 内容
* @param user
* 接收人
*/
public void send(String message, String user);
}

创建邮件消息类实现IMessage接口:

1
2
3
4
5
6
7
8
9
/**
* 邮件信息实现类
*/
public class EmailMessage implements IMessage {
@Override
public void send(String message, String user) {
System.out.println(String.format("使用邮件的方式发送消息 %s 给 %s", message, user));
}
}

创建钉钉消息类也实现IMessage接口:

1
2
3
4
5
6
7
8
9
/**
* 钉钉信息实现类
*/
public class DingMessage implements IMessage {
@Override
public void send(String message, String user) {
System.out.println(String.format("使用钉钉的方式发送消息 %s 给 %s", message, user));
}
}

然后在创建抽象角色 AbstractMessage 类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 抽象消息类
*/
public abstract class AbstractMessage {

//实现对象
IMessage message;

//构造方法传入实现部分的对象
public AbstractMessage(IMessage message) {
this.message = message;
}

/**
* 发送消息,委派给实现对象的方法
* @param message
* @param user
*/
public void sendMessage(String message, String user) {
this.message.send(message, user);
}
}

创建具体的普通消息实现 NormalMessage 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 普通消息类
*/
public class NormalMessage extends AbstractMessage {

//构造方法传入实现的对象
public NormalMessage(IMessage message) {
super(message);
}

/**
* 发送消息,直接调用父类的方法即可
* @param message
* @param user
*/
public void sendMessage(String message, String user) {
super.sendMessage(message, user);
}
}

创建具体的紧急消息实现 UrgencyMessage 类:

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 UngencyMessage extends AbstractMessage {
public UngencyMessage(IMessage message) {
super(message);
}

/**
* 发送消息,直接调用父类的方法即可
* @param message
* @param user
*/
public void sendMessage(String message, String user) {
super.sendMessage(message, user);
}

/**
* 扩展自己的功能,监控消息的状态
* @param messageId
* @return
*/
public Object watch(String messageId) {
return null;
}
}

测试main方法:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {

IMessage message = new EmailMessage();
AbstractMessage abstractMessage = new NormalMessage(message);
abstractMessage.sendMessage("周末加班申请", "张三");

message = new DingMessage();
abstractMessage = new UngencyMessage(message);
abstractMessage.sendMessage("请假申请", "李四");
}

运行结果:


在上面的案例中,我们采用了桥接模式解耦了“消息类型”和“消息紧急程度”这两个独立变化的维度。如果需要扩展这两个维度的内容,按照上述代码的方式进行扩展就好了。

二、桥接模式的源码体现

JDBC中的Driver类

我们都非常熟悉JDBC的API,其中有个Driver类就是桥接类。在使用的时候通过Class.forName() 方法可以动态的加载各个数据库厂商实现的Driver类。具体代码我们以mysql客户端为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Vector<Connection> pool;
private String url = "jdbc:mysql://localhost:3306/testDB";
private String username = "root";
private String password = "123456";
private String driverClassName = "com.mysql.jdbc.Driver";
private int poolSize = 100;
public ConnectionPool() {
pool = new Vector<Connection>(poolSize);
try{
Class.forName(driverClassName);
for (int i = 0; i < poolSize; i++) {
Connection conn = DriverManager.getConnection(url,username,password);
pool.add(conn);
}
}catch (Exception e){
e.printStackTrace();
}
}

首先来看一下Driver接口的定义:

Driver在JDBC中并没有做任何实现,具体的功能实现由各厂商完成,我们以Mysql为例:

1
2
3
4
5
6
7
8
9
10
public class Driver extends NonRegisteringDriver implements java.sql.Driver { 
public Driver() throws SQLExeption {}
static {
try {
DriverManager.registerDriver(new Driver()) ;
} catch (SQLE xception var1) {
throw new RuntimeExcept ion("Can't register driver!");
}
}
}

当我们执行Class.forName(“com.mysql.jdbc.Driver”)方法的时候,就会执行上面类的静态块中的代码。而静态块只是调用了DriverManager的registerDriver()方法,然后将Driver对象注册到DriverManager中。接下来看一下DriverManager中相关的源代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
/**
* Registers the given driver with the {@code DriverManager}.
* A newly-loaded driver class should call
* the method {@code registerDriver} to make itself
* known to the {@code DriverManager}. If the driver is currently
* registered, no action is taken.
*
* @param driver the new JDBC Driver that is to be registered with the
* {@code DriverManager}
* @exception SQLException if a database access error occurs
* @exception NullPointerException if {@code driver} is null
*/
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {

registerDriver(driver, null);
}

/**
* Registers the given driver with the {@code DriverManager}.
* A newly-loaded driver class should call
* the method {@code registerDriver} to make itself
* known to the {@code DriverManager}. If the driver is currently
* registered, no action is taken.
*
* @param driver the new JDBC Driver that is to be registered with the
* {@code DriverManager}
* @param da the {@code DriverAction} implementation to be used when
* {@code DriverManager#deregisterDriver} is called
* @exception SQLException if a database access error occurs
* @exception NullPointerException if {@code driver} is null
* @since 1.8
*/
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {

/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}

println("registerDriver: " + driver);

}

在注册之前,将传递过来的Driver对象封装成一个DriverInfo对象。接下来调用DriverManager中的getConnection() 方法获得连接对象,看下源代码:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
/**
* Attempts to establish a connection to the given database URL.
* The <code>DriverManager</code> attempts to select an appropriate driver from
* the set of registered JDBC drivers.
*<p>
* <B>Note:</B> If a property is specified as part of the {@code url} and
* is also specified in the {@code Properties} object, it is
* implementation-defined as to which value will take precedence.
* For maximum portability, an application should only specify a
* property once.
*
* @param url a database url of the form
* <code> jdbc:<em>subprotocol</em>:<em>subname</em></code>
* @param info a list of arbitrary string tag/value pairs as
* connection arguments; normally at least a "user" and
* "password" property should be included
* @return a Connection to the URL
* @exception SQLException if a database access error occurs or the url is
* {@code null}
* @throws SQLTimeoutException when the driver has determined that the
* timeout value specified by the {@code setLoginTimeout} method
* has been exceeded and has at least tried to cancel the
* current database connection attempt
*/
@CallerSensitive
public static Connection getConnection(String url,
java.util.Properties info) throws SQLException {

return (getConnection(url, info, Reflection.getCallerClass()));
}

/**
* Attempts to establish a connection to the given database URL.
* The <code>DriverManager</code> attempts to select an appropriate driver from
* the set of registered JDBC drivers.
*<p>
* <B>Note:</B> If the {@code user} or {@code password} property are
* also specified as part of the {@code url}, it is
* implementation-defined as to which value will take precedence.
* For maximum portability, an application should only specify a
* property once.
*
* @param url a database url of the form
* <code>jdbc:<em>subprotocol</em>:<em>subname</em></code>
* @param user the database user on whose behalf the connection is being
* made
* @param password the user's password
* @return a connection to the URL
* @exception SQLException if a database access error occurs or the url is
* {@code null}
* @throws SQLTimeoutException when the driver has determined that the
* timeout value specified by the {@code setLoginTimeout} method
* has been exceeded and has at least tried to cancel the
* current database connection attempt
*/
@CallerSensitive
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();

if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}

return (getConnection(url, info, Reflection.getCallerClass()));
}

/**
* Attempts to establish a connection to the given database URL.
* The <code>DriverManager</code> attempts to select an appropriate driver from
* the set of registered JDBC drivers.
*
* @param url a database url of the form
* <code> jdbc:<em>subprotocol</em>:<em>subname</em></code>
* @return a connection to the URL
* @exception SQLException if a database access error occurs or the url is
* {@code null}
* @throws SQLTimeoutException when the driver has determined that the
* timeout value specified by the {@code setLoginTimeout} method
* has been exceeded and has at least tried to cancel the
* current database connection attempt
*/
@CallerSensitive
public static Connection getConnection(String url)
throws SQLException {

java.util.Properties info = new java.util.Properties();
return (getConnection(url, info, Reflection.getCallerClass()));
}

private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}

if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}

println("DriverManager.getConnection(\"" + url + "\")");

// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;

for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}

} else {
println(" skipping: " + aDriver.getClass().getName());
}

}

// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}

println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}

在getConnection()中又会调用各自厂商实现的Driver的Connect()方法获得连接对象。这样巧妙的避开了使用继承,为不同的数据库提供了相同的接口。

三、桥接模式的优缺点

桥接模式很好的遵循了里氏替换原则和依赖倒置原则,最终实现了开闭原则,对修改关闭,对扩展开发。优缺点总结如下:

优点:

  • 分离抽象部分及其具体实现部分;
  • 提高系统的扩展性;
  • 符合开闭原则;
  • 符合合成复原则。

缺点:

  • 增加系统的设计和理解难度;
  • 需要正确识别系统中两个独立变化的维度。

架构师内功心法,作为树形结构系统架构的组合模式详解


title: 架构师内功心法,作为树形结构系统架构的组合模式详解
date: 2020-03-10 14:08:50
tags:

在古代皇帝要想管理整个国家,他是不可能直接管理到具体每一个老百姓的,因此皇帝设置了很多机构,比如三省六部,这些机构下面又有很多小的组织,它们共同管理着整个国家。再比如有一个很大的公司,下面有很多部门,每一个部门下面又有很多个部门,这些就是组合模式的体现。比如树形菜单,操作系统目录结构等,所以组合模式在我们生活中也是非常常见的。


一、组合模式的应用场景

组合模式(Composite Pattern)也称为整体-部分(Part-Whole)模式,它的宗旨是通过将单个对象(叶子节点)和组合对象(树枝节点)用相同的接口进行表示,使得客户对单个对象和组合对象使用具有一致性。

1.1 组合关系与聚和关系的区别

组合关系:在古代皇宫的三宫六院,贵妃很多,但是每一个贵妃都属于一个皇帝(具有相同的生命周期);

聚和关系:一个老师有很多学生,但是每一个学生也属于多个老师(具有不同的生命周期)。

组合模式一般用来描述整体与部分的关系,它将对象组织到树形结构中,最顶层的节点成为根节点,根节点下面包括树枝节点和叶子节点,树枝节点下面又包括树枝节点和叶子节点。


由上图可以看出,其实根节点和树枝节点本质上是同一种数据类型,可以作为容器使用;而叶子节点与树枝节点在语义上不属于同一种类型,但是在组合模式中,会把树枝节点和叶子节点认为是同一种数据类型(用同一接口定义),让它们具备一致行为。这样,在组合模式中,整个树形结构中的对象都是同一种类型,带来的一个好处就是客户无需辨别树枝节点还是叶子节点,而是可以直接进行操作,给客户使用带来极大的便利。

1.2 组合模式的角色

组合模式包含 3 个角色:

  • 抽象根节点(Component):定义系统各层次对象的共有方法和属性,可以预先定义一些默认行为和属性;
  • 树枝节点(Composite):定义树枝节点的行为,存储子节点,组合树枝节点和叶子节点形成一个树形结构;
  • 叶子节点(Leaf):叶子节点对象,其下再无分支,是系统层次遍历的最小单位。

组合模式在代码具体实现上,有两种不同的方式,分别是透明组合模式和安全组合模式。

1.3 组合模式应用场景

当子系统与其内各个对象层次呈现树形结构时,可以使用组合模式让子系统内各个对象层次的行为操作具备一致性。客户端使用该子系统内任意一个层次对象时,无须进行区分,直接使用通用操作即可,为客户端的使用带来了便捷。

组合模式有以下两个应用场景:

  • 希望客户端可以忽略组合对象与单个对象的差异时;
  • 对象层次具备整体和部分,呈树形结构。

1.4 透明组合模式的写法

透明组合模式是把所有的公共方法都定义在Component中,这样做的好处是客户度无需分辨树枝节点(Composite)和叶子节点(Leaf),它们具备完全一致的接口。来看一下UML类图吧:

下面来看一个实际的案例吧。以某IT教育培训的课程为例,来设计一个课程的关系结构。有嵌入式编程、Web前端、Java编程、云计算、Hadoop、Flink等课程。而云计算、Hadoop、Flink都属于大数据系列课程,每个课程的定价也是不一样的。但是这些课程不管怎么组合,都有一些共性,而且都是整体和部分的关系,可以用组合模式来设计。先创建一个顶层的抽象类 CourseComponent:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public abstract class CourseComponent {

public void addChild(CourseComponent catalogComponent) {
throw new UnsupportedOperationException("不支持添加操作!");
}

public void removeChild(CourseComponent catalogComponent) {
throw new UnsupportedOperationException("不支持删除操作!");
}

public String getName(CourseComponent catalogComponent) {
throw new UnsupportedOperationException("不支持获取名称操作!");
}

public double getPrice(CourseComponent catalogComponent) {
throw new UnsupportedOperationException("不支持获取价格操作!");
}

public void print() {
throw new UnsupportedOperationException("不支持打印操作!");
}
}

把所有可能用到的方法都定义到这个最顶层的抽象类中,但是不写任何逻辑处理的代码,而是直接抛异常。这里,有些小伙伴会有疑惑,为什么不用抽象方法?因为,用了抽象方法,其子类就必须实现,这样便体现不出各子类的细微差异。 因此,子类继承此抽象类后,只需要重写有差异的方法覆盖父类的方法即可。接下来分别创建课程类Course 和课程包 CoursePackage类。先创建Course类:

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 Course extends CourseComponent {

private String name;

private double price;

public Course(String name, double price) {
this.name = name;
this.price = price;
}

@Override
public String getName(CourseComponent catalogComponent) {
return this.name;
}

@Override
public double getPrice(CourseComponent catalogComponent) {
return this.price;
}

@Override
public void print() {
System.out.println(name + " (¥" + price + "元)");
}

}

再创建CoursePackage类:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
public class CoursePackage extends CourseComponent {

private List<CourseComponent> items = new ArrayList<>();
private String name;
private Integer level;

public CoursePackage(String name, Integer level) {
this.name = name;
this.level = level;
}

@Override
public void addChild(CourseComponent catalogComponent) {
items.add(catalogComponent);
}

@Override
public void removeChild(CourseComponent catalogComponent) {
items.remove(catalogComponent);
}

@Override
public String getName(CourseComponent catalogComponent) {
return this.name;
}

@Override
public void print() {
System.out.println(name);
for(CourseComponent courseComponent : items) {
//控制显示格式
if(this.level != null){
for(int i = 0; i < this.level; i ++){
//打印空格控制格式
System.out.print(" ");
}
for(int i = 0; i < this.level; i ++){
//每一行开始打印一个+号
if(i == 0){
System.out.print("+");
}
System.out.print("-");
}
//打印标题
courseComponent.print();
}
}
}
}

测试main方法:

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 static void main(String[] args) {
System.out.println("=============透明组合模式=============");

CourseComponent course = new Course("嵌入式编程", 3000.00);
CourseComponent webFront = new Course("Web前端", 10000.00);
CourseComponent java = new Course("Java编程", 8000.00);

CoursePackage bigData = new CoursePackage("大数据", 2);

CourseComponent cloudCalc = new Course("云计算", 3000.00);
CourseComponent hadoop = new Course("hadoop", 3500.00);
CourseComponent flink = new Course("flink", 4000.00);
bigData.addChild(cloudCalc);
bigData.addChild(hadoop);
bigData.addChild(flink);

CourseComponent catalog = new CoursePackage("课程主目录",1);
catalog.addChild(course);
catalog.addChild(webFront);
catalog.addChild(java);
catalog.addChild(bigData);

catalog.print();

}

运行结果:

透明组合模式把所有公共方法都定义在Component中,这样做的好处是客户端无需分辨是叶子节点(Leaf)和树枝节点(Composite),它们具备完全一致的接口;缺点是叶子节点(Leaf)会继承得到一些它所不需要(管理子类操作的方法)的方法,这与设计模式 接口隔离原则相违背。

为了让大家更加透彻理解,下面我们来看安全组合模式的写法。

1.5 安全组合模式的写法

安全组合模式是只规定系统各个层次的最基础的一致行为,而把组合(树节点)本身的方法(管理子类对象的添加,删除等)放到自身当中。其 UML 类图如下所示:

再举一个大家都熟悉的例子吧。电脑里面的文件系统其实就是一个典型的树形结构,目录包含文件夹和文件,文件夹里面又包含文件夹和文件……接下来就用代码来实现这个目录系统。
文件系统有两个大的层次:文件夹,文件。其中,文件夹能容纳其他层次,为树枝节点;文件为最小单位,为叶子节点。由于目录系统层次较少,且树枝节点(文件夹)结构相对稳定,而文件其实可以有很多类型,所以这里我们选择使用 安全组合模式 来实现目录系统,可以避免为叶子类型(文件)引入冗余方法。先创建最顶层的抽象组件 Directory 类:

1
2
3
4
5
6
7
8
9
10
public abstract class Directory {

protected String name;

public Directory(String name) {
this.name = name;
}

public abstract void show();
}

然后分别创建Folder和File类:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Folder extends Directory {

private List<Directory> dirs;

private Integer level;

public Folder(String name, Integer level) {
super(name);
this.level = level;
this.dirs = new ArrayList<>();
}

public boolean add(Directory dir) {
return dirs.add(dir);
}

public boolean remove(Directory dir) {
return dirs.remove(dir);
}

public Object get(int index) {
return dirs.get(index);
}

public void list() {
for(Directory dir : dirs) {
System.out.println(dir.name);
}
}

@Override
public void show() {
System.out.println(this.name);
for (Directory dir : this.dirs) {
//控制显示格式
if (this.level != null) {
for (int i = 0; i < this.level; i++) {
//打印空格控制格式
System.out.print(" ");
}
for (int i = 0; i < this.level; i++) {
//每一行开始打印一个+号
if (i == 0) {
System.out.print("+");
}
System.out.print("-");
}
}
//打印名称
dir.show();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
public class File extends Directory {

public File(String name) {
super(name);
}

@Override
public void show() {
System.out.println(this.name);
}
}

测试main方法:

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 static void main(String[] args) {
System.out.println("==========安全组合模式=============");
File chrome = new File("谷歌浏览器");
File safe360 = new File("360浏览器");
File edge = new File("微软Edge浏览器");

Folder browser = new Folder("浏览器", 2);
browser.add(chrome);
browser.add(safe360);
browser.add(edge);

File wx = new File("wechat.exe");
File qq = new File("QQ.exe");
File dd = new File("DingDing.exe");

Folder root = new Folder("root", 1);
root.add(wx);
root.add(qq);
root.add(dd);
root.add(browser);

System.out.println("----------show()方法效果-----------");
root.show();
System.out.println("----------list()方法效果-----------");
root.list();
}

运行结果:


安全组合模式的好处是接口定义职责清晰,符合设计模式单一职责原则和接口隔离原则;缺点是客户需要区分树枝节点(Composite)和叶子节点(Leaf),这样才能正确处理各个层次的操作,客户端无法依赖抽象(Component),违背了设计模式依赖倒置原则。

二、组合模式在源码中的体现

2.1 HashMap中的putAll()方法

组合模式在源码中应用也是非常广泛的。首先我们来看一个非常熟悉的HashMap,他里面有一个putAll()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}

final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
}
else if (s > threshold)
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}

putAll()方法传入的是Map对象,Map就是一个抽象构件(同时这个构件中只支持键值对的存储方式),而HashMap是一个中间构件,HashMap中间的Node节点是叶子节点。说到中间构件就会有规定的存储方式。HashMap中的存储方式是一个静态内部类的数组Node<K,V>,源码如下:

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
37
38
39
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;

Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}

public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }

public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}

public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}

public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}

2.2 ArrayList中的addAll()方法

常用的 ArrayList 对象也有 addAll()方法,其参数也是 ArrayList 的父类 Collection,来看源代码:

1
2
3
4
5
6
7
8
 public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}

组合对象和被组合对象都应该有统一的接口实现或者统一的抽象父类。

三、组合模式的优缺点

优点:

  • 清楚地定义分层次的复杂对象,表示对象的全部或部分层次;
  • 让客户端忽略了层次的差异,方便对整个层次结构进行控制;
  • 简化客户端代码;
  • 符合开闭原则。

缺点:

  • 限制类型时会较为复杂;
  • 使设计变得更加抽象。

-

架构师内功心法,使用共享对象来提高性能的享元模式详解


title: 架构师内功心法,使用共享对象来提高性能的享元模式详解
date: 2020-03-08 16:00:12
tags:

一、享元模式的应用场景

在我们的日常生活中享元模式很常见,比如各种中介机构的房源共享,再比如全国社保联网。面向对象技术很好的解决一些灵活性或者扩展性问题,但是很多情况下需要在系统内增加对象的个数。当对象太多时,将导致运行代价过高,带来性能下降等问题。享元模式正式为解决这一类问题而诞生的。


享元模式(FlyWeight Pattern)又称为轻量级模式,是对象池的一种实现。类似线程池,线程池可以避免不停的创建和销毁对象,消耗性能。提供了减少对象数量从而改善应用所需的对象结构方式。其宗旨是共享细粒度对象,将多个多同一对象的访问集中起来,不必为每个访问者创建一个单独的对象,以此来降低内存的消耗。

享元模式把一个对象的状态分为内部状态和外部状态,内部状态是不变的,外部状态是变化的。 然后通过共享不变的部分,达到减少对象数量并节约内存的目的。享元模式的本质是缓存共享对象,降低内存消耗。

1.1 享元模式的角色

享元模式有三个参与角色:

  • 抽象享元角色(Flyweight):享元对象抽象基类或者接口,同时定义出对象的外部状态和内部状态的接口或实现;
  • 具体享元角色(ConcreteFlyweight):实现抽象角色定义的业务。该角色的内部状态处理应该与环境无关,不能出现会有一个操作改变内部状态,同时修改了外部状态;
  • 享元工厂(FlyweightFactory):负责管理享元对象池和创建享元对象。

享元模式通用的UML图

享元模式有以下两个应用场景:

  • 常常应用于系统底层的开发,以便解决系统的性能问题;
  • 系统有大量的相似对象、需要缓冲池的场景。

1.2 刷火车票软件的享元模式应用

我们每年春节为了抢到一张回家的火车票都要大费周折,进而出现了很多刷票
软件,刷票软件会将我们填写的信息缓存起来,然后定时检查余票信息。抢票的时候,我们肯定是要查询下有没有我们需要的票信息,这里我们假设一张火车的信息包含:出发站,目的站,价格,座位类别。现在要求编写一个查询火车票查询伪代码,可以通过出发站,目的站查到相关票的信息。

比如要求通过出发站,目的站查询火车票的相关信息,只需要构建火车票对象,然后提供一个查询出发站、目的站的接口给客户进行查询即可,首先创建一个ITicket接口:

1
2
3
4
public interface ITicket {

void showInfo(String bunk);
}

然后创建TrainTicket 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class TrainTicket implements ITicket {

private String from;

private String to;

private double price;

public TrainTicket(String from, String to) {
this.from = from;
this.to = to;
}

@Override
public void showInfo(String bunk) {
this.price = new Random().nextDouble();
System.out.println(String.format("%s->%s:%s 价格:%s 元", this.from, this.to, bunk, this.price));
}

}

最后创建TicketFactory 类:

1
2
3
4
5
6
7
public class TicketFacotry {

public static ITicket queryTicket(String from, String to) {
return new TrainTicket(from, to);
}

}

测试main方法:

1
2
3
4
public static void main(String[] args) {
ITicket ticket = TicketFacotry.queryTicket("北京西", "武汉");
ticket.showInfo("二等座");
}

分析上面的代码,我们发现客户端进行查询时,系统通过TicketFactory直接创建一个火车票对象,但是这样做的话,当某个瞬间如果有大量的用户请求同一张票的信息时,系统就会创建出大量该火车票对象,系统内存压力骤增。而其实更好的做法应该是缓存该票对象,然后复用提供给其他查询请求,这样一个对象就足以支撑数以千计的查询请求,对内存完全无压力,使用享元模式可以很好地解决这个问题。我们继续优化代码,只需在 TicketFactory 类中进行更改,增加缓存机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class TicketFacotry {

private static Map<String, ITicket> ticketTool = new ConcurrentHashMap();

public static ITicket queryTicket(String from, String to) {
String key = from + "->" + to;
if(ticketTool.containsKey(key)) {
System.out.println("使用缓存" + key);
return ticketTool.get(key);
}
System.out.println("首次查询,创建对象: " + key);
ITicket ticket = new TrainTicket(from, to);
ticketTool.put(key, ticket);
return ticket;
}
}

修改main测试方法:

1
2
3
4
5
6
public static void main(String[] args) {
ITicket ticket = TicketFacotry.queryTicket("北京西", "武汉");
ticket.showInfo("二等座");
ITicket ticket1 = TicketFacotry.queryTicket("北京西", "武汉");
ticket1.showInfo("一等座");
}

运行效果:


可以看到,除了第一次查询创建对象后,后续查询相同车次票信息都是使用缓存对象,无需创建新对象了。来看一下类结构图:

其中 ITicket 就是抽象享元角色,TrainTicket 就是具体享元角色,TicketFactory 就是享元工厂。有些小伙伴一定会有疑惑了,这不就是注册式单例模式吗?对,这就是注册式单例模式。虽然,结构上很像,但是享元模式的重点在结构上,而不是在创建对象上。

1.3 数据库连接池Connection对象

我们经常使用的数据库连接池,因为我们使用Connection对象时主要性能消耗在建立连接和关闭连接的时候,为了提高 Connection 在调用时的性能,我们和将 Connection 对象在调用前创建好缓存起来,用的时候从缓存中取值,用完再放回去,达到资源重复利用的目的。来看下面的代码:

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
public class ConnectionPool {

private Vector<Connection> pool;
private String url = "jdbc:mysql://localhost:3306/testDB";
private String username = "root";
private String password = "123456";
private String driverClassName = "com.mysql.jdbc.Driver";
private int poolSize = 100;
public ConnectionPool() {
pool = new Vector<Connection>(poolSize);
try{
Class.forName(driverClassName);
for (int i = 0; i < poolSize; i++) {
Connection conn = DriverManager.getConnection(url,username,password);
pool.add(conn);
}
}catch (Exception e){
e.printStackTrace();
}
}
public synchronized Connection getConnection(){
if(pool.size() > 0){
Connection conn = pool.get(0);
pool.remove(conn);
return conn;
}
return null;
}
public synchronized void release(Connection conn){
pool.add(conn);
}

}

这样的连接池,普遍应用于开源框架,有效提升底层的运行性能。

二、享元模式在源码中的体现

2.1 String中的享元模式

Java 中将 String 类定义为final(不可改变的),JVM中字符串一般保存在字符串常量池中,java会确保一个字符串在常量池中只有一个拷贝,这个字符串常量池在 JDK6.0 以前是位于常量池中,位于永久代,而在JDK7.0中,JVM将其从永久代拿出来放置于堆中。

关于String 的测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StringTest {

public static void main(String[] args) {

String s1 = "hello";
String s2 = "hello";
String s3 = "hell" + "o";
String s4 = "hello" + new String("o");
String s5 = new String("hello");
String s6 = s5.intern();
String s7 = "hell";
String s8 = "o";
String s9 = s7 + s8;
System.out.println(s1 == s2);//true
System.out.println(s1 == s3);//ture
System.out.println(s1 == s4);//false
System.out.println(s4 == s5);//false
System.out.println(s1 == s6);//true
System.out.println(s1 == s9);//false
}
}

String类是用final修饰的,以字面量的形式创建String变量时,JVM会在编译的时候把该字面的“hello”放到字符串常量池中,Java程序启动的时候就已经加载到内存里面了。这个字符串常量池的特点就是有且仅有一份相同的字面量,如果有其它相同的字面量,JVM则返回这个字面量的引用,如果没有相同的字面量,则在字符串常量池中创建字面量并返回它的引用。

  • s2 指向的字面量”hello”在常量池中已经存在了(s1 先于 s2),于是 JVM 就返回这个字面量绑定的引用,所以 s1==s2。
  • s3 中字面量的拼接其实就是”hello”,JVM 在编译期间就已经对它进行优化,所以 s1 和 s3 也是相等的。
  • s4 中的 new String(“o”)生成了两个对象,lo,new String(“o”),o 存在字符串常量池,new String(“o”)存在堆中,String s4 = “hell” + new String(“o”)实质上是两个对象的相加,编译器不会进行优化,相加的结果存在堆中,而 s1 存在字符串常量池中,当然不相等。s1==s9 的原理一样。
  • s4==s5 两个相加的结果都在堆中,不用说,肯定不相等。
  • s1==s6 中,s5.intern()方法能使一个位于堆中的字符串在运行期间动态地加入到字符串常量池中(字符串常量池的内容是程序启动的时候就已经加载好了),如果字符串常量池中有该对象对应的字面量,则返回该字面量在字符串常量池中的引用,否则,创建复制一份该字面量到字符串常量池并返回它的引用。因此 s1==s6 输出 true。

2.2 Integer的享元模式

再举例一个大家都非常熟悉的对象Integer,也用到了享元模式,其中暗藏玄机,我们来看个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IntegerTest {

public static void main(String[] args) {

Integer a = 100;
Integer b = Integer.valueOf(100);

Integer c = 1000;
Integer d = Integer.valueOf(1000);

System.out.println(a == b);
System.out.println(c == d);
}

}

我们跑完程序之后才发现总有些不对,得到了一个意向不到的结果,其运行结果如下:

之所以得到这样的结果,是因为 Integer 用到的享元模式,我们来看 Integer 的源码:

1
2
3
4
5
 public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

我们发现 Integer 源码中的 valueOf()方法做了一个条件判断,如果目标值在-128 到 127 之间,则直接从缓存中取值,否则新建对象。那JDK为何要这样做呢?因为在-128 到 127 之间的数据在int范围内是使用最频繁的,为了节省频繁创建对象带来的内存消耗,这里就用到了享元模式,来提高性能。

2.3 Long的享元模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public final class Long extends Number implements Comparable<Long> {
public static Long valueOf(long var0) {
return var0 >= -128L && var0 <= 127L ? Long.LongCache.cache[(int)var0 + 128] : new Long(var0);
}
private static class LongCache {
private LongCache(){}
static final Long cache[] = new Long[-(-128) + 127 + 1];
static {
for(int i = 0; i < cache.length; i++)
cache[i] = new Long(i - 128);
}
}
//...
}

同理,Long 中也有缓存,不过不能指定缓存最大值。

2.4 Apache Commons Pool2 中的享元模式

对象池化的基本思路:将用过的对象保存起来,等下次需要再次使用这种对象的时候,再拿出来重复使用,从一定程度上减少频繁创建对象所造成的开销。用于充当保存对象的“容器”的对象,被称之为对象池。(Object Pool,或简称Pool)。

  • Apache Commons Pool

实现了对象池的功能。定义了对象的生成、销毁、激活、钝化等操作及其状态转换,并提供几个默认的对象池实现。有几个重要的对象:

  • PooledObject(池对象)

用于封装对象(如:线程、数据库连接、TCP连接),将其包裹成可被池管理的对象。

  • PooledObjectFactory(池对象工厂):
    定义了操作 PooledObject实例生命周期的一些方法,PooledObjectFactory必须实现线程安全。

  • ObjectPool (对象池)

ObjectPool 负责管理 PooledObject,如:借出对象,返回对象,校验对象,有多少激活对象,有多少空闲对象。

1
private final Map<IdentityWrapper<T>, PooledObject<T>> allObjects;

这里我们就不分析其具体源码了。

三、享元模式的内部状态和外部状态

享元模式的定义为我们提出了两个要求:细粒度和共享对象。因为要求细粒度对象,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。

内部状态指对象共享出来的信息,存储在享元对象内部并且不会随环境的改变而改变;外部状态指对象得以依赖的一个标记,是随环境改变而改变的、不可共享的状态

比如,连接池中的连接对象,保存在连接对象中的用户名、密码、连接 url 等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态。而每个连接要回收利用时,我们需要给它标记为可用状态,这些为外部状态。

四、享元模式的优缺点

优点:

  • 减少对象的创建,降低内存中对象的数量,降低系统的内存,提高效率;
  • 减少内存之外的其他资源占用。

缺点:

  • 关注内、外部状态、关注线程安全问题;
  • 使系统、程序的逻辑复杂化。

-

架构师内功心法,外界访问系统内部唯一通道的门面模式详解

一、门面模式的应用场景

门面模式(Facade Pattern)又叫外观模式,提供了一个统一的接口,用来访问子系统中的一群接口。主要特征是定义了一个高层接口,让子系统更容易使用。在我们的日常工作中,都在有意无意大量使用门面模式,但凡只要高层模块需要调度多个子系统,我们都会封装一个新类,提供精简接口,让高层模块很容易的间接调用这些子系统的功能。尤其是现阶段各种第三方API,各种开源类库,很大概率都会使用门面模式。

门面模式适用于以下几种场景:

  • 子系统越来越复杂,增加门面模式提供简单接口;
  • 构建多层系统结构,利用门面对象作为每层的入口,简化层间调用。

门面模式主要有2种角色:

  • 外观角色(Facade):也成为门面角色,系统对外的统一接口;
  • 子系统角色(SubSystem):可以有一个或者多个子系统角色。

1.1 门面模式的通用写法

下面是门面模式的通用代码,首先分别创建 3 个子系统的业务逻辑 SubSystemA、SubSystemB、SubSystemC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SubSystemA {
public void doA() {
System.out.println("doing A stuff");
}
}

public class SubSystemB {
public void doB() {
System.out.println("doing B stuff");
}
}

public class SubSystemC {
public void doC() {
System.out.println("doing C stuff");
}
}

然后,创建外观角色 Facade 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Facade {
private SubSystemA a = new SubSystemA();
private SubSystemB b = new SubSystemB();
private SubSystemC c = new SubSystemC();
// 对外接口
public void doA() {
this.a.doA();
}
// 对外接口
public void doB() {
this.b.doB();
}
// 对外接口
public void doC() {
this.c.doC();
}
}

测试main方法:

1
2
3
4
5
6
 public static void main(String[] args) {
Facade facade = new Facade();
facade.doA();
facade.doB();
facade.doC();
}

二、门面模式在源码中的体现

2.1 Spring JDBC中的JdbcUtils

JdbcUtils封装了和JDBC 相关的所有操作,它一个代码片段:

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
37
38
39
40
public abstract class JdbcUtils {
public static final int TYPE_UNKNOWN = -2147483648;
private static final Log logger = LogFactory.getLog(JdbcUtils.class);
public JdbcUtils() {
}
public static void closeConnection(Connection con) {
if(con != null) {
try {
con.close();
} catch (SQLException var2) {
logger.debug("Could not close JDBC Connection", var2);
} catch (Throwable var3) {
logger.debug("Unexpected exception on closing JDBC Connection", var3);
}
}
}
public static void closeStatement(Statement stmt) {
if(stmt != null) {
try {
stmt.close();
} catch (SQLException var2) {
logger.trace("Could not close JDBC Statement", var2);
} catch (Throwable var3) {
logger.trace("Unexpected exception on closing JDBC Statement", var3);
}
}
}
public static void closeResultSet(ResultSet rs) {
if(rs != null) {
try {
rs.close();
} catch (SQLException var2) {
logger.trace("Could not close JDBC ResultSet", var2);
} catch (Throwable var3) {
logger.trace("Unexpected exception on closing JDBC ResultSet", var3);
}
}
}
...
}

2.2 Tomcat 的源码中的RequestFacade 类

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
public class RequestFacade implements HttpServletRequest {
...
@Override
public String getContentType() {
if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}
return request.getContentType();
}
@Override
public ServletInputStream getInputStream() throws IOException {
if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}
return request.getInputStream();
}
@Override
public String getParameter(String name) {
if (request == null) {
throw new IllegalStateException(
sm.getString("requestFacade.nullRequest"));
}
if (Globals.IS_SECURITY_ENABLED){
return AccessController.doPrivileged(
new GetParameterPrivilegedAction(name));
} else {
return request.getParameter(name);
}
}
...
}

我们看名字就知道它用了门面模式。它封装了非常多的request的操作,也整合了很多 servlet-api 以外的一些内容,给用户使用提供了很大便捷。同样,Tomcat 对 Response 和 Session 当也封装了ResponseFacadeStandardSessionFacade 类,感兴趣的小伙伴可以去深入了解一下。

三、门面模式的优缺点

优点:

  • 简化了调用过程,无需深入了解子系统,以防给子系统带来风险;
  • 减少系统依赖、松散耦合;
  • 更好地划分访问层次,提高了安全性;
  • 遵循迪米特法则,即最少知道原则。

缺点:

  • 当增加子系统和扩展子系统行为时,可能容易带来未知风险;
  • 不符合开闭原则;
  • 某些情况下可能违背单一职责原则。

架构师内功心法,注重方法调用顺序的建造者模式详解

一、建造者模式的定义

大家平时都去过肯德基用餐,那里不变的是炸鸡、汉堡、薯条、可乐等,这些都是一直都有的,不变的,而其它组合是经常变化的,从而生成不同的“套餐”罢了。而建造模式(Builder Pattern)是将一个复杂的对象的构建过程与它的表示分离,使得同样的构建过程构建不同的表示。使用建造者模式对于用户而言只需要关注指定需要建造的类型就可以获得对象,而不需要了解建造的过程以及细节。

建造者模式适用于创建对象需要很多步骤,但是步骤的顺序不是固定不变的。先看一下建造者模式的类图:

建造者模式中的四个重要角色:

  • 产品(Product):要创建的产品类对象
  • 抽象建造者(Builder):规范产品对象的各个组成部分的建造
  • 建造者(Concrete Builder):具体化对象的各个组成部分的创建
  • 调用者(Director):负责保证对象各部分完整创建或按某种顺序创建

二、建造者模式的应用场景

建造者模式适用于一个具有较多的零件的复杂产品的创建过程,由于需求的变化,组成这个复杂产品的各个零件经常猛烈变化,但是它们的组合方式却相对稳定。

建造者模式适用于以下几种场景:

  • 相同的方法,不同的执行顺序,产生的结果也不同
  • 多个部件或零件,装配到一个对象中,产生的结果不同
  • 产品类复杂,或者产品类中调用顺序不同产生不同的作用
  • 初始化对象特别复杂,参数多,而且很多参数都有默认值

2.1 建造者模式的基本写法

我们以公司的技术培训为例,一个完整的技术培训需要由发布培训通知、制作培训PPT、组织员工培训、现场(远程)培训、提交培训问卷等步骤。下面我们用建造模式的代码来简单实现这类场景,首先创建一个技术培训的 TechnicalTraining 产品类 :

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class TechnicalTraining {

private String notice;
private String ppt;
private String training;
private String questionnaire;

public String getNotice() {
return notice;
}

public void setNotice(String notice) {
this.notice = notice;
}

public String getPpt() {
return ppt;
}

public void setPpt(String ppt) {
this.ppt = ppt;
}

public String getTraining() {
return training;
}

public void setTraining(String training) {
this.training = training;
}

public String getQuestionnaire() {
return questionnaire;
}

public void setQuestionnaire(String questionnaire) {
this.questionnaire = questionnaire;
}

@Override
public String toString() {
return "TechnicalTraining{" +
"notice='" + notice + '\'' +
", ppt='" + ppt + '\'' +
", training='" + training + '\'' +
", questionnaire='" + questionnaire + '\'' +
'}';
}

}

接着创建建造者 TrainingBuilder 类,将复杂的构造过程封装起来:

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
37
38
public class TrainingBuilder {

private TechnicalTraining technicalTraining = new TechnicalTraining();

/**
* 发布培训通知
* @param notice
*/
public void PostNotice(String notice) {
technicalTraining.setNotice(notice);
}

/**
* 制作培训PPT
*/
public void createPPT(String ppt) {
technicalTraining.setPpt(ppt);
}

/**
* 组织员工培训
*/
public void organizeTraining(String training) {
technicalTraining.setTraining(training);
}

/**
* 提交培训问卷
*/
public void sumitQuestionnaire(String questionnaire) {
technicalTraining.setQuestionnaire(questionnaire);
}

public TechnicalTraining build() {
return technicalTraining;
}

}

测试main方法:

public static void main(String[] args) {

TrainingBuilder builder = new TrainingBuilder();
builder.PostNotice("发布培训通知");
builder.createPPT("创建ppt");
builder.organizeTraining("组织员工培训");
builder.sumitQuestionnaire("提交培训问卷");
System.out.println(builder.build());

}

最后来看一下类图:

2.2 建造者模式的链式写法

在平时的应用中,建造者模式通常是采用链式编程的方式构造对象,下面我们来改造上面的案例代码,将TechnicalTraining变成TrainingBuilder的内部类,将构造步骤添加进去,每完成一个步骤,都返回 this:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
public class TrainingBuilder {

public class TechnicalTraining {

private String notice;
private String ppt;
private String training;
private String questionnaire;

public String getNotice() {
return notice;
}

public void setNotice(String notice) {
this.notice = notice;
}

public String getPpt() {
return ppt;
}

public void setPpt(String ppt) {
this.ppt = ppt;
}

public String getTraining() {
return training;
}

public void setTraining(String training) {
this.training = training;
}

public String getQuestionnaire() {
return questionnaire;
}

public void setQuestionnaire(String questionnaire) {
this.questionnaire = questionnaire;
}
}

private TechnicalTraining technicalTraining = new TechnicalTraining();

/**
* 发布培训通知
* @param notice
*/
public TrainingBuilder PostNotice(String notice) {
technicalTraining.setNotice(notice);
return this;
}

/**
* 制作培训PPT
*/
public TrainingBuilder createPPT(String ppt) {
technicalTraining.setPpt(ppt);
return this;
}

/**
* 组织员工培训
*/
public TrainingBuilder organizeTraining(String training) {
technicalTraining.setTraining(training);
return this;
}

/**
* 提交培训问卷
*/
public TrainingBuilder sumitQuestionnaire(String questionnaire) {
technicalTraining.setQuestionnaire(questionnaire);
return this;
}

public TechnicalTraining build() {
return this.technicalTraining;
}

}

测试main方法:

1
2
3
4
5
6
7
8
9
 public static void main(String[] args) {

TrainingBuilder builder = new TrainingBuilder();
builder.PostNotice("发布培训通知")
.createPPT("创建ppt")
.organizeTraining("组织员工培训")
.sumitQuestionnaire("提交培训问卷");
System.out.println(builder.build());
}

最后再来看下类图:

三、建造者模式在源码中的体现

3.1 StringBuilder类

使用StringBuilder类,我们常用的有append()、toString()方法,我们来看下append()方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public StringBuilder(CharSequence seq) {
this(seq.length() + 16);
append(seq);
}

@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}

@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}

3.2 Spring中的BeanDefinitionBuilder类

比如 BeanDefinitionBuilder 通过调用 getBeanDefinition()方法获得一个 BeanDefinition 对象,比如下面的源码:

1
2
3
4
5
6
7
8
9
10
11
12
private BeanDefinitionBuilder(AbstractBeanDefinition beanDefinition) {
this.beanDefinition = beanDefinition;
}

public AbstractBeanDefinition getRawBeanDefinition() {
return this.beanDefinition;
}

public AbstractBeanDefinition getBeanDefinition() {
this.beanDefinition.validate();
return this.beanDefinition;
}

四、建造者模式的优缺点

建造者模式的优点:

  • 封装性好,创建和使用分离;

  • 扩展性好,建造类之间独立、一定程度上解耦。

建造者模式的缺点:

  • 产生多余的 Builder 对象;

  • 产品内部发生变化,建造者都要修改,成本较大。

建造者模式和工厂模式的区别

通过前面的学习,我们已经了解建造者模式,那么它和工厂模式有什么区别呢?
1、建造者模式更加注重方法的调用顺序,工厂模式注重于创建对象。

2、创建对象的力度不同,建造者模式创建复杂的对象,由各种复杂的部件组成,工厂模式创建出来的都一样。

3、关注重点不一样,工厂模式模式只需要把对象创建出来就可以了,而建造者模式中不仅要创建出这个对象,还要知道这个对象由哪些部件组成。

4、建造者模式根据建造过程中的顺序不一样,最终的对象部件组成也不一样。

架构师内功心法,属于游戏设计模式的策略模式详解

一、策略模式的应用场景

策略模式(Strategy Pattern)是指定义了算法家族、分别封装起来,让它们之间可以相互替换,此模式让算法的变化不会影响到使用算法的用户。

1.1 应用场景

  • 假如系统中有很多类,而他们的区别仅仅在于他们的行为不同。
  • 一个系统需要动态地在几种算法中选择一种。

1.2 实现餐饮行业选择支付方式的业务场景

我们在外面去吃饭的时候,不同的饭店经常会有优惠活动,优惠策略也有很多很多,比如优惠券折扣、返现促销、拼团下单等等。我们来用程序模拟这样的业务场景,首先创建一个促销策略的接口:

1
2
3
4
public interface IPromotionStrategy {

void doPromotion();
}

然后分别创建优惠券抵扣策略 CouponStrategy 类:

1
2
3
4
5
6
public class CouponStrategy implements IPromotionStrategy {
@Override
public void doPromotion() {
System.out.println("领取的优惠券在指定时间到店消费,订单的价格直接减优惠券面额抵扣!");
}
}

返现促销策略 CashBackStrategy 类:

1
2
3
4
5
6
public class CashBackStrategy implements IPromotionStrategy {
@Override
public void doPromotion() {
System.out.println("返现促销,返回的金额转到支付账号!");
}
}

拼团优惠策略 GroupBuyStrategy 类:

1
2
3
4
5
6
public class GroupBuyStrategy implements IPromotionStrategy {
@Override
public void doPromotion() {
System.out.println("拼团,满5人成团,全团享受团购价!");
}
}

无优惠策略 EmptyStrategy 类:

1
2
3
4
5
6
public class EmptyStrategy implements IPromotionStrategy {
@Override
public void doPromotion() {
System.out.println("无促销活动!");
}
}

然后创建促销活动方案 PromotionActivity 类:

1
2
3
4
5
6
7
8
9
10
11
12
public class PromotionActivity {

private IPromotionStrategy promotionStrategy;

public PromotionActivity(IPromotionStrategy promotionStrategy) {
this.promotionStrategy = promotionStrategy;
}

public void execute() {
promotionStrategy.doPromotion();
}
}

编写测试类:

1
2
3
4
5
6
public static void main(String[] args) {
PromotionActivity activity_618 = new PromotionActivity(new CouponStrategy());
PromotionActivity activity_1212 = new PromotionActivity(new CashBackStrategy());
activity_618.execute();
activity_1212.execute();
}

此时,上面的这段测试代码放到实际的业务场景并不实用,因为餐饮门店做活动的时候是要根据不同需求对促销策略进行动态选择的,并不会一次性执行多种优惠。所以代码会这样写:

1
2
3
4
5
6
7
8
9
10
 public static void main(String[] args) {
PromotionActivity promotionActivity = null;
String promotionKey = "COUPON";
if(promotionKey.equals("COUPON")){
promotionActivity = new PromotionActivity(new CouponStrategy());
}else if(promotionKey.equals("CASHBACK")){
promotionActivity = new PromotionActivity(new CashBackStrategy());
}//......
promotionActivity.execute();
}

这样改造之后,满足了业务需求,客户可根据自己的需求选择不同的优惠策略了。但是,经过一段时间的业务积累,我们的促销活动会越来越多。于是,我们的程序猿小哥哥就忙不赢了,每次上活动之前都要通宵改代码,而且要做重复测试,判断逻辑可能也变得越来越复杂。这时候,我们是不需要思考代码是不是应该重构了?

其实我们可以结合工厂模式和单例模式来进行优化改造。创建PromotionActivityFactory:

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 PromotionActivityFactory {

public interface PromotionKey {
String COUPON = "COUPON";
String CASHBACK = "CASHBACK";
String GROUPBUY = "GROUPBUY";
}

private static Map<String, IPromotionStrategy> PROMOTION_STRATEGY_MAP =
new HashMap<>();
static {
PROMOTION_STRATEGY_MAP.put(PromotionKey.COUPON, new CouponStrategy());
PROMOTION_STRATEGY_MAP.put(PromotionKey.CASHBACK, new CashBackStrategy());
PROMOTION_STRATEGY_MAP.put(PromotionKey.GROUPBUY, new GroupBuyStrategy());
}

private static final IPromotionStrategy NO_PROMOTION = new EmptyStrategy();

private PromotionActivityFactory() {}

public static IPromotionStrategy getPromotionStrategy(String promotionKey) {
IPromotionStrategy promotionStrategy = PROMOTION_STRATEGY_MAP.get(promotionKey);
return promotionStrategy == null ? NO_PROMOTION : promotionStrategy;
}

}

测试代码如下:

1
2
3
4
5
6
public static void main(String[] args) {
String promotionKey = "COUPON";
PromotionActivity promotionActivity = new PromotionActivity(PromotionActivityFactory.getPromotionStrategy(promotionKey));
promotionActivity.execute();

}

修改代码之后维护工作应该轻松了很多,每次上新活动,不会影响原来的代码逻辑。来看一下完整的类图:

为了加深大家对策略模式的理解,结合实际生活场景再举一个例子。大家都用过移动支付进行付款,比较流行的支付方式有支付宝、微信、银联等。一个场景的支付场景就是在支付的时候提示会选择支付方式,如果用户没有进行选择,那么系统会使用默认的支付方式进行结算。

创建抽象类Payment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Payment {

public abstract String getType();

public PayState pay(String id, double amount) {
if(queryBalance(id) < amount) {
return new PayState(500,"支付失败","余额不足");
}
return new PayState(200,"支付成功","支付金额:" + amount);
}

public abstract double queryBalance(String id);

}

分别创建具体的支付方式,支付宝 AliPay 类:

1
2
3
4
5
6
7
8
9
10
11
public class AliPay extends Payment {
@Override
public String getType() {
return "支付宝";
}

@Override
public double queryBalance(String id) {
return 1000;
}
}

微信支付 WechatPay 类:

1
2
3
4
5
6
7
8
9
10
11
public class WechatPay extends Payment {
@Override
public String getType() {
return "微信支付";
}

@Override
public double queryBalance(String id) {
return 512;
}
}

银联云闪付支付 UnionPay 类:

1
2
3
4
5
6
7
8
9
10
11
public class UnionPay extends Payment {
@Override
public String getType() {
return "云闪付";
}

@Override
public double queryBalance(String id) {
return 380;
}
}

创建支付状态的包装类 PayState:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

public class PayState {

private int code;
private Object data;
private String msg;

public PayState(int code, Object data, String msg) {
this.code = code;
this.data = data;
this.msg = msg;
}

@Override
public String toString() {
return "PayState{" +
"code=" + code +
", data=" + data +
", msg='" + msg + '\'' +
'}';
}
}

创建支付策略管理工厂类:

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 PayStrategyFactory {

public interface PayKey {
String DEFAULTPAY = "ALIPAY";
String ALIPAY = "ALIPAY";
String WECHATPAY = "WECHATPAY";
String UNIONPAY = "UNIONPAY";
}

public static final Map<String, Payment> PAYMENT_MAP = new HashMap<>();

static {
PAYMENT_MAP.put(PayKey.ALIPAY, new AliPay());
PAYMENT_MAP.put(PayKey.WECHATPAY, new WechatPay());
PAYMENT_MAP.put(PayKey.UNIONPAY, new UnionPay());
}

private static Payment getPayment(String payKey) {
if(!PAYMENT_MAP.containsKey(payKey)){
return PAYMENT_MAP.get(PayKey.DEFAULTPAY);
}
return PAYMENT_MAP.get(payKey);
}

}

创建订单 Order 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Order {

private String id;
private String orderId;
private double amount;

public Order(String id, String orderId, double amount) {
this.id = id;
this.orderId = orderId;
this.amount = amount;
}

public PayState pay(){
return pay(PayStrategyFactory.PayKey.DEFAULTPAY);
}

public PayState pay(String payKey){
Payment payment = PayStrategyFactory.PAYMENT_MAP.get(payKey);
System.out.println("欢迎使用" + payment.getType());
System.out.println("本次交易金额为:" + amount + ",开始扣款...");
return payment.pay(id,amount);
}
}

测试代码如下:

1
2
3
4
5
6
public static void main(String[] args) {

Order order = new Order("1", "20200225000001", 120.98);

System.out.println(order.pay(PayStrategyFactory.PayKey.ALIPAY));
}

运行结果:

最后来看一下类图结构:

二、源码中的策略模式

2.1 Compartor接口

Compartor接口中的compare()方法就是一个策略模式的抽象实现。

int compare(T o1, T o2);

Comparator 接口下面有非常多的实现类,我们经常会把 Comparator 作为参数传入作为排序策略,例如 Arrays 类的 parallelSort 方法等:

还有 TreeMap 的构造方法:

2.2 Spring中的策略模式

2.2.1 Resouce类

我们来看Resource类的源码:

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
37
38
39
40
41
42
43
44
45
46
package org.springframework.core.io;

import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.ReadableByteChannel;
import org.springframework.lang.Nullable;

public interface Resource extends InputStreamSource {
boolean exists();

default boolean isReadable() {
return this.exists();
}

default boolean isOpen() {
return false;
}

default boolean isFile() {
return false;
}

URL getURL() throws IOException;

URI getURI() throws IOException;

File getFile() throws IOException;

default ReadableByteChannel readableChannel() throws IOException {
return Channels.newChannel(this.getInputStream());
}

long contentLength() throws IOException;

long lastModified() throws IOException;

Resource createRelative(String var1) throws IOException;

@Nullable
String getFilename();

String getDescription();
}

我们虽然没有直接使用 Resource 类,但是我们经常使用它的子类,例如:

Spring 的初始化也采用了策略模式,不同的类型的类采用不
同的初始化策略。首先有一个 InstantiationStrategy 接口,我们来看一下源码:

1
2
3
4
5
6
7
public interface InstantiationStrategy {
Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3) throws BeansException;

Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, Constructor<?> var4, Object... var5) throws BeansException;

Object instantiate(RootBeanDefinition var1, @Nullable String var2, BeanFactory var3, @Nullable Object var4, Method var5, Object... var6) throws BeansException;
}

顶层的策略抽象非常简单,但是它下面有两种策略 SimpleInstantiationStrategyCglibSubclassingInstantiationStrategy,我们看一下类图:


打开类图我们还发现 CglibSubclassingInstantiationStrategy 策略类还继承了
SimpleInstantiationStrategy类,说明在实际应用中多种策略之间还可以继承使用。

三、策略模式的优缺点

优点:

  • 策略模式符合开闭原则;
  • 避免使用多重条件转移语句,如 if…else…语句、switch 语句;
  • 使用策略模式可以提高算法的保密性和安全性。

缺点:

  • 客户端必须知道所有的策略,并且自行决定使用哪一个策略类;
  • 代码中会产生非常多策略类,增加维护难度。

架构师内功心法,干过中介干过快递的代理模式详解

一、代理模式的应用场景

在我们的生活中,经常会见到这样的场景,如:租售房中介、婚介、经纪人、快递等,这些都是代理模式的现实生活体现。代理模式(Proxy Pattern)是指为其它对象提供一种代理,以控制对这个对象的访问。代理对象在客户端和目标对象中间起到了中介的作用,使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。

代理模式的类图结构:

Subject是顶层设计的接口,RealSubject是真实的对象,Proxy是代理对象,代理对象持有真实对象的引用,客户端Client调用代理对象的方法,同时也调用真实对象的方法,在代理对象前后增加一些处理。我们一想到代理模式,就会理解为代码增强,其实就是在原本的代码逻辑前后增加一些逻辑,而使得调用者无感知。代理模式分为静态代理和动态代理。

二、代理模式的分类

2.1 静态代理

我们直接来举例说明静态代理,青年男女到了适婚的年龄,如果没有对象,周围的亲戚朋友总是张罗着要给某某某介绍对象,这个介绍对象相亲的过程,就是一种我们人人都有份的代理。来看代码实现:

顶层接口设计Person类:

1
2
3
4
5
6
7
public interface Person {

/**
* 寻找伴侣
*/
void lookForMate();
}

女儿要求找对象,实现Person接口:

1
2
3
4
5
6
public class Daughter implements Person {
@Override
public void lookForMate() {
System.out.println("女儿要求:高大英俊且有钱!");
}
}

母亲要帮闺女相亲,实现Mother类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Mother {

private Daughter daughter;
//如何扩展呢
public Mother(Daughter daughter) {
this.daughter = daughter;
}

//目标对象的引用daughter拿到,可以调用
public void lookForMate() {
System.out.println("母亲物色女儿的对象");
daughter.lookForMate();
System.out.println("双方同意交往并确立关系");
}

}

测试内容:

1
2
3
4
5
public static void main(String[] args) {
//只能帮女儿找对象,不能帮表妹、不能帮陌生人
Mother mother = new Mother(new Daughter());
mother.lookForMate();
}

运行结果:

上面的这个例子是生活中的例子,我们用代码实现了生活中的代理模式。再来一个具体的实际业务场景的例子吧。我们经常会对数据库进行分库分表,分库分表后用Java代码来操作,就需要配置多个数据源,通过设置数据源路由来动态动态切换数据源。
创建订单实体类:

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 Order {

private String id;

private Object orderInfo;

private Long createTime;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public Object getOrderInfo() {
return orderInfo;
}

public void setOrderInfo(Object orderInfo) {
this.orderInfo = orderInfo;
}

public Long getCreateTime() {
return createTime;
}

public void setCreateTime(Long createTime) {
this.createTime = createTime;
}
}

创建OrderDao持久层操作类:

1
2
3
4
5
6
public class OrderDao {
public int insert(Order order) {
System.out.println("创建order对象成功!");
return 1;
}
}

创建 IOrderService 接口:

1
2
3
public interface IOrderService {
int createOrder(Order order);
}

创建 OrderService 实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class OrderService implements IOrderService {

private OrderDao orderDao;

public OrderService(OrderDao orderDao) {
orderDao = new OrderDao();
}

@Override
public int createOrder(Order order) {
System.out.println("OrderService调用OrderDao创建订单");
return orderDao.insert(order);
}
}

我们来使用静态代理,完成订单创建时间自动按年份进行分库,通过使用代理对象来完成接下来的代码。创建数据源路由对象,使用ThreadLocal单例实现,DynamicDataSourceEntity:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class DynamicDataSourceEntity {

/**
* 默认数据源
*/
public static final String DEFAULT_DATA_SOURCE = null;

private static final ThreadLocal<String> local = new ThreadLocal<>();

private DynamicDataSourceEntity() {}

/**
* 获取当前正在使用的数据源
* @return
*/
public static String get() {
return local.get();
}

/**
* 设置已知名字的数据源
* @param dataSource
*/
public static void set(String dataSource) {
local.set(dataSource);
}

/**
* 还原当前切面的数据源
*/
public static void restore() {
local.set(DEFAULT_DATA_SOURCE);
}

/**
* 根据年份动态设置数据源
* @param year
*/
public static void set(int year) {
local.set("DB_" + year);
}

/**
* 清空数据源
*/
public static void remove() {
local.remove();
}

}

创建切换数据源代理OrderServiceStaticProxy:

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
public class OrderServiceStaticProxy implements IOrderService {

private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");

private IOrderService orderService;

public OrderServiceStaticProxy(IOrderService orderService) {
this.orderService = orderService;
}

@Override
public int createOrder(Order order) {
before();
Long time = order.getCreateTime();
Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
System.out.println("静态代理类自动分配到【DB_" + dbRouter + "】数据源处理数据。");
DynamicDataSourceEntity.set(dbRouter);
orderService.createOrder(order);
after();
return 0;
}

private void before(){
System.out.println("代理方法执行开始了......");
}
private void after(){
System.out.println("代理方法执行结束了......");
}
}

main方法的代码:

public static void main(String[] args) throws ParseException {

Order order = new Order();
order.setId("010101001");
//Date today = new Date();
//order.setCreateTime(today.getTime());

SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
Date date = sdf.parse("2019/02/01");
order.setCreateTime(date.getTime());


IOrderService orderService = new OrderServiceStaticProxy(new OrderService());
orderService.createOrder(order);

}

运行结果是:

符合我们的预期效果。现在我们再来回顾一下类图,看是不是和我们最先画的类结构一致:

2.2 动态代理

动态代理和静态代理的思路基本是一致的,只不过动态代理的功能更加强大,随着业务的扩展适应性更强。前面说到的母亲替闺女找对象的例子,如果找对象的业务发展为一个行业,那么就是婚姻中介了。来升级代码的实现过程,以满足帮助更多的单身人士找对象的需求。下面使用JDK的方式实现婚姻介绍所。

2.2.1 JDK实现方式

创建婚姻介绍JDKMarriage类:

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 JDKMarriage implements InvocationHandler {

private Object target;

public Object getInstance(Object target) {
this.target = target;

Class<?> clazz = target.getClass();
return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before();
Object object = method.invoke(this.target, args);
after();
return object;
}

private void before(){
System.out.println("我是婚姻介绍所:要给你找对象,现在已经拿到你的需求");
System.out.println("开始物色");
}
private void after(){
System.out.println("如果合适的话,就准备办事");
}
}

创建单身客户Customer类:

1
2
3
4
5
6
7
8
public class Customer implements Person {
@Override
public void lookForMate() {
System.out.println("高富帅");
System.out.println("身高180cm");
System.out.println("有房有车");
}
}

测试main方法代码:

1
2
3
4
5
6
7
 public static void main(String[] args) {

JDKMarriage marriage = new JDKMarriage();

Person person = (Person) marriage.getInstance(new Customer());
person.lookForMate();
}

运行结果:

上面的动态代理案例通过实现InvocationHandler接口来完成的,在前面的数据源路由业务,也要用动态代理来实现一下,我们来看下代码:

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
37
38
39
40
public class OrderServiceDynamicProxy implements InvocationHandler {

private SimpleDateFormat yearFormat = new SimpleDateFormat("yyyy");

public Object target;

public Object getInstance(Object target) {
this.target = target;
Class<?> clazz = target.getClass();
return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
before(args[0]);
Object object = method.invoke(target, args);
after();
return object;
}

public void before(Object target) {
try {
System.out.println("代理方法执行开始了......");
Long time = (Long)target.getClass().getMethod("getCreateTime").invoke(target);
Integer dbRouter = Integer.valueOf(yearFormat.format(new Date(time)));
System.out.println("动态代理类自动分配到【DB_" + dbRouter + "】数据源处理数据");
DynamicDataSourceEntity.set(dbRouter);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}

public void after() {
System.out.println("代理方法执行结束了......");
}
}

测试main方法代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws ParseException {
Order order = new Order();
order.setId("010101001");
// Date today = new Date();
// order.setCreateTime(today.getTime());

SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
Date date = sdf.parse("2019/02/01");
order.setCreateTime(date.getTime());

IOrderService orderService = (IOrderService) new OrderServiceDynamicProxy().getInstance(new OrderService());
orderService.createOrder(order);
}

运行结果:

依然可以达到想要的运行效果。但是,动态代理实现之后,我们不仅能实现 Order 的数据源动态路由,还可以实现其他任何类的数据源路由。

2.2.2 CGLib代理调用API及原理分析

pom依赖:

1
2
3
4
5
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>

还是以婚姻介绍所为例,创建CglibMarriage类:

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
public class CglibMarriage implements MethodInterceptor {

public Object getInstance(Class<?> clazz) throws Exception {
Enhancer enhancer = new Enhancer();
//要把哪个类设置成为生成的新类的父类
enhancer.setSuperclass(clazz);
enhancer.setCallback(this);

return enhancer.create();
}

@Override
public Object intercept(Object o, Method method, Object[] objects,
MethodProxy methodProxy) throws Throwable {
before();
Object obj = methodProxy.invokeSuper(o, objects);
after();
return obj;
}

private void before(){
System.out.println("我是婚姻介绍所:要给你找对象,现在已经拿到你的需求");
System.out.println("开始物色");
}
private void after(){
System.out.println("如果合适的话,就准备办事");
}
}

接着创建单身客户类Customer:

1
2
3
4
5
6
7
8
public class Customer {

public void lookForMate() {
System.out.println("高富帅");
System.out.println("身高180cm");
System.out.println("有房有车");
}
}

注意:CGLib代理的目标对象不需要实现任何接口,它是通过动态继承目标对象实现动态代理的。 来看测试代码:

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {

try {
Customer customer = (Customer)new CglibMarriage().getInstance(Customer.class);
customer.lookForMate();
} catch (Exception e) {
e.printStackTrace();
}

}

CGLib代理执行代理对象的方法效率之所以比JDK的高,是因为CGLib采用了FastClass机制,FastClass的原理是:为代理类和被代理类各生成一个class,这个class会为代理类或被代理类的方法分配一个index(int类型),这个index当作入参,FastClass就可以直接定位要调用的方法直接进行调用,这样省去了反射调用,所以调用效率比JDK动态代理通过反射调用高。

2.2.3 CGLib和JDK动态代理对比

1、JDK动态代理实现了被代理对象的接口,CGLib代理继承了被代理对象。

2、JDK和CGLib都在运行期间生成字节码,JDK动态代理直接生成class字节码,CGLib代理通过asm框架生成class字节码,CGLib代理实现更复杂,生成代理类比JDK动态代理效率低。

3、JDK动态代理调用代理方法是通过反射机制调用的,CGLib代理是通过FastClass机制直接调用方法的,CGLib代理的执行效率高。

三、Spring与代理模式

3.1 代理模式在Spring源码中的应用

先看 ProxyFactoryBean 核心的方法就是 getObject() 方法,我们来看一下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Nullable
public Object getObject() throws BeansException {
this.initializeAdvisorChain();
if (this.isSingleton()) {
return this.getSingletonInstance();
} else {
if (this.targetName == null) {
this.logger.info("Using non-singleton proxies with singleton targets is often undesirable. Enable prototype proxies by setting the 'targetName' property.");
}

return this.newPrototypeInstance();
}
}

在 getObject()方法中,主要调用 getSingletonInstance()newPrototypeInstance()
在 Spring 的配置中,如果不做任何设置,那么 Spring 代理生成的 Bean 都是单例对象。如果修改 scope则每次创建一个新的原型对象。newPrototypeInstance()里面的逻辑比较复杂,我们后面的课程再做深入研究,这里我们先做简单的了解。

3.2 Spring 中的代理选择原则

Spring 利用动态代理实现 AOP 有两个非常重要的类,一个是 JdkDynamicAopProxy 类和 CglibAopProxy 类,来看一下类图:

  • 当 Bean 有实现接口时,Spring 就会用 JDK 的动态代理;
  • 当 Bean 没有实现接口时,Spring 选择 CGLib。

三、 静态代理和动态的本质区别

1、静态代理只能通过手动完成代理操作,如果被代理类增加新的方法,代理类需要同步新增,违背开闭原则。

2、动态代理采用在运行时动态生成代码的方式,取消了对被代理类的扩展限制,遵循开闭原则。

3、若动态代理要对目标类的增强逻辑扩展,结合策略模式,只需要新增策略类便可完成,无需修改代理类的代码。

四、代理模式的优缺点

使用代理模式具有以下几个优点:

  • 代理模式能将代理对象与真实被调用的目标对象分离;
  • 一定程度上降低了系统的耦合度,扩展性好;
  • 可以起到保护目标对象的作用;
  • 可以对目标对象的功能增强。

当然,代理模式也是有缺点的:

  • 代理模式会造成系统设计中类的数量增加;
  • 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢;
  • 增加了系统的复杂度。