23种模式 - 结构型
结构型模式主要总结了一些类或对象组合在一起的经典结构。
代理模式
代理模式(Proxy Design Pattern)在不改变原始类(或被代理类)代码的情况下,通过引入代理类来给原始类附加功能。
原理与实现
一般情况下,让代理类和原始类实现相同的接口,如果原始类并没有定义接口,并且原始类代码并不在维护范围内,这种情况下,可以通过让代理类继承原始类的方法来实现代理模式
动态代理
静态代理需要针对每个类都创建一个代理类,并且每个代理类中的代码都有点像模板式的“重复”代码,增加了维护成本和开发成本。对于静态代码存在的问题,可以通过动态代理来解决。不事先为每个原始类编写代理类,而是在运行的时候动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。
代理模式的应用场景
- 业务系统的非功能性需求开发
在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志。将这些附加功能与业务功能解耦,放到代理类中统一处理,让开发人员只需要关注业务方面的开发。 - 代理模式在RPC、缓存中的应用
实际上,RPC框架也可以看作一种代理模式,通过远程代理,将网络通信、数据编解码等细节隐藏起来。
桥接模式
桥接模式(Bridge Design Pattern)有两种理解
- 将抽象和实现解耦,让他们可以独立变化。
定义中的“抽象”指的并非“抽象类”或“接口”,而是被抽象处来的一套“类库”,它只包含骨架代码,真正的业务逻辑需要委派给定义中的“实现”来完成。而定义中的“实现”,也并非“接口的实现类”,而是一套独立的“类库”。“抽象”和“实现”独立开发,通过对象之间的组合关系,组装在一起
// 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);
}
// 当执行 Class.forName("com.mysql.jdbc.Driver") 这条语句时,实际做了两件事
// 1. 要求 JVM 查找并加载指定的 Driver 类
// 2. 执行该类的静态代码,也就是将 MySQL Driver 注册到 DriverManager类中
package com.mysql.jdbc;
import java.sql.SQLException;
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()
}
}
- 一个类存在两个(或多个)独立变化的维度,通过组合的方式,让这两个(或多个)维度可以独立进行扩展
类似“组合优于继承”设计原则,通过组合关系来替代继承关系,避免继承层次的指数级爆炸
public enum NotificationEmergencyLevel {
SEVERE, URGENCY, NORMAL, TRIVIAL
}
public class Notification {
private List<String> emailAddresses;
private List<String> telephones;
private List<String> wechatIds;
public Notification() {}
public void setEmailAddress(List<String> emailAddress) {
this.emailAddresses = emailAddress;
}
public void setTelephones(List<String> telephones) {
this.telephones = telephones;
}
public void setWechatIds(List<String> wechatIds) {
this.wechatIds = wechatIds;
}
public void notify(NotificationEmergencyLevel level, String message) {
if (level.equals(NotificationEmergencyLevel.SEVERE)) {
//...自动语音电话
} else if (level.equals(NotificationEmergencyLevel.URGENCY)) {
//...发微信
} else if (level.equals(NotificationEmergencyLevel.NORMAL)) {
//...发邮件
} else if (level.equals(NotificationEmergencyLevel.TRIVIAL)) {
//...发邮件
}
}
}
//在API监控告警的例子中,我们如下方式来使用Notification类:
public class ErrorAlertHandler extends AlertHandler {
public ErrorAlertHandler(AlertRule rule, Notification notification){
super(rule, notification);
}
@Override
public void check(ApiStatInfo apiStatInfo) {
if (apiStatInfo.getErrorCount() > rule.getMatchedRule(apiStatInfo.getApi()).getMaxErrorCount()) {
notification.notify(NotificationEmergencyLevel.SEVERE, "...");
}
}
}
// 上面代码存在两个独立变化的维度:紧急程度 和 发送渠道
// 可以将不同渠道的发送逻辑剥离出来,形成独立的消息发送类。其中 Notification 类相当于抽象,MsgSender 类相当于实现,两者可以独立开发,通过组合关系任意组合在一起。
// 所谓任意组合的意思就是,不同紧急程度的消息和发送渠道之间的对应关系,不是在代码中固定写死的,可以动态地去指定(比如:通过读取配置来获取对应关系)
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代码结构类似,所以省略...
}
装饰器模式
装饰器模式(Decorator Design Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。
基于继承的设计方案
如果 InputStream 只有一个子类 FileInputStream 的话,那在 FileInputStream 基础上再设计一个子类 BufferedFileInputStream 也可以接受,毕竟继承结构还算简单。但实际上继承 InputStream 的子类很多,需要给每一个 InputStream 的子类再继续派生支持缓存读取的子类。除了支持缓存读取之外,如果还需要对功能进行其他方面的增强,比如支持按照基本数据类型读取等等,那就会导致组合爆炸,类继承结构变得无比复杂,代码既不好扩展,也不好维护。
基于继承设计方案
基于装饰器模式的设计方案
针对继承结构过于复杂的问题,可以通过将继承关系改为组合关系来解决。相对于简单的组合关系,装饰器有两个比较特殊的地方:
- 装饰器类和原始类继承同样的父类,这样可以对原始类“嵌套”多个装饰器类
FileInputStream 嵌套了两个装饰器类:BufferedInputStream 和 DataInputStream,让它既支持缓存读取,又支持按照基本数据类型来读取数据。
InputStream in = new FileInputStream("/user/test.txt");
InputStream bin = new BufferedInputStream(in);
DataInputStream din = new DataInputStream(bin);
int data = din.readInt();
- 装饰器类是对功能的增强,这也是装饰器模式应用场景的一个重要特点
代理模式和装饰器模式的区别在于:代理模式中,代理类附加的是跟原始类无关的功能,而在装饰器模式中,装饰器类附加的是跟原始类相关的增强功能。
适配器模式
适配器模式(Adapter Design Pattern)用来做适配的,它将不兼容的接口转化为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
实现方式
- 类适配器
类适配器使用继承关系来实现
// 类适配器: 基于继承
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();
}
}
应用场景
- 封装有缺陷的接口设计
依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入后会影响到自身代码的可测试性。为了隔离设计上的缺陷,可以采用适配器模式对外部系统提供的接口进行二次封装,抽象出更好的接口设计 - 统一多个类的接口设计
某个功能的实现依赖多个外部系统(或类),通过适配器模式,将它们的接口适配为统一的接口定义,然后就可以使用多态的特性来复用代码逻辑 - 替换依赖的外部系统
把项目中依赖的一个外部系统替换为另一个外部系统的时候,利用适配器模式,可以减少对代码的改动 - 兼容老版本接口
做版本升级的时候,对于一些要废弃的接口,通常不直接将其删除,而是暂时保留,并标注为 Deprecated,同时将内部实现逻辑委托为新的接口实现 - 适配不同格式的数据
除了用于接口的适配,它还可以用在不同格式的数据之间的适配。比如,把从不同征信系统拉取不同格式的征信数据,统一为相同格式方便存储和使用。再比如,Java 中的 Arrays.asList 也可以看作一种数据适配器。
代理、桥接、装饰器、适配器的区别
代理、桥接、装饰器、适配器这 4 种模式是比较常用的结构型设计模式。它们的代码结构非常相似,笼统来说,都可以称之为 Wrapper 模式,也就是通过 Wrapper 类二次封装原始类。
-
代理模式
代理模式在不改变原始类接口的条件下,为原始类定义一个代理类,主要目的是控制访问,而非加强功能,这是它跟装饰器模式最大的不同 -
桥接模式
桥接模式的目的是将接口部分和实现部分分离,从而让它们可以较为容易、也相对独立地加以改变 -
装饰器模式
装饰器模式在不改变原始类接口的情况下,对原始类功能进行增强,并且支持多个装饰器的嵌套使用 -
适配器模式
适配器模式是一种事后的补救策略。适配器提供跟原始类不同的接口,而代理、装饰器模式提供的都是跟原始类相同的接口
门面模式
门面模式(Facade Design Pattern)为子系统提供一组统一的接口,定义一组高层接口让子系统更易用
应用场景
-
解决易用性问题
门面模式可以用来封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。 -
解决性能问题
通过将多个接口调用替换为一个门面接口调用,减少网络通信成本,提高 APP 客户端的响应速度。
如果门面接口不多,可以将它跟非门面接口放在一起,也不需要特殊标记;如果门面接口很多,可以在已有的接口上,再重新抽象出一层,专门防止门面接口,从类、包的命名上跟原来的接口层做区分;如果门面接口特别多,并且很多都是跨多个子系统的,可以将门面接口放到一个新的子系统中。 -
解决分布式事务问题
要支持两个接口调用在一个事务中执行,是比较难实现的,这涉及分布式事务问题。可以借鉴门面模式的思想,再设计一个包裹这两个操作的新接口,让新接口在一个事务中执行这两个接口的业务逻辑。
类、模块、系统之间的“通信”,一般都是通过接口调用来完成的。接口设计的好坏直接影响到类、模块、系统是否好用。接口粒度设计得太大,太小都不好。太大会导致接口不可复用,太小会导致接口不易用。在实际开发中,接口的复用性和易用性需要“微妙”的权衡。通常处理原则是:尽量保持接口的可复用性,但针对特殊情况,允许提供冗余的门面接口,来提供更易用的接口。
组合模式
组合模式(Composite Design Pattern)将一组对象组织(Compose)成树形结构,以表示一种“部分-整体”的层次结构。组合让客户端(使用者)可以统一单个对象和组合对象的处理逻辑。
组合模式的设计思想,与其说是一种设计模式,倒不如说是对业务场景的一种数据结构和算法的抽象。其中,数据可以表示成数这种数据结构,业务需求可以通过树上的递归算法来实现。
组合模式将一组对象组织成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且利用它树形结构的特点,递归地处理每个子树,依次简化代码实现。使用组合模式的前提在于,业务场景必须能够表示成树形结构。所以组合模式的应用场景比较局限。
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);
}
}
}
享元模式
享元模式(Flyweight Design Pattern)顾名思义是被共享的单元。享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象。
一个系统中存在大量重复对象的时候,就可以利用享元模式,将对象设计成享元,在内存中只保留一份实例,供多处代码引用,这样可以减少内存中对象的数量,以起到节省内存的目的。实际上不仅仅相同对象可以设计成享元,对于相似对象,也可以将这些对象中相同的部分(字段),提取出来设计成享元,让这些大量相似对象引用这些享元。
实现
享元模式的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 或 List 来缓存已经创建好的享元对象,以达到复用的目的。
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 对象保存到 chars 数组中。如果有成千上万文字,会很耗内存
// 实际上,在一个文本文件中,用到的字体格式不会太多,所以对于字体格式,可以将它设计成享元,让不同的文字共享使用
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);
}
}
享元模式 VS 单例、缓存、对象池
区别两种设计模式,不能光看代码实现,而是要看设计意图:享元模式是为了实现对象复用,节省内存;单例模式是为了保证对象全局唯一;缓存是为了提高访问效率;池化技术中的“复用”理解为“重复使用”,主要是为了节省时间