上一课时我们讲了单例模式的 8 种实现方式以及它的优缺点,可见设计模式的内容是非常丰富且非常有趣。我们在一些优秀的框架中都能找到设计模式的具体使用,比如前面 MyBatis 中(第 13 课时)讲的那些设计模式以及具体的使用场景,但由于设计模式的内容比较多,有些常用的设计模式在 MyBatis 课时中并没有讲到。因此本课时我们就以全局的视角,来重点学习一下这些常用设计模式。
我们本课时的面试题是,你知道哪些设计模式?它的使用场景有哪些?它们有哪些优缺点?
#### 典型回答
设计模式从大的维度来说,可以分为三大类:创建型模式、结构型模式及行为型模式,这三大类下又有很多小分类。
创建型模式是指提供了一种对象创建的功能,并把对象创建的过程进行封装隐藏,让使用者只关注具体的使用而并非对象的创建过程。它包含的设计模式有单例模式、工厂模式、抽象工厂模式、建造者模式及原型模式。
结构型模式关注的是对象的结构,它是使用组合的方式将类结合起来,从而可以用它来实现新的功能。它包含的设计模式是代理模式、组合模式、装饰模式及外观模式。
行为型模式关注的是对象的行为,它是把对象之间的关系进行梳理划分和归类。它包含的设计模式有模板方法模式、命令模式、策略模式和责任链模式。
下面我们来看看那些比较常见的设计模式的定义和具体的应用场景。
* [ ] 1. 单例模式
单例模式是指一个类在运行期间始终只有一个实例,我们把它称之为单例模式。
单例模式的典型应用场景是 Spring 中 Bean 实例,它默认就是 singleton 单例模式。
单例模式的优点很明显,可以有效地节约内存,并提高对象的访问速度,同时避免重复创建和销毁对象所带来的性能消耗,尤其是对频繁创建和销毁对象的业务场景来说优势更明显。然而单例模式一般不会实现接口,因此它的扩展性不是很好,并且单例模式违背了单一职责原则,因为单例类在一个方法中既创建了类又提供类对象的复合操作,这样就违背了单一职责原则,这也是单例模式的缺点所在。
* [ ] 2. 原型模式
原型模式属于创建型模式,它是指通过“克隆”来产生一个新的对象。所以它的核心方法是 clone(),我们通过该方法就可以复制出一个新的对象。
在 Java 语言中我们只需要实现 Cloneable 接口,并重写 clone() 方法就可以实现克隆了,实现代码如下:
```
public class CloneTest {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建一个新对象
People p1 = new People();
p1.setId(1);
p1.setName("Java");
// 克隆对象
People p2 = (People) p1.clone();
// 输出新对象的名称
System.out.println("People 2:" + p2.getName());
}
static class People implements Cloneable {
private Integer id;
private String name;
/**
* 重写 clone 方法
*/
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
```
程序的执行结果为:
```
People 2:Java
```
但需要注意的是,以上代码为浅克隆的实现方式,如果要实现深克隆(对所有属性无论是基本类型还是引用类型的克隆)可以通过以下手段实现:
*
所有对象都实现克隆方法;
* 通过构造方法实现深克隆;
* 使用 JDK 自带的字节流实现深克隆;
* 使用第三方工具实现深克隆,比如 Apache Commons Lang;
* 使用 JSON 工具类实现深克隆,比如 Gson、FastJSON 等。
具体的实现代码可以参考我们第 07 课时的内容。
原型模式的典型使用场景是 Java 语言中的 Object.clone() 方法,它的优点是性能比较高,因为它是通过直接拷贝内存中的二进制流实现的复制,因此具备很好的性能。它的缺点是在对象层级嵌套比较深时,复制的代码实现难度比较大。
* [ ] 3. 命令模式
命令模式属于行为模式的一种,它是指将一个请求封装成一个对象,并且提供命令的撤销和恢复功能。说得简单一点就是将发送者、接收者和调用命令封装成独立的对象,以供客户端来调用,它的具体实现代码如下。
接收者的示例代码:
```
// 接收者
class Receiver {
public void doSomething() {
System.out.println("执行业务逻辑");
}
}
```
命令对象的示例代码:
```
// 命令接口
interface Command {
void execute();
}
// 具体命令类
class ConcreteCommand implements Command {
private Receiver receiver;
public ConcreteCommand(Receiver receiver) {
this.receiver = receiver;
}
public void execute() {
this.receiver.doSomething();
}
}
```
请求者的示例代码:
```
// 请求者类
class Invoker {
// 持有命令对象
private Command command;
public Invoker(Command command) {
this.command = command;
}
// 请求方法
public void action() {
this.command.execute();
}
}
```
客户端的示例代码:
```
// 客户端
class Client {
public static void main(String[] args) {
// 创建接收者
Receiver receiver = new Receiver();
// 创建命令对象,设定接收者
Command command = new ConcreteCommand(receiver);
// 创建请求者,把命令对象设置进去
Invoker invoker = new Invoker(command);
// 执行方法
invoker.action();
}
}
```
Spring 框架中的 JdbcTemplate 使用的就是命令模式,它的优点是降低了系统的耦合度,新增的命令可以很容易地添加到系统中;其缺点是如果命令很多就会造成命令类的代码很长,增加了维护的复杂性。
考点分析
对于设计模式的掌握程度来说,一般面试官都不会要求你要精通所有的设计模式,但需要对几个比较常用的设计模式有所理解和掌握才行。本课时介绍了 3 种设计模式加上 MyBatis 那一课时介绍的 7 种设计模式,足以应对日常的工作和一般性面试了。
和此知识点相关的面试题还有,软件中的六大设计原则是什么?这也是面试中经常会问的面试题,同时也是优秀程序设计的指导思想。
#### 知识扩展:六大设计原则
六大设计原则包括:单一职责原则、里氏替换原则、依赖倒置原则、接口隔离原则、迪米特法则、开闭原则,接下来我们一一来看看它们分别是什么。
* [ ] 1. 单一职责原则
单一职责是指一个类只负责一个职责。比如现在比较流行的微服务,就是将之前很复杂耦合性很高的业务,分成多个独立的功能单一的简单接口,然后通过服务编排组装的方式实现不同的业务需求,而这种细粒度的独立接口就是符合单一职责原则的具体实践。
* [ ] 2. 开闭原则
开闭原则指的是对拓展开放、对修改关闭。它是说我们在实现一个新功能时,首先应该想到的是扩展原来的功能,而不是修改之前的功能。
这个设计思想非常重要,也是一名优秀工程师所必备的设计思想。至于为什么要这样做?其实非常简单,我们团队在开发后端接口时遵循的也是这个理念。
随着软件越做越大,对应的客户端版本也越来越多,而这些客户端都是安装在用户的手机上。因此我们不能保证所有用户手中的 App(客户端)都一直是最新版本的,并且也不能每次都强制用户进行升级或者是协助用户去升级,那么我们在开发新功能时,就强制要求团队人员不允许直接修改原来的老接口,而是要在原有的接口上进行扩展升级。
因为直接修改老接口带来的隐患是老版本的 App 将不能使用,这显然不符合我们的要求。那么此时在老接口上进行扩展无疑是最好的解决方案,因为这样我们既可以满足新业务也不用担心新加的代码会影响到老版本的使用。
* [ ] 3. 里氏替换原则
里氏替换原则是面向对象(OOP)编程的实现基础,它指的是所有引用了父类的地方都能被子类所替代,并且使用子类替代不会引发任何异常或者是错误的出现。
比如,如果把鸵鸟归为了“鸟”类,那么鸵鸟就是“鸟”的子类,但是鸟类会飞,而鸵鸟不会飞,那么鸵鸟就违背了里氏替换原则。
* [ ] 4. 依赖倒置原则
依赖倒置原则指的是要针对接口编程,而不是面向具体的实现编程。也就说高层模块不应该依赖底层模块,因为底层模块的职责通常更单一,不足以应对高层模块的变动,因此我们在实现时,应该依赖高层模块而非底层模块。
比如我们要从 A 地点去往 B 地点,此时应该掏出手机预约一个“车”,而这个“车”就是一个顶级的接口,它的实现类可以是各种各样的车,不同厂商的车甚至是不同颜色的车,而不应该依赖于某一个具体的车。例如,我们依赖某个车牌为 XXX 的车,那么一旦这辆车发生了故障或者这辆车正拉着其他乘客,就会对我的出行带来不便。所以我们应该依赖是“车”这一个顶级接口,而不是具体的某一辆车。
* [ ] 5. 接口隔离原则
接口隔离原则是指使用多个专门的接口比使用单一的总接口要好,即接口应该是相互隔离的小接口,而不是一个臃肿且庞杂的大接口。
使用接口隔离原则的好处是避免接口的污染,提高了程序的灵活性。
可以看出,接口隔离原则和单一职责原则的概念很像,单一职责原则要求接口的职责要单一,而接口隔离原则要求接口要尽量细化,二者虽然有异曲同工之妙,但可以看出单一职责原则要求的粒度更细。
* [ ] 6. 迪米特法则
迪米特法则又叫最少知识原则,它是指一个类对于其他类知道的越少越好。
迪米特法则设计的初衷是降低类之间的耦合,让每个类对其他类都不了解,因此每个类都在做自己的事情,这样就能降低类之间的耦合性。
这就好比我们在一些电视中看到的有些人在遇到强盗时,会选择闭着眼睛不看强盗,因为知道的信息越少反而对自己就越安全,这就是迪米特法则的基本思想。
#### 小结
本课时我们讲了 3 种设计模式:单例模式、原型模式和命令模式,结合 MyBatis 那一课时(第 13 课时)介绍的 7 种设计模式,足以应对日常的工作和一般性的面试了。最后我们还介绍了设计模式中的 6 大设计原则:单一职责原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则和迪米特法则,我们应该结合这些概念对照日常项目中的代码,看看还有哪些代码可以进行优化和改进。
- 前言
- 开篇词
- 开篇词:大厂技术面试“潜规则”
- 模块一:Java 基础
- 第01讲:String 的特点是什么?它有哪些重要的方法?
- 第02讲:HashMap 底层实现原理是什么?JDK8 做了哪些优化?
- 第03讲:线程的状态有哪些?它是如何工作的?
- 第04讲:详解 ThreadPoolExecutor 的参数含义及源码执行流程?
- 第05讲:synchronized 和 ReentrantLock 的实现原理是什么?它们有什么区别?
- 第06讲:谈谈你对锁的理解?如何手动模拟一个死锁?
- 第07讲:深克隆和浅克隆有什么区别?它的实现方式有哪些?
- 第08讲:动态代理是如何实现的?JDK Proxy 和 CGLib 有什么区别?
- 第09讲:如何实现本地缓存和分布式缓存?
- 第10讲:如何手写一个消息队列和延迟消息队列?
- 模块二:热门框架
- 第11讲:底层源码分析 Spring 的核心功能和执行流程?(上)
- 第12讲:底层源码分析 Spring 的核心功能和执行流程?(下)
- 第13讲:MyBatis 使用了哪些设计模式?在源码中是如何体现的?
- 第14讲:SpringBoot 有哪些优点?它和 Spring 有什么区别?
- 第15讲:MQ 有什么作用?你都用过哪些 MQ 中间件?
- 模块三:数据库相关
- 第16讲:MySQL 的运行机制是什么?它有哪些引擎?
- 第17讲:MySQL 的优化方案有哪些?
- 第18讲:关系型数据和文档型数据库有什么区别?
- 第19讲:Redis 的过期策略和内存淘汰机制有什么区别?
- 第20讲:Redis 怎样实现的分布式锁?
- 第21讲:Redis 中如何实现的消息队列?实现的方式有几种?
- 第22讲:Redis 是如何实现高可用的?
- 模块四:Java 进阶
- 第23讲:说一下 JVM 的内存布局和运行原理?
- 第24讲:垃圾回收算法有哪些?
- 第25讲:你用过哪些垃圾回收器?它们有什么区别?
- 第26讲:生产环境如何排除和优化 JVM?
- 第27讲:单例的实现方式有几种?它们有什么优缺点?
- 第28讲:你知道哪些设计模式?分别对应的应用场景有哪些?
- 第29讲:红黑树和平衡二叉树有什么区别?
- 第30讲:你知道哪些算法?讲一下它的内部实现过程?
- 模块五:加分项
- 第31讲:如何保证接口的幂等性?常见的实现方案有哪些?
- 第32讲:TCP 为什么需要三次握手?
- 第33讲:Nginx 的负载均衡模式有哪些?它的实现原理是什么?
- 第34讲:Docker 有什么优点?使用时需要注意什么问题?
- 彩蛋
- 彩蛋:如何提高面试成功率?