文章目录
- Pre
- 概述
- 依赖倒置原则与解耦
-
- 设计与实现
-
- [1. 定义接口来隔离调用方与实现类](#1. 定义接口来隔离调用方与实现类)
- [2. 实现类`DynamicStubFactory`](#2. 实现类
DynamicStubFactory
) - [3. 调用方与实现类的解耦](#3. 调用方与实现类的解耦)
- 依赖注入与SPI的解耦
-
- 依赖注入
- [SPI(Service Provider Interface)](#SPI(Service Provider Interface))
- 总结
Pre
Simple RPC - 02 通用高性能序列化和反序列化设计与实现
Simple RPC - 03 借助Netty实现异步网络通信
Simple RPC - 04 从零开始设计一个客户端(上)
概述
接 Simple RPC - 04 从零开始设计一个客户端(上) ,我们继续分析 依赖倒置和SPI是如何实现的。
依赖倒置原则与解耦
在软件设计中,依赖倒置原则(Dependence Inversion Principle, DIP) 是SOLID原则之一。它主张高层模块(调用者)不应依赖于低层模块(实现类),而是两者都应该依赖于抽象(接口或抽象类)。这意味着具体的实现细节应当与高层业务逻辑分离,通过接口来隔离依赖关系,从而提高代码的可维护性、可扩展性和可复用性。
设计与实现
在这个RPC框架的设计中,通过定义接口来解耦调用方和具体实现,完全符合依赖倒置原则。
我们来看下是如何应用DIP来解耦的。
1. 定义接口来隔离调用方与实现类
java
public interface StubFactory {
<T> T createStub(Transport transport, Class<T> serviceClass);
}
StubFactory
接口定义了创建桩的方法,而具体的实现类DynamicStubFactory
实现了该接口。
2. 实现类DynamicStubFactory
java
public class DynamicStubFactory implements StubFactory {
// 实现 createStub 方法的逻辑
}
DynamicStubFactory
实现了StubFactory
接口,提供了实际的桩生成逻辑。
3. 调用方与实现类的解耦
在调用方NettyRpcAccessPoint
中,我们并不直接依赖于具体的DynamicStubFactory
,而是依赖于StubFactory
接口。调用方通过接口与实现类进行交互,这样如果以后需要更换不同的StubFactory
实现,只需更改实现类而无需修改调用方的代码。
java
public class NettyRpcAccessPoint {
private final StubFactory stubFactory;
public NettyRpcAccessPoint(StubFactory stubFactory) {
this.stubFactory = stubFactory;
}
public <T> T createStub(Transport transport, Class<T> serviceClass) {
return stubFactory.createStub(transport, serviceClass);
}
}
依赖注入与SPI的解耦
依赖注入
通常情况下,依赖注入(如Spring框架)可以帮助我们实现这种解耦,通过配置或注解,框架会自动将具体的实现注入到调用方中。但在不使用Spring的情况下,我们可以使用Java内置的SPI机制来实现类似的解耦。
SPI(Service Provider Interface)
SPI机制通过在META-INF/services/
目录下配置接口的实现类,在运行时动态加载这些实现类,实现依赖倒置。
-
配置文件:
- 在
META-INF/services/
目录下创建一个文件,文件名是接口的完全限定名(例如com.github.liyue2008.rpc.client.StubFactory
)。 - 文件内容是接口的实现类名(例如
com.github.liyue2008.rpc.client.DynamicStubFactory
)。
- 在
-
SPI加载实现类:
java
/**
* 提供服务加载功能的支持类,特别是处理单例服务
* @author artisan
*/
public class ServiceSupport {
/**
* 存储单例服务的映射,确保每个服务只有一个实例
*/
private final static Map<String, Object> singletonServices = new HashMap<>();
/**
* 加载单例服务实例
*
* @param service 服务类的Class对象
* @param <S> 服务类的类型参数
* @return 单例服务实例
* @throws ServiceLoadException 如果找不到服务实例
*/
public synchronized static <S> S load(Class<S> service) {
return StreamSupport.
stream(ServiceLoader.load(service).spliterator(), false)
.map(ServiceSupport::singletonFilter)
.findFirst().orElseThrow(ServiceLoadException::new);
}
/**
* 加载所有服务实例
*
* @param service 服务类的Class对象
* @param <S> 服务类的类型参数
* @return 所有服务实例的集合
*/
public synchronized static <S> Collection<S> loadAll(Class<S> service) {
return StreamSupport.
stream(ServiceLoader.load(service).spliterator(), false)
.map(ServiceSupport::singletonFilter).collect(Collectors.toList());
}
/**
* 对服务实例进行单例过滤
*
* @param service 服务实例
* @param <S> 服务类的类型参数
* @return 单例过滤后的服务实例,如果该服务是单例的并且已有实例存在,则返回已存在的实例
*/
@SuppressWarnings("unchecked")
private static <S> S singletonFilter(S service) {
if(service.getClass().isAnnotationPresent(Singleton.class)) {
String className = service.getClass().getCanonicalName();
Object singletonInstance = singletonServices.putIfAbsent(className, service);
return singletonInstance == null ? service : (S) singletonInstance;
} else {
return service;
}
}
}
调用ServiceSupport.load(StubFactory.class)
时,SPI机制会查找META-INF/services/
目录下对应的配置文件,加载其中指定的实现类实例。
总结
通过依赖倒置原则(DIP)和SPI机制,我们有效地解耦了调用方与实现类。在这个RPC框架中,StubFactory
接口及其实现类DynamicStubFactory
之间的依赖关系被逆转,调用方只依赖接口,而不直接依赖具体实现。SPI机制进一步解耦了调用方与实现类的实例化,使得在运行时可以动态加载实现类,这为框架的扩展性和灵活性提供了强有力的支持。
通过这种设计,框架可以很容易地替换StubFactory
的实现,而不影响调用方,保持了代码的高可维护性和
扩展性。