在常规的软件开发过程中,我们常常会遇到需要依据不同情况采取相应措施的情景,这导致代码中充斥着大量的 if/else 结构。这种情况不仅影响了代码的易读性和可维护性,还使得未来的扩展变得更加困难。
本文将探讨四种精巧的设计模式来改善这种“条件爆炸”现象:
- 策略模式
1. 策略模式
01 概念
首先,我们先了解一下策略模式的定义。
策略模式(Strategy Pattern)属于行为型设计模式的一种,它定义了一组算法,并将每个算法封装起来,让它们可以互相替换。通过这种方式,策略模式使算法能够独立于使用它们的客户端进行变化。
如何理解策略模式?
在软件开发过程中,实现某个功能往往存在多种算法或方法,我们根据不同的环境或条件选择最合适的算法来完成任务。这种选择过程听起来就像 if...else... 的逻辑判断:
[此处为图片1]
对于初学者来说,这样的实现方式是完全可以接受的,只要程序能够正常运行就足够了。但随着技术水平的提升,这段代码显然违背了面向对象编程(OOP)中的两个基本准则:
- 单一职责原则:一个类或模块应该只承担一种责任。
- 开闭原则:软件实体(如模块、类和方法等)应当“对扩展开放,对修改封闭”。
由于违反了这两个准则,当 if-else 结构中的逻辑变得越来越复杂时,代码将更加难以管理和维护,并且非常容易出错。
策略模式的解决方案是:
定义一系列独立的类来封装不同的算法,每个类专门负责一种具体的算法。通常会有一个抽象的策略类作为所有具体算法的基础接口,确保所有的策略具有一致性。
这一设计方法涉及三个主要角色:
- Context: 环境类
- Strategy: 抽象策略类
- ConcreteStrategy: 具体策略类
[此处为图片2]
接下来,我们将通过一个简单的代码示例来演示这一过程。
02 例子
步骤 1:创建接口
public interface Strategy {
public int doOperation(int num1, int num2);
}
步骤 2:创建实现相同接口的具体类
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
public class OperationMultiply implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 * num2;
}
}
步骤 3:创建上下文类
public class Context {
private Strategy strategy;
public void setStrategy(Strategy strategy) {
this.strategy = strategy;
}
public int executeStrategy(int num1, int num2) {
return strategy.doOperation(num1, num2);
}
}
步骤 4:使用上下文来观察策略改变时的行为变化
public static void main(String[] args) {
Context context = new Context();
context.setStrategy(new OperationAdd());
System.out.println("10 + 5 = " + context.executeStrategy(10, 5));
context.setStrategy(new OperationSubtract());
System.out.println("10 - 5 = " + context.executeStrategy(10, 5));
}
context.setStrategy(new OperationMultiply());
System.out.println("10 * 5 = " + context.executeStrategy(10, 5));
}
执行结果:
从上面的例子,我们简要总结下策略模式的优缺点:
1、优点
- 策略模式提供了对“开闭原则”的理想支持,用户可以在不修改原有系统的基础上选择算法或行为,也能灵活地增加新的算法或行为。
- 策略模式提供了一种管理相关的算法家族的方法。
- 使用策略模式可以避免使用复杂的条件转移语句。
2、缺点
- 客户端必须了解所有的策略类,并自行决定采用哪一个策略类。
- 策略模式会导致产生许多策略类,但可以通过应用享元模式在一定程度上减少对象的数量。
2 SPI 机制
01 概念
SPI 的全称是 Service Provider Interface,是一种服务发现机制。其核心是在文件中配置接口实现类的完整限定名,并通过服务加载器读取这些配置文件来加载实现类。这使得在运行时可以动态地为接口更换实现类。因此,我们可以通过 SPI 机制轻松地扩展程序功能。
02 Java SPI : JDBC Driver
在JDBC4.0之前,我们在开发中连接数据库时,通常首先需要加载相关的数据库驱动,然后执行获取连接等操作。
// STEP 1: 注册 JDBC 驱动
Class.forName("com.mysql.jdbc.Driver");
// STEP 2: 建立连接
String url = "jdbc:xxxx://xxxx:xxxx/xxxx";
Connection conn = DriverManager.getConnection(url, username, password);
自JDBC4.0起,Java引入了SPI扩展机制,不再需要通过 Class.forName("com.mysql.jdbc.Driver") 来手动加载驱动,而是可以直接获取 JDBC 连接。
接下来,我们探讨一下如何加载 MySQL JDBC 8.0.22 驱动:
首先,DriverManager 类作为驱动管理器,是驱动加载的入口。
/**
* 加载初始的 JDBC 驱动通过检查系统属性 jdbc.properties 并使用 {@code ServiceLoader}机制
*/
static {
loadInitialDrivers();
println("JDBC DriverManager 初始化完成");
}
在 Java 中,static 块用于静态初始化,在类加载到Java虚拟机时执行。它负责实例化驱动。
我们关注一下 loadInitialDrivers 方法的实现。
加载驱动的过程包括四个步骤:
- 从系统属性中获取与驱动相关的定义。
- 利用 SPI 获取驱动的具体实现(以字符串形式)。
- 遍历通过SPI获得的所有具体实现,实例化每个实现类。
- 根据第一步获取的驱动列表来创建具体的实现。
我们特别关注 SPI 的使用方式,首先看第二步,利用 SPI 获取驱动的具体实现,对应的代码如下:
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
这一过程并没有直接从 META-INF/services 目录下查找配置文件或加载具体的实现类,而是封装了接口类型和类加载器,并初始化了一个迭代器。
接着看第三步,遍历通过SPI获得的所有具体实现并实例化每个实现类,相应的代码如下:
Iterator driversIterator = loadedDrivers.iterator();
// 遍历所有驱动的实现
while (driversIterator.hasNext()) {
driversIterator.next();
}
在遍历时,首先调用 driversIterator.hasNext() 方法,这一步会搜索 classpath 下以及 jar 包中所有的 META-INF/services 目录下的 java.sql.Driver 文件,并找出文件中的实现类名。此时并未实例化具体的实现类。
然后是调用 driversIterator.next() 方法,这时根据驱动名称具体实例化各个实现类,现在驱动已经被找到并实例化。
这里有一个小问题:如果发现了多个 driver 的话,应该如何选择一个特定的实现呢?
那就是 JDBC URL 了,驱动程序的实现有一个约定,如果驱动根据 JDBC URL 判断不是自己可以管理的连接就直接返回空。DriverManager 基于这个约定直到找到第一个不返回 null 的连接为止。
JDBC 驱动是 Java SPI 机制的一个非常典型的应用场景,包含三个关键步骤: 定义 SPI 文件 ,指定 Driver 类全限定名; 通过 DriverManager 静态类自动加载驱动; 加载之后,当需要获取连接时,还需要根据 ConnectionUrl 判断使用哪一个已经加载的驱动。
因此,JDBC 驱动的 SPI 机制是需要多个步骤配合来实现的。同时基于 Java SPI 机制的不足,同样也无法按需加载。
03 按需加载:Dubbo SPI 机制
由于 Java SPI 的缺陷无法支持按需加载接口实现类,Dubbo 并未采用 Java SPI,而是重新开发了一套功能更强大的 SPI 机制。 Dubbo SPI 的相关逻辑封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。
Dubbo SPI 所需的配置文件需要放置在 META-INF/dubbo 路径下,配置内容如下: optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee
与 Java SPI 实现类配置不同,Dubbo SPI 通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。 此外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。
下面来展示 Dubbo SPI 的用法:
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
测试结果如下:
另外,Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性。
SPI 机制的优势:


雷达卡


京公网安备 11010802022788号







