7种结构型模式:代理、装饰、适配器、组合、外观、享元、桥接。
代理模式 Proxy
也叫委托模式。许多其他模式如状态模式、策略模式、访问者模式本质上是在特殊场合采用了委托模式。
类图
使用场景
典型动态代理:Spring AOP
应用广泛:系统框架、企业平台、事务处理
优点
- 职责清晰:真实角色只需实现实际的业务逻辑,由代理完成事务。
- 高扩展性:具体的主题角色不管怎么变化,只要实现了接口,代理类就可以在不做任何修改的情况下使用。
- 智能化:动态代理。
扩展
普通代理
调用者需要知道代理类的存在才能访问,不用知道真实角色。
适合扩展性要求较高的场合。实际项目中通常约定禁止new一个真实角色。
强制代理
代理管理由真实角色完成,不能随便new。高层模块只需调用 getProxy 就可以访问真实角色所有方法。必须通过真实角色查找到代理角色才能访问,不允许直接访问真实角色。
代理是有个性的
代理可实现其他接口完成不同的任务。
代理的目的是在目标对象方法的基础上作增强,进行拦截和过滤。
动态代理
实现阶段不关心代理谁,而在运行阶段才指定代理哪个对象。
相对来说,自己写代理类的方式是静态代理。
AOP:Aspect Oriented Programming
实现思路:实现 InvocationHandler 动态代理接口,重写 invoke() 方法,完成对真实方法的调用。在运行时动态产生代理对象。
代码演示
普通代理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18interface Subject1 {
void doSomething(String str);
}
class Subject1Impl implements Subject1 {
public void doSomething(String str) {System.out.println("Subject1 做" + str);}
}
// 代理要实现相同接口,高层模块调用时要先构造代理对象
class NormalProxy implements Subject1 {
private Subject1 subject1;
public NormalProxy(Subject1 subject1) {this.subject1 = subject1;}
public void doSomething(String str) {subject1.doSomething(str);}
}强制代理
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
54interface Subject2 {
void doSomething(String str);
// 获取自身特定的代理类
Subject2 getProxy();
}
class Subject2Impl implements Subject2 {
private ForceProxy forceProxy;
public void doSomething(String str) {
if (isProxy()) {
System.out.println("Subject2 在做" + str);
} else {
System.out.println("请使用自身代理执行 做" + str);
}
}
// 判断是否拥有代理
private boolean isProxy() {return this.forceProxy != null;}
public Subject2 getProxy() {
if (this.forceProxy == null) {
synchronized (this) {
if (this.forceProxy == null) {
this.forceProxy = new ForceProxy(this);
}
}
}
return this.forceProxy;
}
}
class ForceProxy implements Subject2 {
private Subject2 subject2;
public ForceProxy(Subject2 subject2) {this.subject2 = subject2;}
public void doSomething(String str) {subject2.doSomething(str);}
public Subject2 getProxy() {return this;}
}
class Client2 {
public static void main(String[] args) {
Subject2 subject2 = new Subject2Impl();
subject2.doSomething("家务");
ForceProxy forceProxy = new ForceProxy(subject2);
forceProxy.doSomething("饭");
subject2.getProxy().doSomething("运动");
}
}个性化的代理类
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
31interface Player {void doSport();}
interface Boss {void invest(String str);}
class PlayerImpl implements Player {
public void doSport() {System.out.println("运动员做XXX运动太出名了");}
}
class MultiImplementProxy implements Player, Boss {
private Player player;
public MultiImplementProxy(Player player) {this.player = player;}
public void doSport() {
player.doSport();
this.invest("拍电影");
}
public void invest(String str) {System.out.println("代理人有钱,给运动员投资" + str);}
}
class Client3 {
public static void main(String[] args) {
Player player = new PlayerImpl();
MultiImplementProxy proxy = new MultiImplementProxy(player);
proxy.doSport();
}
}动态代理
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// 动态代理的 handler 类
class MyInvocationHandler implements InvocationHandler {
// 被代理对象
private Object obj;
public MyInvocationHandler(Object obj) {this.obj = obj;}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
return method.invoke(obj, args);
}
}
// 切面接口
interface IAdvice {void exec();}
class BeforeAdvice implements IAdvice{
public void exec() {System.out.println("前置通知被执行!");}
}
// 通用动态代理类,只做切面增强
class DynamicProxy {
public static <T> T newInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) {
// 寻找 JoinPoint 连接点,AOP框架使用元数据定义
if (true) {
// 执行一个前置通知
new BeforeAdvice().exec();
}
return (T) Proxy.newProxyInstance(loader, interfaces, h);
}
}
// 具有业务意义的动态代理类
class SubjectDynamicProxy extends DynamicProxy {
private SubjectDynamicProxy() {}
public static <T> T newProxyInstance(Subject4 subject4) {
ClassLoader loader = subject4.getClass().getClassLoader();
Class<?>[] interfaces = subject4.getClass().getInterfaces();
MyInvocationHandler handler = new MyInvocationHandler(subject4);
return newInstance(loader, interfaces, handler);
}
}
// 业务接口
interface Subject4 {void doSomething();}
class Subject4Impl implements Subject4 {
public void doSomething() {System.out.println("Subject4 做某事");}
}
// 高层调用模块——客户端
class client4 {
public static void main(String[] args) {
Subject4 proxy = SubjectDynamicProxy.newProxyInstance(new Subject4Impl());
proxy.doSomething();
}
}
装饰模式 Decorator
类图
使用场景
- 需要扩展一个类的功能,或给一个类增加附加功能。
- 需要动态地给一个对象增加功能,这些功能可以再动态地撤销。
- 需要为一批的兄弟类进行改造或加装功能,首选装饰模式。
优点
装饰类和被装饰类可以独立发展,不会相互耦合。换句话说,Component类无须知道Decorator类,Decorator类是从外部来扩展Component类的功能,而Decorator也不用知道具体的构件。
对于 Father、Son、GrandSon三个类,如果要在 son 上增强功能,在 Son 上直接修改可能会影响到 GrandSon,但采用 SonDecorator 类来修饰 Son 相当于创建新类,对原来的程序无影响。
装饰模式是继承关系的替代方案。对于装饰类 Decorator,不管装饰多少层,返回的对象还是 Component,实现的还是 is-a 的关系。
继承会增加很多子类,装饰模式可以有效解决类膨胀的问题。
装饰模式可以动态地扩展一个实现类的功能。
想去掉某个封装只需要在高层模块去掉即可,如果用继承就必须要修改子模块。
缺点
多层的装饰比较复杂。定位问题时不容易发现是那一层装饰出了问题。
因此尽量减少装饰类的数量,以便降低系统的复杂度。
注意事项
与代理模式的比较:类图、代码实现非常相似,区别在于
代理负责接口限定:是否可以调用真实角色,以及是否对发送到真实角色的消息进行变形处理,不对被代理类的功能做任何处理。应用广范。
换个说法,代理模式会由代理进行相关操作,在代理内部通过new等方式创建被代理对象,对外屏蔽真实角色。而装饰模式是对真实角色的增强,装饰时需要传入被装饰对象,外部仍然可以直接调用。
装饰保证接口不变做类的加强,保证被修饰对象功能比原始对象丰富或减弱,但不做准入条件判断和准入参数过滤。例如 JDK 的 java.io.* 包中的
1
OutputStream out = new DataOutPutStream(new FileOutputStream("test.txt"));
代码演示
1 | abstract class SchoolReport { |
适配器模式 Adapter
也叫变压器模式、包装模式(包装模式包括装饰模式)。
类图
使用场景
有动机修改一个已经投产中的接口时,比如系统扩展需要使用一个已有或新建的类,但这个类又不符合系统的接口,采用适配器模式。
作为补偿模式,用来解决接口不相容的问题。
优点
- 让两个没有关联的类一起运行。
- 增加类的透明性,高层模块不需要关注源角色怎么执行。
- 提高类的复用度,源角色在原有系统中可以正常使用,而在目标角色中也可以发挥新作用。
- 灵活性好,不需要适配器时删除掉即可。
注意事项
- 在详细设计阶段不要考虑使用适配器模式,它是为了解决正在服役的项目问题,减少代码修改带来的风险。
- 项目一定要遵守依赖倒置原则和里氏替换原则,否则即使使用适配器模式改动也很大。
扩展
类适配器
通过继承进行适配。智能通过覆写源角色的方法进行扩展。
对象适配器
通过关联、聚合进行适配。适用于多接口适配。可以灵活修补源角色的隐形缺陷、关联其他对象等。实际项目中对象适配器使用场景较多。
代码演示
类适配器
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// 接口
interface Target {void request();}
// 实现类
class TargetImpl implements Target {
public void request() {System.out.println("目标实现类执行request");}
}
// 源角色
class Adaptee {
void doSomething() {System.out.println("源角色要做的事情");}
}
// 适配器角色
class Adapter extends Adaptee implements Target {
public void request() {
System.out.println("适配器要做的事");
super.doSomething();
}
}
class Client {
public static void main(String[] args) {
// 原逻辑
Target target = new TargetImpl();
target.request();
// 使用适配器后,也拥有了request方法,扩展了新功能
Target adapter = new Adapter();
adapter.request();
}
}对象适配器
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// 用户信息
interface IUserInfo {Map getUserInfo();}
class UserInfo implements IUserInfo {
public Map getUserInfo() {return new HashMap();}
}
interface IUserHomeInfo {Map getHomeInfo();}
interface IUserOfficeInfo {Map getOfficeInfo();}
class UserHomeInfo implements IUserHomeInfo {
public Map getHomeInfo() {
Map map = new HashMap();
map.put("homeAddress", "北京市海淀区奥林匹克公园");
map.put("homePhone", "家庭电话:123456");
return map;
}
}
class UserOfficeInfo implements IUserOfficeInfo {
public Map getOfficeInfo() {
Map map = new HashMap();
map.put("officeAddress", "办公地址:鸟巢");
map.put("officePhone", "办公电话:222333");
return map;
}
}
// 用户基本信息,适配home和office两个接口
class UserBaseInfo implements IUserInfo {
private IUserHomeInfo userHomeInfo;
private IUserOfficeInfo userOfficeInfo;
private Map map = new HashMap();
public UserBaseInfo(IUserHomeInfo userHomeInfo, IUserOfficeInfo userOfficeInfo) {
this.userHomeInfo = userHomeInfo;
this.userOfficeInfo = userOfficeInfo;
map.putAll(userHomeInfo.getHomeInfo());
map.putAll(userOfficeInfo.getOfficeInfo());
}
public Map getUserInfo() {return map;}
}
class AdapterClient2 {
public static void main(String[] args) {
IUserHomeInfo homeInfo = new UserHomeInfo();
IUserOfficeInfo officeInfo = new UserOfficeInfo();
IUserInfo userInfo = new UserBaseInfo(homeInfo, officeInfo);
Map info = userInfo.getUserInfo();
System.out.println(info.toString());
}
}
组合模式 Composite
也叫合成模式、部分—整体模式,将对象组合成树形结构用来表示部分与整体的关系。
类图
使用场景
- 维护和展示部分—整体关系的场景,如树形菜单、文件盒文件夹管理。
- 从一个整体中能够独立出部分模块或功能的场景。
优点
高层模块调用简单
一棵树形结构中的所有节点都是Component,局部和整体对调用者来说没有任何区别。高层模块不用关心处理的是单个对象还是整个组合结构。
节点自由增加
增加树枝节点、树叶节点非常容易,符合开闭原则,有利于维护。
缺点
不符合依赖倒置原则
场景类中树叶和树枝直接使用了实现类,限制了接口的影响范围。
注意事项
- 只要是树形结构,就要考虑使用组合模式。
- 体现局部和整体的关系,当关系比较深时,考虑组合模式。
扩展
真实的组合模式
实际使用中仍需组装这棵树,使用关系型数据库或者其他方式存储树形结构。
安全模式
树枝节点和树叶节点彻底分开,树枝节点单独拥有用来组合的方法,这种方法比较安全。
透明模式
把用来组合使用的方法放到抽象类中,不管叶子对象还是数值对象都有相同的结构。通过判断 getChildren 的返回值确认是叶子节点还是树枝节点,如果处理不当,会在运行期出问题。
组合模式的遍历
从上往下遍历没有问题,如果从下往上遍历,则需要设置 parent 属性。从而实现后序、中序等遍历方式。
树叶、树枝节点排列顺序
不用list,改用treeSet存储对象即可,可以加上实现 Comparable 接口,覆盖 compareTo 方法。
代码演示
安全模式
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
50abstract class Staff {
private String name;
private String position;
public Staff(String name, String position) {
this.name = name;
this.position = position;
}
public String getInfo() {return this.toString();}
}
class Leaf extends Staff {
public Leaf(String name, String position) {super(name, position);}
}
class Branch extends Staff {
List<Staff> subordinate = new ArrayList<>();
public Branch(String name, String position) {super(name, position);}
public void addStaff(Staff staff) {this.subordinate.add(staff);}
public List<Staff> getSubordinate() {return this.subordinate;}
}
class CompositeClient1 {
static String getTreeInfo(Branch root) {
String info = "";
List<Staff> staff = root.getSubordinate();
for (Staff s : staff) {
if (s instanceof Leaf) {
info = info + s.getInfo();
} else {
info = info + s.getInfo() + "\t" + getTreeInfo((Branch) s);
}
}
return info;
}
public static void main(String[] args) {
// 组装
Staff staff = new Branch("张三", "局长");
Staff staff2 = new Branch("李四", "科长");
Staff leaf2 = new Leaf("王五", "组员");
Branch branch = ((Branch) staff).addStaff(staff2);
Branch branch2 = ((Branch) staff2).addStaff(leaf2);
// 打印树枝节点
System.out.println(getTreeInfo(branch));
// 打印树枝节点2
System.out.println(getTreeInfo(branch2));
}
}透明模式
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// add、remove等方法放在抽象类中
abstract class Component {
private String name;
public Component(String name) { this.name = name; }
public String getInfo() { return name; }
abstract void add(Component component);
abstract void remove(Component component);
abstract List<Component> getChildren();
}
class Leaf2 extends Component {
public Leaf2(String name) { super(name); }
void add(Component component) { throw new UnsupportedOperationException("叶子没有 add"); }
void remove(Component component) { throw new UnsupportedOperationException("叶子没有 remove"); }
List<Component> getChildren() { throw new UnsupportedOperationException("叶子没有子节点"); }
}
class Branch2 extends Component {
List<Component> subordinate = new ArrayList<>();
public Branch2(String name) { super(name); }
void add(Component component) { subordinate.add(component); }
void remove(Component component) { subordinate.remove(component); }
List<Component> getChildren() { return this.subordinate; }
}
class CompositeClient2 {
public static String display(Component root) {
String s = "";
if (root instanceof Leaf2) {
s += root.getInfo();
} else {
s += root.getInfo();
for (Component child : root.getChildren()) {
s += "\t" + display(child);
}
}
return s;
}
public static void main(String[] args) {
Branch2 b1 = new Branch2("分支1");
Branch2 b2 = new Branch2("分支1-1");
Leaf2 l = new Leaf2("叶子1");
b1.add(b2);
b2.add(l);
System.out.println(display(b1));
System.out.println("------");
System.out.println(display(b2));
System.out.println("------");
System.out.println(display(l));
}
}
外观模式 Facade
也叫 门面模式。子系统外部与内部通信必须通过统一的对象进行,也就是提供一个访问子系统的接口,除了这个接口不允许有任何访问子系统的行为发生。
门面对象是外界访问子系统内部的唯一通道,不管子系统内部多么杂乱,只要有门面对象,就可做到“金玉其外败絮其中”。
类图
使用场景
- 为一个复杂的模块或子系统提供一个供外界访问的接口。
- 子系统相对独立——外界对子系统的访问只要黑箱操作即可。例如利息计算子系统,对于使用者来说只要输入金额以及存期返回结果利息就可以,不用关心其他问题。
- 预防低水平人员带来的风险扩散。“画地为牢”,只在指定的子系统中开发,再提供门面接口进行访问操作。
优点
- 减少系统间的互相依赖。避免外界访问直接深入到子系统内部形成强耦合关系。
- 提高灵活性。子系统内部变化不影响门面对象,则随意使用。
- 提高安全性。门面上未开通的方法无法访问。
缺点
- 不符合开闭原则,对修改关闭,对扩展开放。如果系统投产后发现有问题,只能通过修改门面角色的代码解决。
注意事项
一个子系统可以有多个门面
门面已经庞大到不能忍受的程度。比如一个纯洁的门面对象已经超过了200行代码,虽然都是非常简单的委托操作,也建议拆分成多个门面,否则不利于以后的维护和扩展。拆分原则可按照功能拆分,例如数据库操作门面可以拆分为查询门面、删除门面、更新门面等。
子系统需要提供不同访问路径。例如模块一的门面已经封装了3个类ABC,模块二可以委托模块一的门面对象完成调用ABC中某一个类的操作,当外界调用模块二门面对象时,就减少了访问权限。
1
2
3
4
5
6
7
8
9
10class Facade {
private ClassA a = new ClassA();
private ClassB b = new ClassB();
public void methodA() {this.a.doSomethingA();}
public void methodB() {this.b.doSomethingB();}
}
class Facade2() {
private Facade facade = new Facade();
public void methodB() {this.facade.methodB();}
}门面不参与子系统内的业务逻辑
当需要修改B方法时,不能这样改!
1
2
3
4public void methodB() {
this.a.doSomethingA();
this.b.doSomethingB();
}而是建立一个封装类,封装完毕后提供给门面对象!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 建立封装类
class Context {
private ClassA a = new ClassA();
private ClassB b = new ClassB();
public void complexMethod(){
this.a.doSomethingA();
this.b.doSomethingB();
}
}
// 门面上添加相应的方法
class Facade {
private ClassA a = new ClassA();
private ClassB b = new ClassB();
private Context context = new Context();
public void methodA() {this.a.doSomethingA();}
public void methodB() {this.b.doSomethingB();}
public void methodC() {this.context.conplexMethod();}
}
代码演示
以写信为例。
1 | /** |
享元模式 Flyweight
flyweight是拳击比赛中的特用名词,意思是“特轻量级”,指51公斤级比赛。在设计模式中指类要轻量级、粒度要小。粒度小带来的问题就是对象太多,就可以用共享技术来解决。
享元模式是池技术的重要实现方式。要求:细粒度的对象和共享对象。抽象后包括:
内部状态:对象可共享出来的信息,存储在享元对象内部并且不会随环境改变而改变。
外部状态:对象得以依赖的一个标记,随环境改变而改变、不可共享的状态。
Flyweight——抽象享元角色:抽象类,同时定义出对象的外部状态和内部状态的接口或实现。
ConcreteFlyweight——具体享元角色:实现类。
unsharedConcreteFlyweight——不可共享的享元角色:不存在外部状态或者安全要求(如线程安全)无法使用共享技术的对象,该对象一般不会出现在享元工厂中。
FlyweightFactory——享元工厂:构造池容器,同时提供从池中获得对象的方法。
类图
使用场景
- 系统中存在大量的相似对象。
- 细粒度的对象都具备较接近的外部状态,而且内部状态与环境无关,也就是说对象没有特定身份。
- 需要缓冲池的场景。
优点
- 大大减少应用创建的对象数量,降低程序内存的占用,增强程序性能。
缺点
- 提高了系统复杂性,需要分离出外部状态和内部状态。而且内部状态具有固化特性,不应该随外部状态改变而改变,否则会导致系统逻辑混乱。
注意事项
享元模式和对象池
可以使用享元模式实现对象池,但两者差异较大。
对象池是构造型模式,着重在对象的复用上,池中每个对象可替换,从同一个池中获得A对象和B对象对客户端来说是完全相同的。即对象池中的对象都是等价的,任意两个个对象在任何使用场景中都可以被池中的其他对象代替。例如数据库连接池、线程池等。
享元模式是结构型模式,主要解决对象的访问问题,如何建立多个可共享的细粒度对象才是关注的重点。享元工厂所维护的所有对象都是不同的,任何两个对象不能互相代替。侧重对象之间的衔接,把动态的、会变化的状态剥离、外部化,共享不变的东西。但是这部分外部化的东西和享元模式内部共享的不会变的东西之间存在关联。所以享元对外提供的接口常常会包含一个 String 类型的参数,表示 key、名称之类。
扩展
线程安全问题
多个线程从对象池中获得对象然后修改其属性,会出现线程安全问题。
性能平衡
尽量使用java基本类型作为外部状态。使用自定义类作为外部条件,需要覆写 equals 和 hashCode 方法,执行效率还低。使用 java 基本类型又简洁效率又高。
代码演示
1 | abstract class Flyweight { |
桥接模式 Bridge
也叫桥梁模式,将抽象和实现解耦,使得两者可以独立变化。包含4个角色:
- 抽象化角色 Abstraction:定义该角色的行为,同时保存一个队实现化角色的引用,该角色一般是抽象类。
- 实现化角色 Implementor:接口或抽象类,定义角色必须的行为和属性。
- 修正抽象化角色 RefinedAbstraction:引用实现化角色对抽象化角色进行修正。
- 具体实现化角色 ConcreteImplementor:实现接口或抽象类定义的方法和属性。
抽象角色引用实现角色,或者说抽象角色的部分实现是由实现角色完成的。
类图
使用场景
- 不希望或者不适合使用继承的场景。例如继承层次过多、无法更细化设计颗粒等场景。
- 接口或抽象类不稳定的场景。明知道接口不稳定还想通过实现或继承来实现业务需求,得不偿失。
- 重用性要求较高的场景。设计的颗粒度越细,则被重用的可能性就越大,而采用继承则受父类的限制,不可能出现太细的颗粒度。
优点
抽象和实现分离
完全是为了解决继承的缺点而提出的设计模式。在该模式下,实现可以不受抽象的约束,不用再绑定在一个固定的抽象层次上。
优秀的扩充能力
增加实现、抽象非常容易,只要堆外暴露的接口层允许这样的变化,已经把变化的可能性减到最小。
实现细节对客户透明
客户不用关心细节的实现,已经由抽象层通过聚合关系完成了封装。
注意事项
- 使用时主要考虑如何拆分抽象和实现,并不是一涉及继承就要使用。桥接模式的意图主要是对变化的封装,尽量把可能变化的因素封装到最细、最小的逻辑单元中,避免风险扩散。因此在系统设计时,发现类的继承有N层时,可以考虑使用该模式。例如 Father 类有一个方法 A,Son 继承了这个方法,GrandSon 也继承了这个方法,那么 Son 则不能再随便修改这个方法了。
代码演示
1 | // 实现化角色 |