设计模式:结构型模式

结构型模式主要涉及如何组合各种对象以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。

结构型模式有:

  • 适配器
  • 桥接
  • 组合
  • 装饰器
  • 外观
  • 享元
  • 代理

适配器 Adapter

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

适配器模式是Adapter,也称Wrapper,是指如果一个接口需要B接口,但是待传入的对象却是A接口,怎么办?

我们举个例子。如果去美国,我们随身带的电器是无法直接使用的,因为美国的插座标准和中国不同,所以,我们需要一个适配器:

比如我们原本有一个Task继承的callable接口,但是Thread并不接受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;
    }
}

我们可以使用adapter来实现

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);
        }
    }
}

编写一个Adapter的步骤如下:

  1. 实现目标接口,这里是Runnable
  2. 内部持有一个待转换接口的引用,这里是通过字段持有Callable接口;
  3. 在目标接口的实现方法内部,调用待转换接口的方法。

这样一来,Thread就可以接收这个RunnableAdapter,因为它实现了Runnable接口。Thread作为调用方,它会调用RunnableAdapterrun()方法,在这个run()方法内部,又调用了Callablecall()方法,相当于Thread通过一层转换,间接调用了Callablecall()方法。

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

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

注意到List<T> Arrays.asList(T[])就相当于一个转换器,它可以把数组转换为List

小结

Adapter模式可以将一个A接口转换为B接口,使得新的对象符合B接口规范。

编写Adapter实际上就是编写一个实现了B接口,并且内部持有A接口的类:

public BAdapter implements B {
    private A a;
    public BAdapter(A a) {
        this.a = a;
    }
    public void b() {
        a.a();
    }
}

在Adapter内部将B接口的调用“转换”为对A接口的调用。

只有A、B接口均为抽象接口时,才能非常简单地实现Adapter模式。

桥接模式 Barage

将抽象部分与它的实现部分分离,使它们都可以独立地变化。

桥接模式的定义非常玄乎,直接理解不太容易,所以我们还是举例子。

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

image-20210131105136537

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

在桥接模式中,首先把Car按品牌进行子类化,但是,每个品牌选择什么发动机,不再使用子类扩充,而是通过一个抽象的“修正”类,以组合的形式引入。我们来看看具体的实现。首先定义抽象类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();

使用桥接模式的好处在于,如果要增加一种引擎,只需要针对Engine派生一个新的子类,如果要增加一个品牌,只需要针对RefinedCar派生一个子类,任何RefinedCar的子类都可以和任何一种Engine自由组合,即一辆汽车的两个维度:品牌和引擎都可以独立地变化。

image-20210131111241852

桥接模式实现比较复杂,实际应用也非常少,但它提供的设计思想值得借鉴,即不要过度使用继承,而是优先拆分某些部件,使用组合的方式来扩展功能。

组合 Composite

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

组合模式(Composite)经常用于树形结构,为了简化代码,使用Composite可以把一个叶子节点与一个父节点统一起来处理。

我们来看一个具体的例子。在XML或HTML中,从根节点开始,每个节点都可能包含任意个其他节点,这些层层嵌套的节点就构成了一颗树。

要以树的结构表示XML,我们可以先抽象出节点类型Node

public interface Node {
    // 添加一个节点为子节点:
    Node add(Node node);
    // 获取子节点:
    List<Node> children();
    // 输出为XML:
    String toXml();
}

比如我们按照这个要求定义三种节点,一个是ElementNode一个是TextNode还有一个CommentNode

这三种节点按照下面要求定义

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();
    }
}
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());

最后输出结果

<school>
    <classA>
        Tom
        Alice
    </classA>
    <classB>
        Bob
        Grace
    	<!-- comment... -->
    </classB>
</school>

image-20210131112235817

小结

Composite模式使得叶子对象和容器对象具有一致性,从而形成统一的树形结构,并用一致的方式去处理它们。

装饰器 Decorator

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

装饰器(Decorator)模式,是一种在运行期动态给某个对象的实例增加功能的方法。我们在IO的Filter模式一节中其实已经讲过装饰器模式了。在Java标准库中,InputStream是抽象类,FileInputStreamServletInputStreamSocket.getInputStream()这些InputStream都是最终数据源。

现在,如果要给不同的最终数据源增加缓冲功能、计算签名功能、加密解密功能,那么,3个最终数据源、3种功能一共需要9个子类。如果继续增加最终数据源,或者增加新功能,子类会爆炸式增长,这种设计方式显然是不可取的。Decorator模式的目的就是把一个一个的附加功能,用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模式画出来如下:

image-20210131112841765

Decorator模式有什么好处?它实际上把核心功能和附加功能给分开了。核心功能指FileInputStream这些真正读数据的源头,附加功能指加缓冲、压缩、解密这些功能。如果我们要新增核心功能,就增加Component的子类,例如ByteInputStream。如果我们要增加附加功能,就增加Decorator的子类,例如CipherInputStream。两部分都可以独立地扩展,而具体如何附加功能,由调用方自由组合,从而极大地增强了灵活性。

实际例子

我们同样定义一个TextNode

public interface TextNode {
    // 设置text:
    void setText(String text);
    // 获取text:
    String getText();
}

对于核心节点,例如<span>,它需要从TextNode直接继承:

public class SpanNode implements TextNode {
    private String text;
    public void setText(String text) {
        this.text = text;
    }
    public String getText() {
        return "<span>" + text + "</span>";
    }
}

紧接着,为了实现Decorator模式,需要有一个抽象的Decorator类:

public abstract class NodeDecorator implements TextNode {
    protected final TextNode target;
    protected NodeDecorator(TextNode target) {
        this.target = target;
    }
    public void setText(String text) {
        this.target.setText(text);
    }
}

这个NodeDecorator类的核心是持有一个TextNode,即将要把功能附加到的TextNode实例。接下来就可以写一个加粗功能:

public class BoldDecorator extends NodeDecorator {
    public BoldDecorator(TextNode target) {
        super(target);
    }
    public String getText() {
        return "<b>" + target.getText() + "</b>";
    }
}

类似的,可以继续加ItalicDecoratorUnderlineDecorator等。客户端可以自由组合这些Decorator:

TextNode n1 = new SpanNode();
TextNode n2 = new BoldDecorator(new UnderlineDecorator(new SpanNode()));
TextNode n3 = new ItalicDecorator(new BoldDecorator(new SpanNode()));
n1.setText("Hello");
n2.setText("Decorated");
n3.setText("World");
System.out.println(n1.getText());
// 输出<span>Hello</span>

System.out.println(n2.getText());
// 输出<b><u><span>Decorated</span></u></b>

System.out.println(n3.getText());
// 输出<i><b><span>World</span></b></i>

小结

使用Decorator模式,可以独立增加核心功能,也可以独立增加附加功能,二者互不影响;

可以在运行期动态地给核心功能增加任意个附加功能。

外观 Facade

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

外观模式,即Facade,是一个比较简单的模式。它的基本思想如下:

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

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

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

以下是三个系统的接口:

// 工商注册:
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;
    }
}

这样,客户端只跟Facade打交道,一次完成公司注册的所有繁琐流程:

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

很多Web程序,内部有多个子系统提供服务,经常使用一个统一的Facade入口,例如一个RestApiController,使得外部用户调用的时候,只关心Facade提供的接口,不用管内部到底是哪个子系统处理的。

更复杂的Web程序,会有多个Web服务,这个时候,经常会使用一个统一的网关入口来自动转发到不同的Web服务,这种提供统一入口的网关就是Gateway,它本质上也是一个Facade,但可以附加一些用户认证、限流限速的额外服务。

小结

Facade模式是为了给客户端提供一个统一入口,并对外屏蔽内部子系统的调用细节。

享元 Flyweight

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

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

享元模式在Java标准库中有很多应用。我们知道,包装类型如ByteInteger都是不变类,因此,反复创建同一个值相同的包装类型是没有必要的。以Integer为例,如果我们通过Integer.valueOf()这个静态工厂方法创建Integer实例,当传入的int范围在-128~+127之间时,会直接返回缓存的Integer实例:

对于Byte来说,因为它一共只有256个状态,所以,通过Byte.valueOf()创建的Byte实例,全部都是缓存对象。因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。总是使用工厂方法而不是new操作符创建实例,可获得享元模式的好处。

在实际应用中,享元模式主要应用于缓存,即客户端如果重复请求某些对象,不必每次查询数据库或者读取文件,而是直接返回内存中缓存的数据。我们以Student为例,设计一个静态工厂方法,它在内部可以返回缓存的对象:

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;
    }
}

在实际应用中,我们经常使用成熟的缓存库,例如GuavaCache,因为它提供了最大缓存数量限制、定时过期等实用功能。

小结

享元模式的设计思想是尽量复用已创建的对象,常用于工厂方法内部的优化。

代理 Proxy

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

代理模式,即Proxy,它和Adapter模式很类似。我们先回顾Adapter模式,它用于把A接口转换为B接口:

public BAdapter implements B {
    private A a;
    public BAdapter(A a) {
        this.a = a;
    }
    public void b() {
        a.a();
    }
}

Proxy模式的代码如下

public AProxy implements A {
    private A a;
    public AProxy(A a) {
        this.a = a;
    }
    public void a() {
        this.a.a();
    }
}

看起来好像是一样的,但是不同的地方在这里 this.a.a();,看起来好像啥也没干,但是我们可以在代码里面加上权限检查的功能,这样我们就实现了权限检查:

public void a() {
    if (getCurrentUser().isRoot()) {
        this.a.a();
    } else {
        throw new SecurityException("Forbidden");
    }
}

有的童鞋会问,为啥不把权限检查的功能直接写到目标实例A的内部?

因为我们编写代码的原则有:

  • 职责清晰:一个类只负责一件事;
  • 易于测试:一次只测一个功能。

用Proxy实现这个权限检查,我们可以获得更清晰、更简洁的代码:

  • A接口:只定义接口;
  • ABusiness类:只实现A接口的业务逻辑;
  • APermissionProxy类:只实现A接口的权限检查代理。

如果我们希望编写其他类型的代理,可以继续增加类似ALogProxy,而不必对现有的A接口、ABusiness类进行修改。

实际上权限检查只是代理模式的一种应用。Proxy还广泛应用在:

远程代理

远程代理即Remote Proxy,本地的调用者持有的接口实际上是一个代理,这个代理负责把对接口的方法访问转换成远程调用,然后返回结果。Java内置的RMI机制就是一个完整的远程代理模式。

虚代理

虚代理即Virtual Proxy,它让调用者先持有一个代理对象,但真正的对象尚未创建。如果没有必要,这个真正的对象是不会被创建的,直到客户端需要真的必须调用时,才创建真正的对象。JDBC的连接池返回的JDBC连接(Connection对象)就可以是一个虚代理,即获取连接时根本没有任何实际的数据库连接,直到第一次执行JDBC查询或更新操作时,才真正创建实际的JDBC连接。

保护代理

保护代理即Protection Proxy,它用代理对象控制对原始对象的访问,常用于鉴权。

智能引用

智能引用即Smart Reference,它也是一种代理对象,如果有很多客户端对它进行访问,通过内部的计数器可以在外部调用者都不使用后自动释放它。

小结

代理模式通过封装一个已有接口,并向调用方返回相同的接口类型,能让调用方在不改变任何代码的前提下增强某些功能(例如,鉴权、延迟加载、连接池复用等)。

使用Proxy模式要求调用方持有接口,作为Proxy的类也必须实现相同的接口类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值