37.设计模式(2)结构型

1. 适配器

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

适配器模式是Adapter,也称Wrapper,是指如果一个接口需要B接口,但是待传入的对象却是A接口。
编写一个Adapter的步骤如下:

  • 实现目标接口,这里是Runnable
  • 内部持有一个待转换接口的引用,这里是通过字段持有Callable接口;
  • 在目标接口的实现方法内部,调用待转换接口的方法。
public class Task implements Callable<Long> {
    private long num;
    public Task(long num) {
        this.num = num;
    }
    public Long call() throws Exception {
        long r = 0;
        for (long n = 1; n <= this.num; n++) {
            r = r + n;
        }
        System.out.println("Result: " + r);
        return r;
    }
}

public class RunnableAdapter implements Runnable {
    // 引用待转换接口:
    private Callable<?> callable;
    public RunnableAdapter(Callable<?> callable) {
        this.callable = callable;
    }
    // 实现指定接口:
    public void run() {
        // 将指定接口调用委托给转换接口调用:
        try {
            callable.call();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Callable<Long> callable = new Task(123450000L);
Thread thread = new Thread(new RunnableAdapter(callable));
thread.start();

适配器模式在Java标准库中有广泛应用。比如我们持有数据类型是String[],但是需要List接口时,可以用一个Adapter

String[] exist = new String[] {"Good", "morning", "Bob", "and", "Alice"};
Set<String> set = new HashSet<>(Arrays.asList(exist));

2. 桥接

将抽象部分与它的实现部分分离,使它们都可以独立地变化。(为了避免直接继承带来的子类爆炸)

示例:假设某个汽车厂商生产三种品牌的汽车:Big、Tiny和Boss,每种品牌又可以选择燃油、纯电和混合动力。
如果用传统的继承来表示各个最终车型,一共有3个抽象类加9个最终子类:

                   ┌───────┐
                   │  Car  │
                   └───────┘
                       ▲
    ┌──────────────────┼───────────────────┐
    │                  │                   │
┌───────┐          ┌───────┐          ┌───────┐
│BigCar │          │TinyCar│          │BossCar│
└───────┘          └───────┘          └───────┘
    ▲                  ▲                  ▲
    │                  │                  │
    │ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
    ├─│  BigFuelCar   │├─│  TinyFuelCar  │├─│  BossFuelCar  │
    │ └───────────────┘│ └───────────────┘│ └───────────────┘
    │ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
    ├─│BigElectricCar │├─│TinyElectricCar│├─│BossElectricCar│
    │ └───────────────┘│ └───────────────┘│ └───────────────┘
    │ ┌───────────────┐│ ┌───────────────┐│ ┌───────────────┐
    └─│ BigHybridCar  │└─│ TinyHybridCar │└─│ BossHybridCar │
      └───────────────┘  └───────────────┘  └───────────────┘

如果要新增一个品牌,或者加一个新的引擎(比如核动力),那么子类的数量增长更快。
用桥接就可以避免因直接继承带来的子类爆炸。

  • 首先定义抽象类Car,它引用一个Engine
public abstract class Car {
    // 引用Engine:
    protected Engine engine;
    public Car(Engine engine) {
        this.engine = engine;
    }
    public abstract void drive();
}

Engine接口的定义如下:

public interface Engine {
    void start();
}
  • 在一个“修正”的抽象类RefinedCar中定义一些额外操作:
public abstract class RefinedCar extends Car {
    public RefinedCar(Engine engine) {
        super(engine);
    }
    public void drive() {
        this.engine.start();
        System.out.println("Drive " + getBrand() + " car...");
    }
    public abstract String getBrand();
}
  • 最终的不同品牌继承自RefinedCar,例如BossCar
public class BossCar extends RefinedCar {
    public BossCar(Engine engine) {
        super(engine);
    }
    public String getBrand() {
        return "Boss";
    }
}
  • 而针对每一种引擎,继承自Engine,例如HybridEngine
public class HybridEngine implements Engine {
    public void start() {
        System.out.println("Start Hybrid Engine...");
    }
}
  • 客户端通过自己选择一个品牌,再配合一种引擎,得到最终的Car:
RefinedCar car = new BossCar(new HybridEngine());
car.drive();

这样品牌和引擎都可以独立地变化。结构如下:

       ┌───────────┐
       │    Car    │
       └───────────┘
             ▲
             │
       ┌───────────┐       ┌─────────┐
       │RefinedCar │ ─ ─ ─>│ Engine  │
       └───────────┘       └─────────┘
             ▲                  ▲
    ┌────────┼────────┐         │ ┌──────────────┐
    │        │        │         ├─│  FuelEngine  │
┌───────┐┌───────┐┌───────┐     │ └──────────────┘
│BigCar ││TinyCar││BossCar│     │ ┌──────────────┐
└───────┘└───────┘└───────┘     ├─│ElectricEngine│
                                │ └──────────────┘
                                │ ┌──────────────┐
                                └─│ HybridEngine │
                                  └──────────────┘

3. 组合

将对象组合成树形结构以表示“部分-整体”的层次结构,使得用户对单个对象和组合对象的使用具有一致性。

组合模式(Composite)经常用于树形结构,为了简化代码,使用Composite可以把一个叶子节点与一个父节点统一起来处理。
举例:在XML或HTML中,从根节点开始,每个节点都可能包含任意个其他节点,这些层层嵌套的节点就构成了一颗树。

  • 先抽象出节点类型Node:
public interface Node {
    // 添加一个节点为子节点:
    Node add(Node node);
    // 获取子节点:
    List<Node> children();
    // 输出为XML:
    String toXml();
}
  • 对于一个<abc></abc>这样的节点,我们称之为ElementNode,它可以作为容器包含多个子节点:
public class ElementNode implements Node {
    private String name;
    private List<Node> list = new ArrayList<>();
    public ElementNode(String name) {
        this.name = name;
    }
    public Node add(Node node) {
        list.add(node);
        return this;
    }
    public List<Node> children() {
        return list;
    }
    public String toXml() {
        String start = "<" + name + ">\n";
        String end = "</" + name + ">\n";
        StringJoiner sj = new StringJoiner("", start, end);
        list.forEach(node -> {
            sj.add(node.toXml() + "\n");// 循环子阶段
        });
        return sj.toString();
    }
}
  • 对于普通文本,我们把它看作TextNode,它没有子节点:
public class TextNode implements Node {
    private String text;
    public TextNode(String text) {
        this.text = text;
    }
    public Node add(Node node) {
        throw new UnsupportedOperationException();
    }
    public List<Node> children() {
        return List.of();
    }
    public String toXml() {
        return text;
    }
}
  • 还可以有注释节点:
public class CommentNode implements Node {
    private String text;
    public CommentNode(String text) {
        this.text = text;
    }
    public Node add(Node node) {
        throw new UnsupportedOperationException();
    }
    public List<Node> children() {
        return List.of();
    }
    public String toXml() {
        return "<!-- " + text + " -->";
    }
}
  • 通过ElementNodeTextNodeCommentNode,我们就可以构造出一颗树:
Node root = new ElementNode("school");
root.add(new ElementNode("classA")
        .add(new TextNode("Tom"))
        .add(new TextNode("Alice")));
root.add(new ElementNode("classB")
        .add(new TextNode("Bob"))
        .add(new TextNode("Grace"))
        .add(new CommentNode("comment...")));
System.out.println(root.toXml());
  • 输出的XML如下
<school>
<classA>
Tom
Alice
</classA>
<classB>
Bob
Grace
<!-- comment... -->
</classB>
</school>

使用Composite模式时,需要先统一单个节点以及“容器”节点的接口:

             ┌───────────┐
             │   Node    │
             └───────────┘
                   ▲
      ┌────────────┼────────────┐
      │            │            │
┌───────────┐┌───────────┐┌───────────┐
│ElementNode││ TextNode  ││CommentNode│
└───────────┘└───────────┘└───────────┘

4. 装饰器

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

装饰器(Decorator)模式,是一种在运行期动态给某个对象的实例增加功能的方法。

在Java标准库中,InputStream是抽象类,FileInputStreamServletInputStreamSocket.getInputStream()这些InputStream都是最终数据源。
如果要给不同的最终数据源增加缓冲功能、计算签名功能、加密解密功能,那么,3个最终数据源、3种功能一共需要9个子类。如果继续增加最终数据源,或者增加新功能,子类会爆炸式增长,这种设计方式显然是不可取的。

  • FileInputStream增加缓冲和解压缩功能,用Decorator模式写出来如下:
// 创建原始的数据源:
InputStream fis = new FileInputStream("test.gz");
// 增加缓冲功能:
InputStream bis = new BufferedInputStream(fis);
// 增加解压缩功能:
InputStream gis = new GZIPInputStream(bis);

InputStream input = new GZIPInputStream( // 第二层装饰
                        new BufferedInputStream( // 第一层装饰
                            new FileInputStream("test.gz") // 核心功能
                        ));

观察BufferedInputStreamGZIPInputStream,它们实际上都是从FilterInputStream继承的,这个FilterInputStream就是一个抽象的Decorator。我们用图把Decorator模式画出来如下:

             ┌───────────┐
             │ Component │
             └───────────┘
                   ▲
      ┌────────────┼─────────────────┐
      │            │                 │
┌───────────┐┌───────────┐     ┌───────────┐
│ComponentA ││ComponentB │...  │ Decorator │
└───────────┘└───────────┘     └───────────┘
                                     ▲
                              ┌──────┴──────┐
                              │             │
                        ┌───────────┐ ┌───────────┐
                        │DecoratorA │ │DecoratorB │...
                        └───────────┘ └───────────┘

最顶层的Component是接口,对应到IO的就是InputStream这个抽象类。ComponentAComponentB是实际的子类,对应到IO的就是FileInputStreamServletInputStream这些数据源。Decorator是用于实现各个附加功能的抽象装饰器,对应到IO的就是FilterInputStream。而从Decorator派生的就是一个一个的装饰器,它们每个都有独立的功能,对应到IO的就是BufferedInputStreamGZIPInputStream等。

5. 外观

为子系统中的一组接口提供一个一致的界面。Facade模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

外观模式,即Facade,是一个比较简单的模式。它的基本思想如下:
如果客户端要跟许多子系统打交道,那么客户端需要了解各个子系统的接口,比较麻烦。如果有一个统一的“中介”,让客户端只跟中介打交道,中介再去跟各个子系统打交道,对客户端来说就比较简单。所以Facade就相当于搞了一个中介。

我们以注册公司为例,假设注册公司需要三步:

  • 向工商局申请公司营业执照;
  • 在银行开设账户;
  • 在税务局开设纳税号。

以下是三个系统的接口:

// 工商注册:
public class AdminOfIndustry {
    public Company register(String name) {
        ...
    }
}
// 银行开户:
public class Bank {
    public String openAccount(String companyId) {
        ...
    }
}
// 纳税登记:
public class Taxation {
    public String applyTaxCode(String companyId) {
        ...
    }
}

如果子系统比较复杂,并且客户对流程也不熟悉,那就把这些流程全部委托给中介:

public class Facade {
    public Company openCompany(String name) {
        Company c = this.admin.register(name);
        String bankAccount = this.bank.openAccount(c.getId());
        c.setBankAccount(bankAccount);
        String taxCode = this.taxation.applyTaxCode(c.getId());
        c.setTaxCode(taxCode);
        return c;
    }
}

Company c = facade.openCompany("Facade Software Ltd.");

6. 享元

运用共享技术有效地支持大量细粒度的对象。

享元(Flyweight)的核心思想很简单:如果一个对象实例一经创建就不可变,那么反复创建相同的实例就没有必要,直接向调用方返回一个共享的实例就行,这样即节省内存,又可以减少创建对象的过程,提高运行速度。

public class Student {
    // 持有缓存:
    private static final Map<String, Student> cache = new HashMap<>();
    // 静态工厂方法:
    public static Student create(int id, String name) {
        String key = id + "\n" + name;
        // 先查找缓存:
        Student std = cache.get(key);
        if (std == null) {
            // 未找到,创建新对象:
            System.out.println(String.format("create new Student(%s, %s)", id, name));
            std = new Student(id, name);
            // 放入缓存:
            cache.put(key, std);
        } else {
            // 缓存中存在:
            System.out.println(String.format("return cached Student(%s, %s)", std.id, std.name));
        }
        return std;
    }
    private final int id;
    private final String name;
    public Student(int id, String name) {
        this.id = id;
        this.name = name;
    }
}

Java的Integer.valueOf()Byte.valueOf()都是使用的享元模式。

7. 代理

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

代理模式,即Proxy,它和适配器(Adapter)模式很类似。

  • Adapter模式,它用于把A接口转换为B接口
  • Proxy模式,还是转换成A接口
public class AProxy implements A {
    private A a;
    public AProxy(A a) {
        this.a = a;
    }
    public void a() {
        if (getCurrentUser().isRoot()) {
            this.a.a();
        } else {
            throw new SecurityException("Forbidden");
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • """1.个性化消息: 将用户的姓名存到一个变量中,并向该用户显示一条消息。显示的消息应非常简单,如“Hello ...
    她即我命阅读 3,315评论 0 5
  • 为了让我有一个更快速、更精彩、更辉煌的成长,我将开始这段刻骨铭心的自我蜕变之旅!从今天开始,我将每天坚持阅...
    李薇帆阅读 1,958评论 0 3
  • 似乎最近一直都在路上,每次出来走的时候感受都会很不一样。 1、感恩一直遇到好心人,很幸运。在路上总是...
    时间里的花Lily阅读 1,412评论 0 2
  • 1、expected an indented block 冒号后面是要写上一定的内容的(新手容易遗忘这一点); 缩...
    庵下桃花仙阅读 550评论 0 1
  • 一、工具箱(多种工具共用一个快捷键的可同时按【Shift】加此快捷键选取)矩形、椭圆选框工具 【M】移动工具 【V...
    墨雅丫阅读 548评论 0 0