我们常把 23 种经典的设计模式分为三类:创建型、结构型、行为型。
我们知道,设计模式要干的事情就是解耦
- 创建型模式是将对象的创建和使用代码解耦
- 结构型模式是将不同功能代码解耦
- 行为型模式是将不同的行为代码解耦
代理模式
装饰器模式(java IO)
装饰器模式本质是通过组合来替代继承,解决继承关系过于复杂的问题。 它主要的作用是对原始类已有功能进行增强(而代理模式是添加新功能,这是二者的主要区别)
当继承关系过于复杂时,就可以考虑用装饰器模式去替换继承了。
举例理解
Java IO是典型的装饰器模式
InputStream in = new FileInputStream("/user/wangzheng/test.txt");
InputStream bin = new BufferedInputStream(in);
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
初看上面的代码,我们会觉得 Java IO 的用法比较麻烦,我在想,Java IO 为什么不设计一个继承
FileInputStream 并且支持缓存的 BufferedFileInputStream
类呢?
这样我们就可以像下面的代码中这样,直接创建一个 BufferedFileInputStream 类对象,打开文件读取数据,用起来岂不是更加简单?
InputStream bin = new BufferedFileInputStream("/user/wangzheng/test.txt");
byte[] data = new byte[128];
while (bin.read(data) != -1) {
//...
}
如果 InputStream 只有一个子类 FileInputStream 的话,那我们在 FileInputStream 基础之上,再设计一个孙子类 BufferedFileInputStream,也算是可以接受的,毕竟继承结构还算简单。但实际上,继承 InputStream 的子类有很多。我们需要给每一个 InputStream 的子类,再继续派生支持缓存读取的子类,就会使得类继承结构变得无比复杂,代码既不好扩展,也不好维护,而jdk采用组合的方式,虽然代码书写麻烦一点,但是类继承结构简单,便于代码的维护和扩展。
桥接模式
定义:
一个类存在多个独立变化的维度,我们通过组合的方式,让这两个或多个维度可以独立进行扩展。通过组合关系来替代继承,避免继承的层次过多导致混乱。
举例理解:
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 换成 oracle.jdbc.driver.OracleDriver 就可以了
适配器模式
将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作
举例理解:USB 转接头充当适配器
具体开发中常用到适配器模式的场景:
- 封装有缺陷的接口设计
- 统一多个类的接口设计
- 兼容老版本接口
1、封装有缺陷的接口设计
假设我们依赖的外部系统在接口设计方面有缺陷(比如包含大量静态方法),引入之后会影响到我们自身代码的可测试性。为了隔离设计上的缺陷,我们希望对外部系统提供的接口进行二次封装,抽象出更好的接口设计,这个时候就可以使用适配器模式了。
2、统一多个类的接口设计
某个功能的实现依赖多个外部类。通过适配器模式,将它们的接口适配为统一的接口定义,然后我们就可以使用多态的特性来复用代码逻辑。
3、 兼容老版本接口
在做版本升级的时候,对于一些要废弃的接口,我们不直接将其删除,而是暂时保留,并且标注为 deprecated
,并将内部实现逻辑委托为新的接口实现。这样做的好处是,让使用它的项目有个过渡期,而不是强制进行代码修改。
门面模式
为了保证接口的通用,我们需要将接口尽量设计得细粒度一点
,尽量职责单一。但是如果接口的粒度过小,在接口的使用者开发一个业务功能时,就会导致需要调用很多细粒度的接口才能完成。就很麻烦
相反,如果接口粒度设计得太大,一个接口返回 n 多数据,要做 n 多事情,就会导致接口不够通用。针对不同的调用者的业务需求,我们就需要开发不同的接口来满足,这就会导致系统的接口无限膨胀。
总之,接口的粒度过大过小都不好,怎么办?——门面模式
门面模式定义:为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。
理解:假设有一个系统 A,提供了 a、b、c、d 四个接口。系统 B 完成某个业务功能,需要调用 A 系统的 a、b、d 接口。利用门面模式,我们提供一个封装 a、b、d 接口调用的门面接口 x
,给系统 B 使用,系统B只需要调用x接口即可。
适配器 vs 门面
- 适配器是做接口转换,解决的是原接口和目标接口不匹配的问题。
- 门面模式做接口整合,解决的是多接口调用带来的问题。
组合模式(此组合非彼组合)
组合模式和“组合关系”(组合两个类,组合优于继承)中的"组合"不一样
组合模式常用来实现支持递归遍历的文件系统目录树结构:
组合模式将一组对象组织成树形结构
,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。
使用组合模式的前提在于,你的业务场景必须能够表示成树形结构,所以,组合模式的应用场景也比较局限,它并不是一种很常用的设计模式。
享元模式
享元模式的思想本质就是共享,比如java的Integer包装类,在数值小于128时就会采用享元模式返回内存中已存在的Integer对象,以达到节省内存的目的。
注意共享的对象是不可变对象,否则无法共享。
组合优于继承的设计原则
上面多种模式都是利用组合去替换继承,那组合到底比继承好在哪里呢?推荐看下面这篇文章:
组合优于继承