授季傥芽在日常开发中,我们经常会遇到需要根据不同条件执行不同逻辑的场景,导致代码中出现大量的if/else嵌套。这不仅降低了代码的可读性和可维护性,还会增加后续扩展的难度。
本文将介绍四种优雅的设计模式来优化这种"条件爆炸"问题:
1 策略模式
01 概念
首先我们来看下策略模式的定义。
策略模式(Strategy Pattern)是行为型设计模式之一,它定义了一系列算法,并将每个算法封装起来,使它们可以相互替换。 策略模式使得算法可以独立于使用它们的客户端变化。
怎么理解策略模式 ?
软件开发中常常遇到这种情况,实现某一个功能有多种算法或者策略,我们需要根据环境或者条件的不同选择不同的算法或者策略来完成该功能。
这么一看,不就是 if...else...的逻辑 :
图中的实现逻辑非常简单,当我们是初学者时,这样写没有问题,只要能正常运行即可。但随着经验的增长,这段代码明显违反了 OOP 的两个基本原则:
单一职责原则:一个类或模块只负责完成一个职责或功能。
开闭原则:软件实体(模块、类、方法等)应该"对扩展开放,对修改关闭"。
由于违反了这两个原则,当 if-else 块中的逻辑日益复杂时,代码会变得越来越难以维护,极容易出现问题。
策略模式的设计思路是:
定义一些独立的类来封装不同的算法,每一个类封装一个具体的算法。
每一个封装算法的类我们都可以称之为策略 (Strategy) ,为了保证这些策略的一致性,一般会用一个抽象的策略类来做算法的定义,而具体每种算法则对应于一个具体策略类。
这种设计思想里,包含三个角色:
Context: 环境类
Strategy: 抽象策略类
ConcreteStrategy: 具体策略类
对应的时序图:
接下来,我们用代码简单演示一下。
02 例子
步骤 1:创建接口
public interface Strategy {
public int doOperation(int num1, int num2);
}
Strategy 接口定义了一个 doOperation 方法,所有具体策略类将实现这个接口,以提供具体的操作。
步骤 2:创建实现相同接口的具体类
OperationAdd 是一个具体的策略类,实现了加法操作。
public class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
OperationSubtract 是另一个具体的策略类,实现了减法操作。
public class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
OperationMultiply 是第三个具体的策略类,实现了乘法操作。
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);
}
}
Context 类持有一个策略对象的引用,并允许客户端设置其策略。它提供了一个 executeStrategy 方法,用于执行策略并返回结果。
步骤 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 OperationSubstract());
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 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。
02 Java SPI : JDBC Driver
在JDBC4.0 之前,我们开发有连接数据库的时候,通常先加载数据库相关的驱动,然后再进行获取连接等的操作。
// STEP 1: Register JDBC driver
Class.forName("com.mysql.jdbc.Driver");
// STEP 2: Open a connection
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类是驱动管理器,也是驱动加载的入口。
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在 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 了,driver 的实现有一个约定,如果 driver 根据 JDBC URL 判断不是自己可以处理的连接就直接返回空,DriverManager 就是基于这个约定直到找到第一个不返回 null 的连接为止。
JDBC Driver 是 Java SPI 机制的一个非常经典的应用场景,包含三个核心步骤:
定义 SPI 文件 ,指定 Driver class 全限定名 ;
通过 DriverManager 静态类自动加载驱动 ;
加载之后,当需要获取连接时,还需要根据 ConnectionUrl 判断使用哪一个加载完成的 Driver 。
因此, JDBC Driver 的 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 机制的优势: