【设计模式】结构型设计模式汇总

结构型设计模式汇总

结构型设计模式名称

结构型设计模式主要包括 7 大类:

  1. 代理模式
  2. 桥接模式
  3. 装饰器模式
  4. 适配器模式
  5. 门面模式
  6. 组合模式
  7. 享元模式

结构型设计模式作用

主要解决的是类或对象之间的组合问题。

1. 代理模式

1.1 定义

通过不改变原有类的情况下,通过新添加类并持有原有类的方式,给原始类附加功能。

1.2 代理模式的作用

为原始类添加功能

1.3 代理模式经典实现

1.3.1 静态代理

基于接口的实现

public interface IUserController {
    UserVo login(String telephone, String password);
    UserVo register(String telephone, String password);
}

public class UserController implements IUserController {
    //... 省略其他属性和方法... 
    @Override public UserVo login(String telephone, String password) {
        //... 省略 login 逻辑... //... 返回 UserVo 数据...
    }
    @Override public UserVo register(String telephone, String password) {
        //... 省略 register 逻辑... //... 返回 UserVo 数据...
    }
}

public class UserControllerProxy implements IUserController {
    private MetricsCollector metricsCollector;
    private UserController userController;
    public UserControllerProxy(UserController userController) {
        this.userController = userController;
        this.metricsCollector = new MetricsCollector();
    }
    @Override public UserVo login(String telephone, String password) {
        long startTimestamp = System.currentTimeMillis();
        // 委托 
        UserVo userVo = userController.login(telephone, password);
        long endTimeStamp = System.currentTimeMillis();
        long responseTime = endTimeStamp - startTimestamp;
        RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
        metricsCollector.recordRequest(requestInfo);
        return userVo;
    }
    @Override public UserVo register(String telephone, String password) {
        long startTimestamp = System.currentTimeMillis();
        UserVo userVo = userController.register(telephone, password);
        long endTimeStamp = System.currentTimeMillis();
        long responseTime = endTimeStamp - startTimestamp;
        RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
        metricsCollector.recordRequest(requestInfo);
        return userVo;
    }
}

//UserControllerProxy 使用举例 
// 因为原始类和代理类实现相同的接口,是基于接口而非实现编程 
// 将 UserController 类对象替换为 UserControllerProxy 类对象,不需要改动太多代码 
IUserController userController = new UserControllerProxy(new UserController());

基于继承的实现

应用场景:主要用于无法改变的外部类,如:依赖的第三方库,需要附加功能的情况。

public class UserControllerProxy extends UserController {
    private MetricsCollector metricsCollector;
    public UserControllerProxy() {
        this.metricsCollector = new MetricsCollector();
    }
    public UserVo login(String telephone, String password) {
        long startTimestamp = System.currentTimeMillis();
        UserVo userVo = super.login(telephone, password);
        long endTimeStamp = System.currentTimeMillis();
        long responseTime = endTimeStamp - startTimestamp;
        RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
        metricsCollector.recordRequest(requestInfo);
        return userVo;
    }
    public UserVo register(String telephone, String password) {
        long startTimestamp = System.currentTimeMillis();
        UserVo userVo = super.register(telephone, password);
        long endTimeStamp = System.currentTimeMillis();
        long responseTime = endTimeStamp - startTimestamp;
        RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
        metricsCollector.recordRequest(requestInfo);
        return userVo;
    }
}
//UserControllerProxy 使用举例 
UserController userController = new UserControllerProxy();

1.3.2 动态代理实现

public class MetricsCollectorProxy {
    private MetricsCollector metricsCollector;
    public MetricsCollectorProxy() {
        this.metricsCollector = new MetricsCollector();
    }
    public Object createProxy(Object proxiedObject) {
        Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
        DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
        return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
    }
    private class DynamicProxyHandler implements InvocationHandler {
        private Object proxiedObject;
        public DynamicProxyHandler(Object proxiedObject) {
            this.proxiedObject = proxiedObject;
        }
        @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            long startTimestamp = System.currentTimeMillis();
            Object result = method.invoke(proxiedObject, args);
            long endTimeStamp = System.currentTimeMillis();
            long responseTime = endTimeStamp - startTimestamp;
            String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
            RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);
            metricsCollector.recordRequest(requestInfo);
            return result;
        }
    }
}

//MetricsCollectorProxy 使用举例 
MetricsCollectorProxy proxy = new MetricsCollectorProxy(); 
IUserController userController = (IUserController) proxy.createProxy(new UserController());

实现步骤

  1. 实现 java 中的 InvocationHandler 接口中的 invoke(Object proxy, Method method, Object[] args) 方法,在该方法中增加附加功能。并通过 method.invoke(proxiedObject, args) 反射的方式,调用原始对象的方法。
  2. 通过 java 方法中的提供的 Proxy 类中的 newProxyInstance 方法来创建代理对象。参数分别为原始类的 classLoader 对象,原始类所实现的接口及 1 中 InvocationHandler 的实现。
  3. 调用代理对象时,需要将代理对象转换为对应的接口来进行实现。

java 动态代理局限

被代理类必须实现对应的接口,代理类只能为实现了接口的类进行代理。

1.4 静态代理存在的问题

  1. 需要在代理类中将原始类中的所有的方法,都实现一遍,并且每个方法都附加相似的逻辑。导致存在大量相似的重复代码。
  2. 如果需要添加功能的类有很多,就需要创建大量的代理类,类的数据会急剧膨胀,增加的类的维护成本。

1.5 为什么需要使用动态代理

使用动态代理不需要事先为每个原始类创建相应的代理类,而是在运行的时候,动态地为原始类创建对应的代理类,在系统中使用代理类来代替原始类来使用。调用方直接通过代理类来对相应的接口方法进行调用。

1.6 如何使用动态代理为未实现接口的类进行代理

基于继承实现的动态代理

CGLIB:CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。

CGLIB 原理

动态生成一个要代理类的子类,子类重写要代理的类的所有不是final的方法。在子类中采用方法拦截的技术拦截所有父类方法的调用,顺势织入横切逻辑。

CGLIB底层:使用字节码处理框架ASM,来转换字节码并生成新的类。不鼓励直接使用ASM,因为它要求你必须对JVM内部结构包括class文件的格式和指令集都很熟悉。

CGLIB 局限

对于final方法,无法进行代理。

1.7 应用场景

Spring 的 AOP

用户配置好需要给哪些类创建代理,并定义好在执行原始类的业务代码前后执行哪些附加功能。Spring 为这些类创建动态代理对象,并在 JVM 中替代原始类对象。原本在代码中执行的原始类的方法,被换作执行代理类的方法。

非业务功能开发

一般会在业务系统中,开发一些非功能性需求。比如:监控、统计、鉴权、限流、事务、幂等、日志。

好处

在不改变原始业务实现的基础上,将附加功能与业务功能解耦。

RPC框架的应用

RPC 框架可以被看作是一种代理模式。通过远程代理,将网络通信、信息编解码等细节隐藏起来。客户端在使用 RPC 接口的时候,就像使用本地接口一样,无需了解跟服务器的交互细节。

缓存的应用

如果是基于 Spring 框架来开发的话,那就可以在 AOP 切面中完成接口缓存的功能。在应用启动的时候,我们从配置文件中加载需要支持缓存的接口,以及相应的缓存策略(比如过期时间)等。当请求到来的时候,我们在 AOP 切面中拦截请求,如果请求中带有支持缓存的字段(比如 http://…?..&cached=true),我们便从缓存(内存缓存或者 Redis 缓存等)中获取数据直接返回。

2. 桥接模式

2.1 定义

GOF 解释

将抽象和实现解耦,让他们可以独立的变化。

JDBC 例子

JDBC 本身就相当于这里的抽象。这里的抽象并不是指的抽象类或者接口,而是根具体的数据库无关的,被抽象出来的一套“类库”。具体的 Driver 就相当于这里的实现。这里所说的实现,也并非指接口的实现类,而是跟具体数据库相关的一套“类库”。

JDBC 和 Driver 独立开发,通过对象之间的组合关系,组装在一起。

通俗解释

一个类存在两个或多个独立变化的维度,我们通过组合的方式,让这两个或多个维度可以独立进行扩展。

2.2 桥接模式的作用

将抽象和实现解耦,通过组合的方式来将对象组合在一起。这里的抽象不是抽象类或者接口,实现也不是抽象类或接口的实现,而是逻辑上更加广泛的抽象和实现。

2.3 桥接模式的经典实现

根据不同类型的告警规则,触发不同类型的告警

public interface MsgSender {
    void send(String message);
}
public class TelephoneMsgSender implements MsgSender {
    private List<String> telephones;
    public TelephoneMsgSender(List<String> telephones) {
        this.telephones = telephones;
    }
    @Override public void send(String message) {
        //...
    }
}
public class EmailMsgSender implements MsgSender {
    // 与 TelephoneMsgSender 代码结构类似,所以省略...
}
public class WechatMsgSender implements MsgSender {
    // 与 TelephoneMsgSender 代码结构类似,所以省略...
}
public abstract class Notification {
    protected MsgSender msgSender;
    public Notification(MsgSender msgSender) {
        this.msgSender = msgSender;
    }
    public abstract void notify(String message);
}
public class SevereNotification extends Notification {
    public SevereNotification(MsgSender msgSender) {
        super(msgSender);
    }
    @Override public void notify(String message) {
        msgSender.send(message);
    }
}
public class UrgencyNotification extends Notification {
    // 与 SevereNotification 代码结构类似,所以省略...
}
public class NormalNotification extends Notification {
    // 与 SevereNotification 代码结构类似,所以省略...
}
public class TrivialNotification extends Notification {
    // 与 SevereNotification 代码结构类似,所以省略...
}

我们将不同渠道的发送逻辑剥离出来,形成独立的消息发送类(MsgSender 相关类)。其中,Notification 类相当于抽象,MsgSender 类相当于实现,两者可以独立开发,通过组合关系(也就是桥梁)任意组合在一起。所谓任意组合的意思就是,不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,我们可以动态地去指定(比如,通过读取配置来获取对应关系)。

应用场景

利用 JDBC 驱动来查询数据库

Class.forName("com.mysql.jdbc.Driver");
// 加载及注册 JDBC 驱动程序 
String url = "jdbc:mysql://localhost:3306/sample_db?user=root&password=your_password";
Connection con = DriverManager.getConnection(url);
Statement stmt = con.createStatement(); String query = "select * from test";
ResultSet rs=stmt.executeQuery(query);
while(rs.next()) {
    rs.getString(1);
    rs.getint(2);
}

如果想要从 MySql 切换到 Oracle 数据库,只需要将 com.mysql.jdbc.Driver 替换成 com.mysql.jdbc.OracleDriver 就可以了。

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        }
        catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    /** * Construct a new driver and register it with DriverManager * @throws SQLException if a database error occurs. */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

从上面的代码可以看出,在调用 Class.forName(...) 的方法时,会执行上面的静态方法,并把当前 Driver 对象注册到 DriverManager 中,交由 DriverManager 来进行管理。注册到 DriverManager 之后,后续所有对 JDBC 接口的调用,都会委派到对具体的 Driver 实现类来执行。

3. 装饰器模式

3.1 定义

装饰器模式主要对现有的类对象进行包裹和封装,以期望在不改变类对象及其类定义的情况下,为对象添加额外功能。

3.2 装饰器模式的作用

装饰器模式主要解决继承关系过于复杂的问题,通过组合来替代继承。它主要的作用是给原始类添加增强功能。

3.3 装饰器模式的经典实现

通过组合模式简化继承实现的复杂性

public abstract class InputStream {
    //... 
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }
    public int read(byte b[], int off, int len) throws IOException {
        //...
    }
    public long skip(long n) throws IOException {
        //...
    }
    public int available() throws IOException {
        return 0;
    }
    public void close() throws IOException {
    }
    public synchronized void mark(int readlimit) {
    }
    public synchronized void reset() throws IOException {
        throw new IOException("mark/reset not supported");
    }
    public Boolean markSupported() {
        return false;
    }
}
public class BufferedInputStream extends InputStream {
    protected volatile InputStream in;
    protected BufferedInputStream(InputStream in) {
        this.in = in;
    }
    //... 实现基于缓存的读数据接口...
}
public class DataInputStream extends InputStream {
    protected volatile InputStream in;
    protected DataInputStream(InputStream in) {
        this.in = in;
    }
    //... 实现读取基本类型数据的接口
}

3.4 装饰器模式和组合优于继承中的组合方式有什么区别

  1. 装饰类和原始类继承同样的父类,这样可以对原始类“嵌套”多个装饰类。
// 这段代码既支持缓存,又支持按照基本数据类型读取数据。
InputStream in = new FileInputStream("/user/wangzheng/test.txt"); 
InputStream bin = new BufferedInputStream(in); 
DataInputStream din = new DataInputStream(bin); 
int data = din.readInt(); 
  1. 装饰器类是对功能的增强,而不仅仅是通过组合方式降低代码的耦合性和复杂性。

3.5 符合组合关系的常见结构型设计模式有哪些,及各自意图?

代理模式:附加和原始类无关的功能。

桥接模式:将抽象和实现解耦,让其可以独立变化。

装饰模式:附加对原始类相关功能的增强

3.6 Java IO 为什么没有设计一个继承 FileInputStream 并且支持缓存的 BufferedInputStream 类

功能扩展时,继承的层级太深。java 只支持单继承,如果我们需要设计一个既支持缓存,又支持按照基本数据读取数据的类,就需要创建一下 BufferedDataFileInputStream 类,而如果要创建这个类,继承关系是 InputStream -> FileInputStream -> DataFileInputStream -> BufferedDataFileInputStream。 明显可以看出,继承关系链太长了,如果多加几个功能,类继承结构结构变得无比复杂,代码既不好扩展,也不好维护。

3.7 装饰模式是如何实现嵌套功能增强的?

所有的装饰类和原始类都继承自同一个抽象类。每个装饰类在接收到其它装饰类或原始类对象的时候,都会对已有的方法进行功能的增强,并且在增加功能的过程中,会调用被装饰类的对象来完成功能。

3.8 应用场景

Java IO 相关操作类

主要分为 InputStream、OutputStream 和 Reader、Writer 两大类型。

4. 适配器模式

4.1 定义

它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。

适配器模式有两种实现方式:类适配器和对象适配器。其中,类适配器使用继承关系来实现,对象适配器使用组合关系来实现。

4.2 适配器模式的作用

主要作用是做接口兼容的工作,让原本由于接口不兼容而不能一起工作的类可以一起工作。

4.3 适配器模式经典实现

基于继承的类适配器

// 类适配器: 基于继承 
public interface ITarget {
    void f1();
    void f2();
    void fc();
}
public class Adaptee {
    public void fa() {
        //...
    }
    public void fb() {
        //...
    }
    public void fc() {
        //...
    }
}
public class Adaptor extends Adaptee implements ITarget {
    public void f1() {
        super.fa();
    }
    public void f2() {
        //... 重新实现 f2()...
    }
    // 这里 fc() 不需要实现,直接继承自 Adaptee,这是跟对象适配器最大的不同点
}

基于组合的对象适配器

// 对象适配器:基于组合 
public interface ITarget {
    void f1();
    void f2();
    void fc();
}
public class Adaptee {
    public void fa() {
        //...
    }
    public void fb() {
        //...
    }
    public void fc() {
        //...
    }
}
public class Adaptor implements ITarget {
    private Adaptee adaptee;
    public Adaptor(Adaptee adaptee) {
        this.adaptee = adaptee;
    }
    public void f1() {
        adaptee.fa();
        // 委托给 Adaptee
    }
    public void f2() {
        //... 重新实现 f2()...
    }
    public void fc() {
        adaptee.fc();
    }
}

两者的区别

继承实现和组合实现唯一的区别就是对于被适配的类,继承实现是直接继承了被适配类,而组合是通过构造方法注入的方式,来组合被适配类。

4.4 实际开发中如何选择使用哪种适配器实现呢?

  • 如果 Adaptee 接口并不多,两种方式都 OK
  • 如果 Adaptee 接口比较多,而且 Adaptee 和 ITarget 接口定义大部分都相同,那推荐使用类适配器。因为 Adaptor 复用父类 Adaptee 的接口,比起对象适配器的实现方式,Adaptor 的代码量要少一些。
  • 如果 Adaptee 接口很多,而且 Adaptee 和 ITarget 接口定义大部分都不相同,那我们推荐使用对象适配器,因为组合结构相对于继承更加灵活。

4.5 哪些情况下,会出现接口不兼容的情况

1. 封装有缺陷的接口设计

假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。

2. 统一多个类的接口设计

某个功能的实现依赖多个外部系统(或者说类)。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。

// 未使用适配器模式之前的代码:代码的可测试性、扩展性不好 
public class RiskManagement {
    private ASensitiveWordsFilter aFilter = new ASensitiveWordsFilter();
    private BSensitiveWordsFilter bFilter = new BSensitiveWordsFilter();
    private CSensitiveWordsFilter cFilter = new CSensitiveWordsFilter();
    public String filterSensitiveWords(String text) {
        String maskedText = aFilter.filterSexyWords(text);
        maskedText = aFilter.filterPoliticalWords(maskedText);
        maskedText = bFilter.filter(maskedText);
        maskedText = cFilter.filter(maskedText, "***");
        return maskedText;
    }
}


// 扩展性更好,更加符合开闭原则,如果添加一个新的敏感词过滤系统, 
// 这个类完全不需要改动;而且基于接口而非实现编程,代码的可测试性更好。 
public class RiskManagement {
    private List<ISensitiveWordsFilter> filters = new ArrayList<>();
    public void addSensitiveWordsFilter(ISensitiveWordsFilter filter) {
        filters.add(filter);
    }
    public String filterSensitiveWords(String text) {
        String maskedText = text;
        for (ISensitiveWordsFilter filter : filters) {
            maskedText = filter.filter(maskedText);
        }
        return maskedText;
    }
}

3. 替换依赖的外部系统

当我们把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动。

// 外部系统 A 
public interface IA {
    //... void fa();
}
public class A implements IA {
    //... 
    public void fa() {
        //...
    }
}
// 在我们的项目中,外部系统 A 的使用示例 
public class Demo {
    private IA a;
    public Demo(IA a) {
        this.a = a;
    }
    //...
}
Demo d = new Demo(new A());
// 将外部系统 A 替换成外部系统 B 
public class BAdaptor implemnts IA {
    private B b;
    public BAdaptor(B b) {
        this.b= b;
    }
    public void fa() {
        //... 
        b.fb();
    }
}
// 借助 BAdaptor,Demo 的代码中,调用 IA 接口的地方都无需改动, // 只需要将 BAdaptor 如下注入到 Demo 即可。 
Demo d = new Demo(new BAdaptor(new B()));

4. 兼容老版本接口

JDK1.0 中包含一个遍历集合容器的类 Enumeration。JDK2.0 对这个类进行了重构,将它改名为 Iterator 类,并且对它的代码实现做了优化。但是考虑到如果将 Enumeration 直接从 JDK2.0 中删除,那使用 JDK1.0 的项目如果切换到 JDK2.0,代码就会编译不通过。为了避免这种情况的发生,我们必须把项目中所有使用到 Enumeration 的地方,都修改为使用 Iterator 才行。

public class Collections {
    public static Emueration emumeration(final Collection c) {
        return new Enumeration() {
            Iterator i = c.iterator();
            public Boolean hasMoreElments() {
                return i.hashNext();
            }
            public Object nextElement() {
                return i.next():
            }
        }
    }
}

5. 适配不同格式的数据

适配器模式主要用于接口的适配,实际上,它还可以用在不同格式的数据之间的适配。比如,把从不同征信系统拉取的不同格式的征信数据,统一为相同的格式,以方便存储和使用。

List<String> stooges = Arrays.asList("Larry", "Moe", "Curly"); 

4.6 应用场景

适配器模式的应用场景是“接口不兼容”。

适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。

4.7 代理、桥接、装饰器、适配器 4 种设计模式的区别

代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。

桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。

装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。

适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。

5. 门面模式

5.1 定义

它为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用。其应用场景非常明确,主要在接口设计方面使用。

5.2 作用

  1. 为外部访问提供统一的入口,屏蔽子系统实现的细节。提高子系统的易用性。
  2. 通过合理的接口分类,在满足业务基本需求的前提下,提高接口的复用性。
  3. 通过将多个接口进行有限合并,让业务可以在一次请求获得相应数据,避免因为多次请求导致响应慢的问题(比如:多次 App 请求后台),来提供系统的性能。

5.3 门面模式典型结构图

image

5.4 经典实现

public interface Device {
    void open();
}

public class Door implements Device {
    @Override
    public void open() {
        System.out.println("门开了");
    }
}

public class Light implements Device {
    @Override
    public void open() {
        System.out.println("灯开了");
    }
}

public class Heater implements Device {
    @Override
    public void open() {
        System.out.println("热水器开了");
    }
}

public class Heater implements Device {
    @Override
    public void open() {
        System.out.println("热水器开了");
    }
}

public class Host {
    public static void main(String[] args) {
        Facade facade = new Facade();
        facade.open();
    }
}

// result
门开了
灯开了
热水器开了

5.5 接口粒度问题

  1. 为了保证接口的可复用性,我们会尽可能将接口粒度设计得小一点,保证其职责单一。但是,粒度设计得过小,调用者实现一个功能需要调用 n 多个接口。对于调用者来说,显然会抱怨接口不好用。
  2. 如果一个接口粒度过大,一个接口返回 n 多数据,需要做 n 多事情,就会导致接口不可复用,需要为不同的业务需求提供对应的接口,这样会导致接口无限膨胀。

5.6 典型应用场景

假设一个系统 A 提供 a,b,c,d 四个接口,系统 B 完成某个业务功能需要调用系统 A 的 a,b,c 接口。如果利用门面模式的话,会提供一个包裹了 a,b,c 三个接口的门面接口给系统 B 来进行调用。

为什么要要对接口进行包装而不是让系统 B 分别调用 a,b,c 三个接口呢?

假设系统 A 是服务端,系统 B 是 App 端。App 端每次接口请求都需要通过 http 访问网络的方式来与服务端进行通信。而网络通信一般都比较耗时,如果分别调用 a,b,c 三个接口,显然会使 App 的响应时间变得较长。如果碰到网络不太好的情况,就会严重影响用户的使用体验。在这种情况下,对接口进行包裹后,只需要经过一次请求,减少了网络请求的次数,客户端的响应时间自然也就降低了。

解决易用性的例子

  1. Linux 系统调用函数。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更加基础的 Linux 内核调用。
  2. Linux shell 命令。同样也是封装了系统调用,提供了更加易于使用的命令。

解决分布式事务问题的例子

金融系统中的“用户”和“钱包”两个业务领域模式(同一数据库对象)。

当在创建用户时,需要同时为用户创建一个钱包这样一个需要,需要保证两个操作要么都成功,要么都失败,这需要使用分布式事务保证一致性。

  1. 可以通过分布式事务框架或事后补偿机制来解决。
  2. 通过门面模式封装两个接口的操作,并提供一个统一的接口,利用数据库事务或者 Spring 框架提供的事务,在一个事务中执行两个 SQL 操作。

5.7 设计原则、思想与模式的关系

设计原则、设计思想和设计模式是同一个道理在不同角度的表述。

隐藏复杂性,提供易用接口

门面模式与迪米特原则和接口隔离原则很像,都是为了屏蔽系统的复杂性,让两个有依赖关系的系统保持有限的依赖关系。

同时,门面模式还有封装和抽象的设计思想,提供更加抽象的接口,封装底层实现细节。

6. 组合模式

6.1 定义

将一组对象组织成树形结构,以表示“部分-整体”的层次结构,组合模式可以让客户端(代码调用者)统一单个对象和组合对象的处理逻辑(递归遍历)。

6.2 作用

  1. 将业务场景抽象成树型结构,方便使用递归遍历来处理业务逻辑,降低业务实现的复杂度。
  2. 通过复用业务处理逻辑,来提高代码的复用性。

6.3 类结构图

image

6.4 经典实现(设计一个类来表示文件系统中的目录或文件)

public class FileSystemNode {
  private String path;
  private boolean isFile;
  private List<FileSystemNode> subNodes = new ArrayList<>();

  public FileSystemNode(String path, boolean isFile) {
    this.path = path;
    this.isFile = isFile;
  }

  public int countNumOfFiles() {
    if (isFile) {
      return 1;
    }
    int numOfFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      numOfFiles += fileOrDir.countNumOfFiles();
    }
    return numOfFiles;

  }

  public long countSizeOfFiles() {
    if (isFile) {
      File file = new File(path);
      if (!file.exists()) return 0;
      return file.length();
    }
    long sizeofFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      sizeofFiles += fileOrDir.countSizeOfFiles();
    }
    return sizeofFiles;

  }

  public String getPath() {
    return path;
  }

  public void addSubNode(FileSystemNode fileOrDir) {
    subNodes.add(fileOrDir);
  }

  public void removeSubNode(FileSystemNode fileOrDir) {
    int size = subNodes.size();
    int i = 0;
    for (; i < size; ++i) {
      if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
        break;
      }
    }
    if (i < size) {
      subNodes.remove(i);
    }
  }
}

改进版本(区分目录和文件)

public abstract class FileSystemNode {
  protected String path;

  public FileSystemNode(String path) {
    this.path = path;
  }

  public abstract int countNumOfFiles();
  public abstract long countSizeOfFiles();

  public String getPath() {
    return path;
  }
}

public class File extends FileSystemNode {
  public File(String path) {
    super(path);
  }

  @Override
  public int countNumOfFiles() {
    return 1;
  }

  @Override
  public long countSizeOfFiles() {
    java.io.File file = new java.io.File(path);
    if (!file.exists()) return 0;
    return file.length();
  }
}

public class Directory extends FileSystemNode {
  private List<FileSystemNode> subNodes = new ArrayList<>();

  public Directory(String path) {
    super(path);
  }

  @Override
  public int countNumOfFiles() {
    int numOfFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      numOfFiles += fileOrDir.countNumOfFiles();
    }
    return numOfFiles;
  }

  @Override
  public long countSizeOfFiles() {
    long sizeofFiles = 0;
    for (FileSystemNode fileOrDir : subNodes) {
      sizeofFiles += fileOrDir.countSizeOfFiles();
    }
    return sizeofFiles;
  }

  public void addSubNode(FileSystemNode fileOrDir) {
    subNodes.add(fileOrDir);
  }

  public void removeSubNode(FileSystemNode fileOrDir) {
    int size = subNodes.size();
    int i = 0;
    for (; i < size; ++i) {
      if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
        break;
      }
    }
    if (i < size) {
      subNodes.remove(i);
    }
  }
}

6.5 与面向对象设计中的类之间依赖关系(组合)的区别

  1. 类之间的依赖关系(组合)是面向对象设计中类之间依赖关系的一种。
  2. 组合模式中的组合是指用来处理树形结构数据。

6.6 应用场景

公司组织结构

image
public abstract class HumanResource {
  protected long id;
  protected double salary;

  public HumanResource(long id) {
    this.id = id;
  }

  public long getId() {
    return id;
  }

  public abstract double calculateSalary();
}

public class Employee extends HumanResource {
  public Employee(long id, double salary) {
    super(id);
    this.salary = salary;
  }

  @Override
  public double calculateSalary() {
    return salary;
  }
}

public class Department extends HumanResource {
  private List<HumanResource> subNodes = new ArrayList<>();

  public Department(long id) {
    super(id);
  }

  @Override
  public double calculateSalary() {
    double totalSalary = 0;
    for (HumanResource hr : subNodes) {
      totalSalary += hr.calculateSalary();
    }
    this.salary = totalSalary;
    return totalSalary;
  }

  public void addSubNode(HumanResource hr) {
    subNodes.add(hr);
  }
}

// 构建组织架构的代码
public class Demo {
  private static final long ORGANIZATION_ROOT_ID = 1001;
  private DepartmentRepo departmentRepo; // 依赖注入
  private EmployeeRepo employeeRepo; // 依赖注入

  public void buildOrganization() {
    Department rootDepartment = new Department(ORGANIZATION_ROOT_ID);
    buildOrganization(rootDepartment);
  }

  private void buildOrganization(Department department) {
    List<Long> subDepartmentIds = departmentRepo.getSubDepartmentIds(department.getId());
    for (Long subDepartmentId : subDepartmentIds) {
      Department subDepartment = new Department(subDepartmentId);
      department.addSubNode(subDepartment);
      buildOrganization(subDepartment);
    }
    List<Long> employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
    for (Long employeeId : employeeIds) {
      double salary = employeeRepo.getEmployeeSalary(employeeId);
      department.addSubNode(new Employee(employeeId, salary));
    }
  }
}

7. 享元模式

7.1 定义

主要通过复用对象来减少创建对象的数量,以减少内存占用及提高性能。前提是享元对象是不可变的。

为什么享元对象需要是不可变化的

主要是因为享元对象被多处使用,为了避免一处代码对享元对象进行修改后,影响到其它使用该享元的代码。

哪些对象可以设计为享元对象

  1. 相同对象
  2. 相似对象,将相似对象中的相同部分提取出来,设计成享元,让这些大量相似对象引用这些享元。

7.2 作用

  1. 通过复用对象,来达到节省内存,提高系统性能的目的。

7.3 类结构图

image

7.4 经典实现

主要是通过工厂模式,在工厂类中,通过一个 Map 或 List 来缓存已经创建好的享元对象,以达到复用的目的。

7.5 应用场景

棋局和棋牌

一个游戏厅有 n 个房间,每个房间有一个棋局,每个棋局对应 n 个棋子(包括棋子的类型、棋子的颜色、棋子的位置等)。利用这些信息就能显示一个完整的棋局。

public class ChessPiece {//棋子
  private int id;
  private String text;
  private Color color;
  private int positionX;
  private int positionY;

  public ChessPiece(int id, String text, Color color, int positionX, int positionY) {
    this.id = id;
    this.text = text;
    this.color = color;
    this.positionX = positionX;
    this.positionY = positionX;
  }

  public static enum Color {
    RED, BLACK
  }

  // ...省略其他属性和getter/setter方法...
}

public class ChessBoard {//棋局
  private Map<Integer, ChessPiece> chessPieces = new HashMap<>();

  public ChessBoard() {
    init();
  }

  private void init() {
    chessPieces.put(1, new ChessPiece(1, "車", ChessPiece.Color.BLACK, 0, 0));
    chessPieces.put(2, new ChessPiece(2,"馬", ChessPiece.Color.BLACK, 0, 1));
    //...省略摆放其他棋子的代码...
  }

  public void move(int chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}

存在问题

如果每个棋局都要存储 n 个完整的棋子对象,当棋局非常多(比如:上千万个),那么按照一个棋局 30 个棋子,一个棋子有 5 个对象(其中四个基本数据类型),这样子算下来的话就是:

总的内存占用大小 = 棋局数量 * 30 * 5 个对象所占用的内存大小

通过分析发现,所有的棋局中的棋子除了位置数据(positionX,positionY)是不一样的外,其它的数据(id,text,color)都是一样的。

解决方法

通过将相似对象中相同部分,如:id,text,color 属性拆分出来,设计成一个独立的类。作为享元对象提供给其它棋局使用,这样每个棋子的大小就从原来的 5 个对象的大小变成了 2 个对象的大小,大大减少了棋子的内存占用。

具体做法是:

  1. 先将公共对象存储在一个工厂类中,进行缓存
  2. 在棋子类中引用这个公共对象
  3. 创建棋子类对象时,通过从工厂类中获取到对应公共对象的缓存,来共用同一个不可变对象
// 享元类
public class ChessPieceUnit {
  private int id;
  private String text;
  private Color color;

  public ChessPieceUnit(int id, String text, Color color) {
    this.id = id;
    this.text = text;
    this.color = color;
  }

  public static enum Color {
    RED, BLACK
  }

  // ...省略其他属性和getter方法...
}

public class ChessPieceUnitFactory {
  private static final Map<Integer, ChessPieceUnit> pieces = new HashMap<>();

  static {
    pieces.put(1, new ChessPieceUnit(1, "車", ChessPieceUnit.Color.BLACK));
    pieces.put(2, new ChessPieceUnit(2,"馬", ChessPieceUnit.Color.BLACK));
    //...省略摆放其他棋子的代码...
  }

  public static ChessPieceUnit getChessPiece(int chessPieceId) {
    return pieces.get(chessPieceId);
  }
}

public class ChessPiece {
  private ChessPieceUnit chessPieceUnit;
  private int positionX;
  private int positionY;

  public ChessPiece(ChessPieceUnit unit, int positionX, int positionY) {
    this.chessPieceUnit = unit;
    this.positionX = positionX;
    this.positionY = positionY;
  }
  // 省略getter、setter方法
}

public class ChessBoard {
  private Map<Integer, ChessPiece> chessPieces = new HashMap<>();

  public ChessBoard() {
    init();
  }

  private void init() {
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(1), 0,0));
    chessPieces.put(1, new ChessPiece(
            ChessPieceUnitFactory.getChessPiece(2), 1,0));
    //...省略摆放其他棋子的代码...
  }

  public void move(int chessPieceId, int toPositionX, int toPositionY) {
    //...省略...
  }
}

通过共享 ChessPieceUnit 对象,比如有 30 万个棋局的话,原来没有使用享元对象时,需要创建 30 万个 ChessPieceUnit 对象。而现在只需要创建 30 个 ChessPieceUnit 对象。效果还是非常惊人的。

文本编辑器

将每个文字当作一个单独的对象,这个对象包含字体、大小、颜色和内容等 4 部分。

public class Character {//文字
  private char c;

  private Font font;
  private int size;
  private int colorRGB;

  public Character(char c, Font font, int size, int colorRGB) {
    this.c = c;
    this.font = font;
    this.size = size;
    this.colorRGB = colorRGB;
  }
}

public class Editor {
  private List<Character> chars = new ArrayList<>();

  public void appendCharacter(char c, Font font, int size, int colorRGB) {
    Character character = new Character(c, font, size, colorRGB);
    chars.add(character);
  }
}

存在问题

每敲一个字都会创建一个 Character 对象,如果一个文本文件有几十万行数据,每行有 30 个文字,那就是上千万个文字对象了,这样势必会浪费大量的内存。

解决方法

根据字体样式(字体、大小、颜色)的种类一般是有限的,我们将每次文字使用到的字体样式缓存起来,在添加文字时,通过字体样式去缓存中获取缓存的对象,来减少字体样式所占用的内存,达到节省内存的目的。

public class CharacterStyle {
  private Font font;
  private int size;
  private int colorRGB;

  public CharacterStyle(Font font, int size, int colorRGB) {
    this.font = font;
    this.size = size;
    this.colorRGB = colorRGB;
  }

  @Override
  public boolean equals(Object o) {
    CharacterStyle otherStyle = (CharacterStyle) o;
    return font.equals(otherStyle.font)
            && size == otherStyle.size
            && colorRGB == otherStyle.colorRGB;
  }
}

public class CharacterStyleFactory {
  private static final List<CharacterStyle> styles = new ArrayList<>();

  public static CharacterStyle getStyle(Font font, int size, int colorRGB) {
    CharacterStyle newStyle = new CharacterStyle(font, size, colorRGB);
    for (CharacterStyle style : styles) {
      if (style.equals(newStyle)) {
        return style;
      }
    }
    styles.add(newStyle);
    return newStyle;
  }
}

public class Character {
  private char c;
  private CharacterStyle style;

  public Character(char c, CharacterStyle style) {
    this.c = c;
    this.style = style;
  }
}

public class Editor {
  private List<Character> chars = new ArrayList<>();

  public void appendCharacter(char c, Font font, int size, int colorRGB) {
    Character character = new Character(c, CharacterStyleFactory.getStyle(font, size, colorRGB));
    chars.add(character);
  }
}

7.6 享元对象与单例、缓存和对象池的区别

跟单例的区别

单例模式中一个类只能创建一个对象,而享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。类似于单例的变种:多例。

设计目的:应用享元模式是为了对象复用,节省内存,而应用多例是为了限制对象的个数。

跟缓存的区别

一回事。都是缓存数据用于后期利用。

跟对象池的区别

都是复用,只是在复用的范围上有一些差别,对象池中的对象任一时刻只被一个使用者独占使用。而享元模式中的复用,则是可以被多处同时使用。

7.7 享元模式在 Java 中的应用

Java Integer

自动装箱所调用的方法:Integer.valueOf(value)

自动拆箱所调用的方法:Integer.intValue()

Java Integer 是通过 IntegerCache 来缓存 -128 到 127 所对应的 Integer 对象的。通过自动装箱的代码可以看出:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

这里的 IntegerCache 相当于享元模式实现中的工厂类。IntegerCache.cache[i + (-IntegerCache.low) 这里是为了获取当前传递进来的数据的实际 Integer 对象所存储的位置,由于 cache[] 数据下标为 0 的数据为 -128 这个 Integer 对象,所以,需要 - IntergerCache.low(-128)来定位当前值所对应 Integer 对象的存储位置。

为什么只存储 -128 到 127 之间的整形值呢?

因为缓存的享元对象是在类被加载的时候,一次性创建的。如果缓存整形值范围过大既占用存储,又使得 IntegerCache 对象加载的时间过长。所以,这里缓存了最常用的整形值,即一个字节的大小(-128 ~ 127)。

一个字节大小的范围为什么是 -128 ~ 127,而不是 0 ~ 255 呢?

这是由于 Java 中使用的是带符号整数,即最高位为符号位,当为 0 时,其值最大为 127,当为 1 时,其值最小为 -128。

其它整形数使用享元对象

整形数(Byte,Short,Long)都使用了享元对象来缓存常用的整形值对象。且缓存享元对象的大小都是一个字节的大小。

什么情况下,不会使用缓存的享元对象

我们知道,只有调用了 xxx.valueOf(value) 函数的赋值行为才会调用 IntegerCache 中的缓存享元对象。如果直接调用 Integer 的构造方法,是不会调用 xxx.valueOf(value) 函数的,也就不会使用缓存的享元对象,也就是每次创建都是一个新的对象。

Integer a = new Integer(2)

Java String

JVM 会单独开辟一块内存区域来存储字符串常量,这块存储区就是“常量池”。

String 的缓存享元对象的逻辑与 Integer 的类加载时就创建所有要缓存的整形值不同。因为我们事先并不知道需要缓存哪些字符串常量,所以,当某个字符串常量第一次被使用时,会直接存储在常量池中,当第二次再被使用的时候,就会直接拿到常量池中所对应的 String 对象。

当使用 String a = "abc" 这种方式创建字符串时,会调用 public native String intern();。是一个 native 方法。也就是 String 的享元对象的复用逻辑及其常量池都是在 C 层实现的。

说明

此文是根据王争设计模式之美相关专栏内容整理而来,非原创。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345