4. 模块间通信接口定义

4.1 原理:为什么要将接口下沉?

在模块化架构中,模块间通信的最大挑战是解耦。如果特性模块直接依赖其他特性模块的具体实现类,会导致编译期强耦合、无法独立编译和并行开发。

核心目标是让特性模块只依赖公共模块中的抽象接口,而非其他特性模块中的具体实现。通过以下三个关键技术实现模块间的松耦合:

  • 接口下沉到公共模块:将所有服务接口、仓库接口等合约定义放置在 common 或 core 模块中,作为全球契约。
  • 依赖反转(Dependency Inversion):特性模块依赖 common 模块的接口,并在自己模块内提供实现。调用者(如ViewModel)只感知接口,不感知实现细节。
  • ServiceLocator 模式:提供一个全局或作用域内的注册表,用于获取接口的实现实例,避免了构造函数的直接传入或手动注入框架的耦合。

4.2 核心架构流程图

下图清晰地展示了接口下沉、依赖反转以及 ServiceLocator 在整个模块通信中的角色与数据流向:

依赖反转与模块间通信架构 Common 模块 接口 (契约) ViewModel Feature 模块 具体实现 ServiceLocator 核心原则:依赖反转 1. 抽象(接口)不应依赖细节(实现)。 2. 细节(实现)应依赖抽象(接口)。 3. 接口下沉到公共模块,实现留在特性模块。 4. 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 框架。