4. 模块间通信接口定义:接口下沉、依赖反转与ServiceLocator模式
4.1 原理:为什么要将接口下沉?
在模块化架构中,模块间通信的最大挑战是解耦。如果特性模块直接依赖其他特性模块的具体实现类,会导致编译期强耦合、无法独立编译和并行开发。
核心目标是让特性模块只依赖公共模块中的抽象接口,而非其他特性模块中的具体实现。通过以下三个关键技术实现模块间的松耦合:
- 接口下沉到公共模块:将所有服务接口、仓库接口等合约定义放置在
common或core模块中,作为全球契约。 - 依赖反转(Dependency Inversion):特性模块依赖
common模块的接口,并在自己模块内提供实现。调用者(如ViewModel)只感知接口,不感知实现细节。 - ServiceLocator 模式:提供一个全局或作用域内的注册表,用于获取接口的实现实例,避免了构造函数的直接传入或手动注入框架的耦合。
4.2 核心架构流程图
下图清晰地展示了接口下沉、依赖反转以及 ServiceLocator 在整个模块通信中的角色与数据流向:
4.3 实现流程与代码实操
按照以下步骤,从零搭建基于接口下沉和ServiceLocator的模块通信机制:
4.3.1 在 Common 模块中定义接口
所有的通信契约都定义在 common 模块中,该模块不包含任何业务逻辑。
package com.app.common.repository;
import androidx.lifecycle.LiveData;
public interface UserRepository {
LiveData<User> getUser(String userId);
void updateUser(User user);
}
4.3.2 在 Feature 模块中引入依赖并实现接口
特性模块(如 user_feature)在 build.gradle 中引入 common 模块,并提供具体实现。
// build.gradle (user_feature模块)
dependencies {
implementation project(':common') // 依赖公共模块
// ... 其他依赖
}
// UserRepositoryImpl.java (位于 user_feature 模块)
package com.app.user.repository;
import com.app.common.repository.UserRepository;
import androidx.lifecycle.LiveData;
public class UserRepositoryImpl implements UserRepository {
@Override
public LiveData<User> getUser(String userId) {
// 具体的数据获取逻辑 (网络/本地)
return null; // 返回实际数据
}
@Override
public void updateUser(User user) {
// 具体的更新逻辑
}
}
4.3.3 实现线程安全的 ServiceLocator
// ServiceLocator.java (位于 common 模块)
package com.app.common.di;
import java.util.HashMap;
import java.util.Map;
public class ServiceLocator {
private static volatile ServiceLocator INSTANCE;
private final Map<Class<?>, Object> services = new HashMap<>();
private ServiceLocator() {}
public static ServiceLocator getInstance() {
if (INSTANCE == null) {
synchronized (ServiceLocator.class) {
if (INSTANCE == null) {
INSTANCE = new ServiceLocator();
}
}
}
return INSTANCE;
}
public <T> void register(Class<T> clazz, T implementation) {
services.put(clazz, implementation);
}
@SuppressWarnings("unchecked")
public <T> T get(Class<T> clazz) {
return (T) services.get(clazz);
}
}
4.3.4 初始化注册(Application类)
// MyApplication.java
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 注册接口与具体实现的映射关系
ServiceLocator.getInstance()
.register(UserRepository.class, new UserRepositoryImpl());
}
}
4.3.5 在 ViewModel 中消费接口
// UserViewModel.java
public class UserViewModel extends ViewModel {
private final UserRepository userRepository;
public UserViewModel() {
// 通过 ServiceLocator 获取接口,而不是直接 new UserRepositoryImpl()
this.userRepository = ServiceLocator.getInstance().get(UserRepository.class);
}
public LiveData<User> getUser(String id) {
return userRepository.getUser(id);
}
}
4.4 调试与常见错误
| 常见错误 | 原因 | 解决方案 |
|---|---|---|
ClassCastException |
注册的实现类型与获取的类型不匹配 | 使用泛型安全的 register 方法,或自定义注解处理器编译时校验 |
NullPointerException |
获取服务时尚未注册,或初始化顺序错误 | 在 Application.onCreate 中保证初始化顺序;调用前使用 isRegistered 方法断言 |
| 模块间依赖循环 | 服务A依赖服务B,服务B又依赖服务A的构造函数 | 重构设计,引入事件总线(如 LiveDataBus)或协程 Channel 打破循环 |
| 内存泄漏 | ServiceLocator 持有 Activity/Fragment 引用 | 确保注册的服务不持有生命周期敏感对象的引用;按作用域管理 ServiceLocator 实例 |
4.5 性能与稳定性优化
- 懒加载:避免在应用启动时一次性注册所有服务,可结合
Lazy代理或按需注册机制,减少启动耗时。 - 作用域管理:为不同的生命周期创建
ServiceLocator实例(例如UserScopeLocator),在作用域结束时清除所有注册的服务,防止泄漏和侧逸。 - 线程安全:使用
ConcurrentHashMap结合Double-Checked Locking(如示例代码)确保多线程环境下的安全读写。 - 编译时校验:配合注解处理器(如 Dagger、Hilt 或自定义
@Service注解),在编译时检查服务是否被正确注册,将运行时错误提前到编译期暴露。
最佳实践:虽然手写 ServiceLocator 有助于理解模块化通信的原理,但在大型项目中,推荐使用成熟的 DI 框架(如 Dagger Hilt 或 Koin)。它们在编译期安全性、作用域管理和性能优化方面更为强大。本课程后续章节将专门讲解如何从手写 ServiceLocator 平滑迁移到 Hilt 框架。