结构型设计模式

结构型设计模式是从程序的结构上解决模块之间的耦合问题。它包含适配器模式、代理模式、装饰模式、外观模式、桥接模式、组合模式和享元模式。

代理模式(委托模式)

在现实生活中用到类似代理模式的场景有很多,比如代理上网、打官司等。

定义:为其他对象提供一种代理以控制对这个对象的访问。

在代理模式中有如下角色

  • Subject:抽象主题类,声明真实主题与代理的共同接口方法。
  • RealSubject:真实主题类,代理类所代表的的真实主题。客户端通过代理类间接地调用真实主题类的方法。
  • Proxy:代理类,持有对真实主题类的引用,在其所实现的接口方法中调用真实主题类中相应的接口方法执行。
  • Client:客户端类。

代理模式的简单实现:比如代购

抽象主题类:具有真实主题类和代理的共同接口方法,此方法就是购买。

1
2
3
public interface IShop {
void buy();
}

真实主题类:购买者实现了 IShop 接口提供的方法。

1
2
3
4
5
6
7
public class LiuWangShu implements IShop {

@Override
public void buy() {
System.out.println("购买");
}
}

代理类:购买者找的代理类也要实现 IShop 接口,并且要持有被代理者,在 buy 方法中调用了被代理者的 buy 方法。

1
2
3
4
5
6
7
8
9
10
public class Purchasing implements IShop {
private IShop mShop;
public Purchasing(IShop mShop){
this.mShop = mShop;
}
@Override
public void buy() {
mShop.buy();
}
}

客户端类:由代理类包含了真实主题类(被代理者),最终调用的都是真实主题类实现的方法。

1
2
3
4
5
6
7
public class Client{
public static void main(String[] args){
IShop liuwangshu = new LiuWangShu();
IShop purchasing = new Purchasing(liuwangshu);
purchasing.buy();
}
}

动态代理的简单实现:

从编码的角度来说,代理模式分为静态代理和动态代理。上面的例子是静态代理,在代码运行前就已经存在了代理类的 class 编译文件。而动态代理则是在代码运行时通过反射来动态地生成代理类的对象,并确定到底来代理谁。也就是在编码阶段无需知道代理谁,代理谁将会在代码运行时决定。Java 提供了动态的代理接口 InvocationHandler,实现该接口需要重写 invoke()。接下来在上面静态代理的基础上,做些修改。

首先创建动态代理类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DynamicPurchasing implements InvocationHandler{
/**
* 声明了一个 Object 的引用,该引用指向被代理类。
*/
private Object obj;
public DynamicPurchasing(Object obj){
this.obj = obj;
}

/**
* 调用被代理类的具体方法在 invoke 方法中执行
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = method.invoke(obj,args);
if (method.getName().endsWith("buy")){
System.out.println("Liuwangshu 在买");
}
return result;
}
}

修改客户端类代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Client{
public static void main(String[] args){
// 创建 Liuwangshu
IShop liuwangshu = new LiuWangShu();
// 创建动态代理
DynamicPurchasing dynamicPurchasing = new DynamicPurchasing(liuwangshu);
// 创建 Liuwangshu 的 ClassLoader
ClassLoader loader = liuwangshu.getClass().getClassLoader();
// 动态创建代理类。调用 Proxy.newProxyInstance 来生成动态代理类。
IShop purchasing = (IShop) Proxy.newProxyInstance(loader,new Class[]{IShop.class},dynamicPurchasing);
// 调用 purchasing 的 buy 方法会调用 DynamicPurchasing 的 invoke 方法。
purchasing.buy();
}
}

代理模式的类型和优点

代理模式从编码的角度来说可分为静态代理和动态代理,而从适用范围来讲则可分为以下 4 中类型:

  • 远程代理:为一个对象在不同的地址空间提供局部代表,这样系统可以将 Server 部分的实现隐藏。
  • 虚拟代理:使用一个代理对象表示一个十分耗费资源的对象并在真正需要时才创建。
  • 安全代理:用来控制真实对象访问时的权限。一般用于真实对象有不同的访问权限时。
  • 智能指引:当调用真实的对象时,代理处理另外一些事,比如计算真实对象的引用计数,当该对象没有引用时,可以自动释放它;或者访问一个实际对象时,检查是否已经能够锁定它,以确保其它对象不能改变它。

代理模式的优点:

  • 真实主题类就是实现实际的的业务逻辑,不用关心其它非本职的工作。
  • 真实主题类随时都会发生变化,但是因为它实现了公共的接口,所以代理类可以不做任何修改就能够使用。

装饰模式

在不必改变类文件和使用继承的情况下,动态地扩展一个对象的功能,是继承的替代方案之一。它通过创建一个包装对象,也就是装饰来包裹真实的对象。

定义:动态地给一个对象添加一些额外的职责,就增加功能来说,装饰模式比生成子类更为灵活。

在装饰模式中有如下角色:

  • Component:抽象组件,可以是接口或是抽象类,被装饰的最原始的对象。
  • ConcreteComponent:组件具体实现类。Component 的具体实现类,被装饰的具体对象。
  • Decorator:抽象装饰者,从外类来拓展 Component 类的功能,但对于 Component 来说无需知道 Decorator 的存在。在它的属性中必然有一个 private 变量指向 Component 抽象组件。
  • ConcreteDecorator:装饰者的具体实现类。

装饰模式的简单实现:杨过本身会全真剑法,有两位武学前辈洪七公和欧阳锋分别向杨过传授过打狗棒法和蛤蟆功,这样杨过除了会全真剑法,还会打狗棒法和蛤蟆功。洪七公和欧阳锋就起到了 ”装饰“ 杨过的作用。

抽象组件:

1
2
3
4
5
6
7
8
9
10
/**
* 定义一个武侠的抽象类
*/
public abstract class Swordsman{

/**
* 使用武功的抽象方法
*/
public abstract void attackMagic();
}

组件具体实现类:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 被装饰的具体对象
* 在这里就是被教授武学的具体武侠,也就是杨过。
*/
public class YangGuo extends Swordsman{

@Override
public void attackMagic() {
// 杨过初始的武学是全真剑法
System.out.println("杨过使用全真剑法");
}
}

抽象装饰者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public abstract class Master extends Swordsman{
/**
* 抽象装饰者保持了一个对抽象组件的引用,方便调用被装饰对象中的方法。
* 在这个例子中就是武学前辈要持有武侠的引用,方便教授他武学并使他融会贯通。
*/
private Swordsman swordsman;
public Master(Swordsman swordsman){
this.swordsman = swordsman;
}

@Override
public void attackMagic() {
swordsman.attackMagic();
}
}

装饰者具体实现类:

这个例子中用两个装饰者具体实现类,分别是洪七公和欧阳锋,他们负责向杨过传授新的武功。

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 HongQiGong extends Master{

public HongQiGong(Swordsman swordsman) {
super(swordsman);
}

public void teachAttachMagic(){
System.out.println("洪七公教授打狗棒法");
System.out.println("杨过使用打狗棒法");
}

@Override
public void attackMagic() {
super.attackMagic();
teachAttachMagic();
}
}



public class OuYangFeng extends Master{

public OuYangFeng(Swordsman swordsman) {
super(swordsman);
}

public void teachAttachMagic(){
System.out.println("欧阳锋教授蛤蟆功");
System.out.println("杨过使用蛤蟆功");
}

@Override
public void attackMagic() {
super.attackMagic();
teachAttachMagic();
}
}

客户端调用:

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

public static void main(String[] args) {
// 创建杨过
YangGuo yangGuo = new YangGuo();
// 洪七公向杨过传授打狗棒法,杨过学会了打狗棒法。
HongQiGong hongQiGong = new HongQiGong(yangGuo);
hongQiGong.attackMagic();
// 欧阳锋向杨过传授蛤蟆功,杨过学会了蛤蟆功。
OuYangFeng ouYangFeng = new OuYangFeng(yangGuo);
ouYangFeng.attackMagic();
}
}

使用场景和优缺点

  • 使用场景:
    • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
    • 需要动态地给一个对象增加功能,这些功能可以动态地撤销。
    • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。
  • 优点:
    • 通过组合而非继承的方式,动态地扩展一个对象的功能,在运行时选择不同的装饰器,从而实现不同的行为。
    • 有效避免了使用继承的方式扩展对象功能而带来的灵活性差、子类无限制扩张的问题。
    • 具体组件类与具体装饰类可以独立变化,用户可以根据需要增加新的具体组件类和具体装饰类,在使用时再对其进行组合,原有代码无需改变,符合 “开放封闭原则”。
  • 缺点:
    • 因为所有对象均继承于 Component,所以如果 Component 内部结构发生变化,则不可避免地影响所有子类(装饰者和被装饰者)。如果基类改变,则势必影响对象的内部。
    • 比继承更加灵活机动的特性,也同时意味着装饰模式比继承更加易于出错,排错也很困难。对于多次装饰的对象,调试时寻找错误可能需要逐级排查,较为烦琐。所以,只在必要的时候使用装饰模式。
    • 装饰层数不能过多,否则会影响效率。

外观模式(门面模式)

当开发 Android 的时候,无论是做 SDK 开发还是封装 API,大多都会用到外观模式,它通过一个外观类使得整个系统的结构只有一个统一的高层接口,这样能降低用户的使用成本。

定义:要求一个子系统的外部与内部的通信必须通过一个统一的对象进行。此模式提供一个高层的接口,使得子系统更易于使用。

在外观模式中有如下角色:

  • Facade:外观类,知道哪些子系统类负责处理请求,将客户端的请求代理给适当的子系统对象。
  • Subsystem:子系统类,可以有一个或者多个子系统。实现子系统的功能,处理外观类指派的任务,注意子系统类不含有外观类的引用。

外观模式的简单实现

把武侠张无忌当做一个系统,张无忌作为一个武侠,他本身分为 3 个系统,分别是招式、内功和经脉。

子系统类:

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

public void TaiJiQuan(){
System.out.println("使用招式太极拳");
}
public void QiShangQuan(){
System.out.println("使用招式七伤拳");
}
public void ShengHuo(){
System.out.println("使用招式圣火令");
}
}

/**
* 子系统内功
*/
public class NeiGong{

public void JiuYang(){
System.out.println("使用九阳神功");
}
public void QianKun(){
System.out.println("使用乾坤大挪移");
}
}

/**
* 子系统经脉
*/
public class JingMai{

public void jingMai(){
System.out.println("开启经脉");
}
}

外观类:

这里的外观类就是张无忌,他负责将自己的招式、内功和经脉通过不同的情况合理地搭配运用并对外界隐藏。

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 ZhangWuJi{
private JingMai jingMai;
private ZhaoShi zhaoShi;
private NeiGong neiGong;

public ZhangWuJi() {
// 初始化外观类的同时将各个子系统类创建好
jingMai = new JingMai();
zhaoShi = new ZhaoShi();
neiGong = new NeiGong();
}

public void QianKun(){
// 使用乾坤大挪移
jingMai.jingMai(); // 开启经脉
neiGong.QianKun(); // 使用内功乾坤大挪移
}

public void QiShang(){
// 使用七伤拳
jingMai.jingMai(); // 开启经脉
neiGong.JiuYang(); // 使用内功九阳神功
zhaoShi.QiShangQuan(); // 使用招式七伤拳
}
}

客户端调用:

外观模式会使子系统的逻辑和交互隐藏起来,为用户提供一个高层次的接口,使得系统更加易用,同时也隐藏了具体的实现。这样即使具体的子系统发生了变化,用户也不会感知到。

1
2
3
4
5
6
7
8
9
10
public class Client{
public static void main(String[] args) {
ZhangWuJi zhangWuJi = new ZhangWuJi();
// 张无忌使用乾坤大挪移和七伤拳。
// 此时,比武对手并不知道张无忌本身运用了什么,同时张无忌也无需重新计划使用七伤拳时要怎么做(他早已计划好了),如果每次使用时都要计划怎么做很显然会增加成本并贻误战机。
// 另外,张无忌也可以改变自己的内功、招式和经脉,这些都是对比武对手有所隐瞒的。
zhangWuJi.qianKun();
zhangWuJi.qiShang();
}
}

使用场景和优缺点

  • 使用场景
    • 构建一个有层次结构的子系统时,使用外观模式定义子系统中每层的入口点。如果子系统之间是相互依赖的,则可以让其通过外观接口进行通信,减少子系统之间的依赖关系。
    • 子系统往往会因为不断地重构演化而变得越来越复杂,大多数的模式使用时也会产生很多很小的类,这给外部调用它们的用户程序带来了使用上的困难。我们可以使用外观类提供一个简单的接口,对外隐藏子系统的具体实现并隔离变化。
    • 当维护一个遗留的大型系统时,可能这个系统已经非常难以维护和拓展,但因为它含有重要的功能,所以新的需要必须依赖于它,这时可以使用外观类,为设计粗糙或者复杂的遗留代码提供一个简单的接口,让新系统和外观类交互,而外观类负责与遗留的代码进行交互。
  • 优点
    • 减少系统的相互依赖,所有的依赖都是对外观类的依赖,与子系统无关。
    • 对用户隐藏了子系统的具体实现,减少用户对子系统的耦合。这样即使具体的子系统发生了变化,用户也不会感知到。
    • 加强了安全性,子系统中的方法如果不在外观类中开通,就无法访问到子系统中的方法。
  • 缺点
    • 不符合开放封闭原则。如果业务出现变更,则可能要直接修改外观类。

享元模式

是池技术的重要实现方式。它可以减少应用程序创建的对象,降低程序内存的占用,提供程序的性能。

定义:使用共享对象有效地支持大量细粒度的对象。

要求细粒度对象,那么不可避免地使得对象数量多且性质相近。这些对象分为两个部分:

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

在享元模式中有如下角色:

  • Flyweight:抽象享元角色,同时定义出对象的外部状态和内部状态的接口或者实现。
  • ConcreteFlyweight:具体享元角色,实现抽象享元角色定义的业务。
  • FlyWeightFactory:享元工厂,负责管理对象池和创建享元对象。

享元模式的简单实现

某著名网上商城卖商品,如果每个用户下单都生成商品对象,这显然会耗费很多资源。如果赶上 “双 11”,那恐怖的订单量会生成很多商品对象,更何况商城卖的商品种类繁多,这样就极易产生 “Out Of Memory”。因此,采用享元模式来对商品的创建进行优化。

抽象享元角色:

1
2
3
4
5
6
7
8
9
/**
* 商品接口
*/
public interface IGoods{
/**
* 展示商品的价格
*/
public void showGoodsPrice(String name);
}

具体享元角色:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Goods implements IGoods{
private String name; // 名称。内部状态。
private String version; // 版本。外部状态。

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

@Override
public void showGoodsPrice(String name) {
if ("32G".endsWith(version)){
System.out.println("价格为 5199 元");
}else if ("128G".endsWith(version)){
System.out.println("价格为 5999 元");
}
}
}

享元工厂:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 享元工厂 GoodsFactory 用来创建 Goods 对象。
* 通过 Map 容器来存储 Goods 对象,将内部状态 name 作为 key,以便标识 Goods 对象。
*/
public class GoodsFactory{
private static Map<String,Goods> pool = new HashMap<>();
public static Goods getGoods(String name){
// 如果包含 key,则使用存储的对象。否则创建新对象,并放入容器中。
if (pool.containsKey(name)){
System.out.println("使用缓存,key 为:"+name);
return pool.get(name);
}else {
Goods goods = new Goods(name);
pool.put(name,goods);
System.out.println("创建商品,key 为:"+name);
return goods;
}
}
}

客户端调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Client{
public static void main(String[] args) {
Goods goods1 = GoodsFactory.getGoods("iphone7");
goods1.showGoodsPrice("32G");
Goods goods2 = GoodsFactory.getGoods("iphone7");
goods2.showGoodsPrice("32G");
Goods goods3 = GoodsFactory.getGoods("iphone7");
goods3.showGoodsPrice("128G");

// 执行结果:
// 创建商品,key 为:iphone7
// 使用缓存,key 为:iphone7
// 使用缓存,key 为:iphone7
}
}

使用场景

  • 系统中存在大量的相似对象。
  • 需要缓冲池的场景。

备注

参考资料:

Android 进阶之光