结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。
代理模式 代理模式在不改变原始类的情况下,通过添加代理类来给原始类添加附加功能。代理模式经常用在比如监控,统计,鉴权这样的非业务逻辑之中。同时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); } }