结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。
代理模式
代理模式在不改变原始类的情况下,通过添加代理类来给原始类添加附加功能。代理模式经常用在比如监控,统计,鉴权这样的非业务逻辑之中。同时RPC框架也可以看成一种代理模式,被称为远程代理,在使用RPC的时候,不需要考虑与服务器交互的细节,只关注业务逻辑即可。
代理模式通过针对每个类都创建一个代理类,具体实现代码如下所示:
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
| interface A { void doSomething(); }
class ANeedProxy implements A { @Override public void doSomething() { } }
class AProxy implements A { private final A a;
AProxy(A a) { this.a = a; }
@Override public void doSomething() { if (checkPermission()) { this.a.doSomething(); } else { throw new SecurityException("Forbidden"); } }
private boolean checkPermission() { return Math.random() < 0.5; }
public static void main(String[] args) { AProxy proxy = new AProxy(new ANeedProxy()); proxy.doSomething(); } }
|
在上面代码之中,代理类AProxy和原始类B都实现了A接口,在使用的时候将B对象使用构造函数传入AProxy中,然后调用原始类来执行代码,同时在调用代码之前检查相关的权限信息。
动态代理
在刚才实现的代理模式中,需要将原始类中的所有方法都重新实现一遍。同时如果有多个类需要使用代理,那么就需要写多个代理类,会导致类的个数不必要的增多,可以使用动态代理来解决这一个问题。Java本身就已经提供了动态代理的语法,使用jdk动态代理实现的代码如下:
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
| class ADynamicProxyHandler implements InvocationHandler { private final Object proxiedObject;
ADynamicProxyHandler(Object proxiedObject) { this.proxiedObject = proxiedObject; }
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (checkPermission()) { return method.invoke(proxiedObject, args); } else { throw new SecurityException("Forbidden"); } }
private boolean checkPermission() { return Math.random() < 0.5; }
public static void main(String[] args) { B b = new B(); ADynamicProxyHandler handler = new ADynamicProxyHandler(b); A a = (A) Proxy.newProxyInstance(b.getClass().getClassLoader(), b.getClass().getInterfaces(), handler); a.doSomething(); } }
|
动态代理十分有用,可以帮助在编写代码时减少模板代码,减少维护和开发的成本,Spring AOP的底层实现原理就是基于这样的动态代理的。
桥接模式
桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化,其思路是不要过度使用继承,而是优先拆分部分组件,通过组合的方式来进行扩展。桥接模式的目的是为了避免直接继承带来的子类爆炸。
例如如果对于图形类而言,有是什么形状这一维度,也有颜色这一维度,那么就可以使用桥接模式来编写代码。具体代码如下:
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
| abstract class Color protected constructor(protected val name: String) { abstract fun bepaint(shape: String) }
class Grey : Color("灰色") { override fun bepaint(shape: String) = println("灰色的$shape"); }
class Green : Color("绿色") { override fun bepaint(shape: String) = println("绿色的$shape"); }
abstract class Shape protected constructor(protected val color: Color) { abstract fun draw() }
class Circle(color: Color) : Shape(color) { override fun draw() = color.bepaint("正方形") }
class Rectangle(color: Color) : Shape(color) { override fun draw() = color.bepaint("长方形") }
fun main() { val white = Grey() val rectangle = Rectangle(white) rectangle.draw() }
|
在实际使用中,如果出现一个类出现多个可以单个可以单独变化的维度,那么可以使用桥接模式进行设计。
装饰器模式
装饰器模式能够在运行器动态的给某个对象增加功能,同时将核心功能与附加功能分开。Java标准库的IO库就是装饰器模式的经典案例。
例如我们需要给FileInputStream添加缓存功能以及GZIP解压缩功能,那么实现的代码如下:
1 2 3 4 5 6 7 8 9 10
| public class Decorator { public static void main(String[] args) throws IOException { InputStream in = new GZIPInputStream( new BufferedInputStream( new FileInputStream("a.txt") ) ); } }
|
这样的代码就是典型的装饰器模式的代码。通过查看源码可以看到GZIPInputStream,BufferedInputStream,FileInputStream都是InputStream的子类。在这个例子中,核心功能指的就是FileInputStream这个真正读取数据的源头,附加工作指的是缓冲和压缩这两个功能。如果我们仍然需要新增附加功能,就可以继承InputStream独立的进行扩展。
在装饰器模式中,装饰器类和原始类都继承自同样的父类,装饰器模式有个特点就是可以对原始类嵌套使用多个装饰器。
适配器模式
适配器模式用于将不兼容的接口转换为可兼容的接口,让原本不能一起工作的类一起工作。适配器模式可以通过继承以及组合的方式进行实现,通过继承来实现适配器模式的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| interface ITarget{ void fa(); } public class Adaptee { void f1(); }
public class Adaptor extends Adaptee implements ITarget{ @Override void fa(){ super.f1(); } }
|
在这段代码之中,ITarget表示要转化成的接口的定义,Adaptee是一组不兼容的ITarget接口的类,Adaptor将Adaptee转化成为符合ITarget接口定义的类。
在实际应用之中,适配器模式可以看作一种补救设计上缺陷的方法。例如Java中有很多日志框架例如log4j,logback,大部分的日志都提供了相似的功能但没有实现统一的接口。Slf4j提供了打印日志的统一接口规范,像log4j这样的日志框架需要将接口改为符合slf4j的接口规范,就使用到了适配器模式。
门面模式
门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。例如一个系统A,提供了a、b、c、d 四个接口。系统B需要调用a、b、d三个接口,利用门面模式,可以提供一个包含a、b、d三个接口调用的门面接口x,供B系统使用,这就是门面模式。
门面模式不仅让子系统更加的易用,有时还可以解决性能上的问题。例如客户端APP之前可能需要使用三次调用服务器端的接口才能获取到想要的数据,而使用门面模式之后,就可以只发送一次请求,提升了响应速度。
组合模式
组合模式将对象组织成为树形的结构,表示部分与整体之间的层次结构。例如对于文件系统,可以将文件和目录进行区分,定义成为File和Directory两个类,在下面的代码中实现了打印当前目录下递归打印文件的功能,就使用了组合模式。
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
| abstract class FileNode { protected final String path; protected FileNode(String path) { this.path = path; } abstract List<FileNode> list(); abstract String toTreeString(); }
class File extends FileNode { File(String path) { super(path); } @Override List<FileNode> list() { return Collections.emptyList(); }
@Override String toTreeString() { return super.path + "\n"; } }
class Directory extends FileNode { Directory(String path) { super(path); } @Override List<FileNode> list() { var file = new java.io.File(super.path); return Arrays.stream(file.listFiles()).map(f -> { if (f.isFile()) { return new File(f.getPath()); } else { return new Directory(f.getPath()); } }).collect(Collectors.toList()); } @Override String toTreeString() { StringBuilder ret = new StringBuilder(); ret.append(super.path).append("\n"); list().forEach(node -> ret.append(node.toTreeString())); return ret.toString(); } }
|
组合模式将一组对象抽象成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用场景必须要能够表示成为树形结构是其前提,因此其使用场景比较有局限性。
享元模式
享元模式指的是复用被共享的变量,其意图在于复用对象,节省内存。如果在一个系统中存在大量重复对象,且这些对象是不可变变量,就可以将其设计为享元,在内存中只保留一份实例,供多处代码引用。因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。
享元模式在Java标准库中有许多的的应用,例如Integer.valueOf这个静态工厂创建实例,当传入的int范围在-128到+127的范围之间时,会直接返回缓存的Integer实例:
1 2 3 4 5 6 7 8 9 10 11
| public class Main { public static void main(String[] args) { Integer n1 = Integer.valueOf(100); Integer n2 = Integer.valueOf(100); System.out.println(n1 == n2); Integer n3 = Integer.valueOf(200); Integer n4 = Integer.valueOf(200); System.out.println(n3 == n4); } }
|