结构型设计模式汇总
结构型设计模式名称
结构型设计模式主要包括 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());
实现步骤
- 实现 java 中的 InvocationHandler 接口中的
invoke(Object proxy, Method method, Object[] args)
方法,在该方法中增加附加功能。并通过method.invoke(proxiedObject, args)
反射的方式,调用原始对象的方法。 - 通过 java 方法中的提供的 Proxy 类中的 newProxyInstance 方法来创建代理对象。参数分别为原始类的 classLoader 对象,原始类所实现的接口及 1 中 InvocationHandler 的实现。
- 调用代理对象时,需要将代理对象转换为对应的接口来进行实现。
java 动态代理局限
被代理类必须实现对应的接口,代理类只能为实现了接口的类进行代理。
1.4 静态代理存在的问题
- 需要在代理类中将原始类中的所有的方法,都实现一遍,并且每个方法都附加相似的逻辑。导致存在大量相似的重复代码。
- 如果需要添加功能的类有很多,就需要创建大量的代理类,类的数据会急剧膨胀,增加的类的维护成本。
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 telephones;
public TelephoneMsgSender(List 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 装饰器模式和组合优于继承中的组合方式有什么区别
- 装饰类和原始类继承同样的父类,这样可以对原始类“嵌套”多个装饰类。
// 这段代码既支持缓存,又支持按照基本数据类型读取数据。
InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();
- 装饰器类是对功能的增强,而不仅仅是通过组合方式降低代码的耦合性和复杂性。
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 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 stooges = Arrays.asList("Larry", "Moe", "Curly");
4.6 应用场景
适配器模式的应用场景是“接口不兼容”。
适配器模式可以看作一种“补偿模式”,用来补救设计上的缺陷。应用这种模式算是“无奈之举”。如果在设计初期,我们就能协调规避接口不兼容的问题,那这种模式就没有应用的机会了。
4.7 代理、桥接、装饰器、适配器 4 种设计模式的区别
代理模式:代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同。
桥接模式:桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变。
装饰器模式:装饰者模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用。
适配器模式:适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理模式、装饰器模式提供的都是跟原始类相同的接口。
5. 门面模式
5.1 定义
它为子系统中的一组接口提供一个统一的高层接口,使得子系统更容易使用。其应用场景非常明确,主要在接口设计方面使用。
5.2 作用
- 为外部访问提供统一的入口,屏蔽子系统实现的细节。提高子系统的易用性。
- 通过合理的接口分类,在满足业务基本需求的前提下,提高接口的复用性。
- 通过将多个接口进行有限合并,让业务可以在一次请求获得相应数据,避免因为多次请求导致响应慢的问题(比如:多次 App 请求后台),来提供系统的性能。
5.3 门面模式典型结构图
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 接口粒度问题
- 为了保证接口的可复用性,我们会尽可能将接口粒度设计得小一点,保证其职责单一。但是,粒度设计得过小,调用者实现一个功能需要调用 n 多个接口。对于调用者来说,显然会抱怨接口不好用。
- 如果一个接口粒度过大,一个接口返回 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 的响应时间变得较长。如果碰到网络不太好的情况,就会严重影响用户的使用体验。在这种情况下,对接口进行包裹后,只需要经过一次请求,减少了网络请求的次数,客户端的响应时间自然也就降低了。
解决易用性的例子
- Linux 系统调用函数。它是 Linux 操作系统暴露给开发者的一组“特殊”的编程接口,它封装了底层更加基础的 Linux 内核调用。
- Linux shell 命令。同样也是封装了系统调用,提供了更加易于使用的命令。
解决分布式事务问题的例子
金融系统中的“用户”和“钱包”两个业务领域模式(同一数据库对象)。
当在创建用户时,需要同时为用户创建一个钱包这样一个需要,需要保证两个操作要么都成功,要么都失败,这需要使用分布式事务保证一致性。
- 可以通过分布式事务框架或事后补偿机制来解决。
- 通过门面模式封装两个接口的操作,并提供一个统一的接口,利用数据库事务或者 Spring 框架提供的事务,在一个事务中执行两个 SQL 操作。
5.7 设计原则、思想与模式的关系
设计原则、设计思想和设计模式是同一个道理在不同角度的表述。
隐藏复杂性,提供易用接口
门面模式与迪米特原则和接口隔离原则很像,都是为了屏蔽系统的复杂性,让两个有依赖关系的系统保持有限的依赖关系。
同时,门面模式还有封装和抽象的设计思想,提供更加抽象的接口,封装底层实现细节。
6. 组合模式
6.1 定义
将一组对象组织成树形结构,以表示“部分-整体”的层次结构,组合模式可以让客户端(代码调用者)统一单个对象和组合对象的处理逻辑(递归遍历)。
6.2 作用
- 将业务场景抽象成树型结构,方便使用递归遍历来处理业务逻辑,降低业务实现的复杂度。
- 通过复用业务处理逻辑,来提高代码的复用性。
6.3 类结构图
6.4 经典实现(设计一个类来表示文件系统中的目录或文件)
public class FileSystemNode {
private String path;
private boolean isFile;
private List 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 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 与面向对象设计中的类之间依赖关系(组合)的区别
- 类之间的依赖关系(组合)是面向对象设计中类之间依赖关系的一种。
- 组合模式中的组合是指用来处理树形结构数据。
6.6 应用场景
公司组织结构
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 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 subDepartmentIds = departmentRepo.getSubDepartmentIds(department.getId());
for (Long subDepartmentId : subDepartmentIds) {
Department subDepartment = new Department(subDepartmentId);
department.addSubNode(subDepartment);
buildOrganization(subDepartment);
}
List employeeIds = employeeRepo.getDepartmentEmployeeIds(department.getId());
for (Long employeeId : employeeIds) {
double salary = employeeRepo.getEmployeeSalary(employeeId);
department.addSubNode(new Employee(employeeId, salary));
}
}
}
7. 享元模式
7.1 定义
主要通过复用对象来减少创建对象的数量,以减少内存占用及提高性能。前提是享元对象是不可变的。
为什么享元对象需要是不可变化的
主要是因为享元对象被多处使用,为了避免一处代码对享元对象进行修改后,影响到其它使用该享元的代码。
哪些对象可以设计为享元对象
- 相同对象
- 相似对象,将相似对象中的相同部分提取出来,设计成享元,让这些大量相似对象引用这些享元。
7.2 作用
- 通过复用对象,来达到节省内存,提高系统性能的目的。
7.3 类结构图
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 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 个对象的大小,大大减少了棋子的内存占用。
具体做法是:
- 先将公共对象存储在一个工厂类中,进行缓存
- 在棋子类中引用这个公共对象
- 创建棋子类对象时,通过从工厂类中获取到对应公共对象的缓存,来共用同一个不可变对象
// 享元类
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 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 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 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 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 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 层实现的。
说明
此文是根据王争设计模式之美相关专栏内容整理而来,非原创。