你知道哪些设计模式?分别对应的应用场景有哪些?
上一课时我们讲了单例模式的 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 大设计原则:单一职责原则、开闭原则、里氏替换原则、依赖倒置原则、接口隔离原则和迪米特法则,我们应该结合这些概念对照日常项目中的代码,看看还有哪些代码可以进行优化和改进。