Android架构面试(九):Clean Architecture 终极指南
1.1 分层结构与依赖规则
Clean Architecture将Android应用分为三层:
- Presentation Layer(表现层):处理UI逻辑(如Activity/Fragment/Compose),通过ViewModel与领域层交互
- Domain Layer(领域层):封装业务逻辑与Use Cases,严格独立于Android框架(无Android SDK依赖)
- Data Layer(数据层):管理数据源(数据库/API),通过Repository模式提供统一接口
依赖规则:内层不依赖外层 → 领域层不感知数据层实现细节,仅通过接口交互。
1.2 SOLID原则在架构中的应用
// 示例:依赖倒置原则(DIP)实现
interface PaymentRepository { // 领域层定义抽象
suspend fun processPayment(amount: Double): Boolean
}
class PaymentUseCase(
private val repo: PaymentRepository // 依赖抽象而非具体实现
) {
suspend fun execute() = repo.processPayment(100.0)
}
// 数据层实现接口(外层依赖内层)
class PaymentRepositoryImpl : PaymentRepository {
override suspend fun processPayment(amount: Double): Boolean {
// 调用支付SDK(具体实现)
}
}
代码注释:
PaymentRepository
接口在领域层定义,确保业务逻辑与支付SDK解耦PaymentRepositoryImpl
在数据层实现,通过依赖注入(如Dagger/Hilt)提供给UseCase
1.3 与领域驱动设计(DDD)的关系
- 领域模型:如
User
、Order
实体类,包含核心业务属性与方法 - 领域服务:跨实体的业务逻辑(如订单校验服务)
- 界限上下文:模块化拆分业务边界(外卖拆分为订单/支付/用户服务)
🔧 二、分层实现详解与代码实战
2.1 领域层(Domain Layer)设计
2.1.1 Use Cases封装业务逻辑
class LoginUseCase(
private val authRepo: AuthRepository,
private val threadExecutor: ThreadExecutor // 线程调度抽象
) {
suspend operator fun invoke(email: String, password: String): Result<Boolean> {
// 业务规则校验
if (!isValidEmail(email)) throw ValidationException("Invalid email")
return authRepo.login(email, password) // 调用仓库
}
}
关键点:
- UseCase命名规范:
功能名 + UseCase
(如LoginUseCase
) - 每个UseCase仅处理单一业务逻辑
2.1.2 领域模型映射
// 数据层模型 → 领域模型转换
data class ArticleEntity(val id: Int, val title: String, val content: String)
data class Article(val id: Int, val title: String) // 领域模型
class ArticleMapper : Mapper<Article, ArticleEntity> {
override fun mapToDomain(entity: ArticleEntity): Article {
return Article(entity.id, entity.title) // 仅保留核心字段
}
}
优势: 隔离数据层细节,避免数据库结构污染业务逻辑
2.2 数据层(Data Layer)实现
2.2.1 Repository模式与数据源选择
class ArticleRepositoryImpl(
private val localSource: ArticleLocalDataSource,
private val remoteSource: ArticleRemoteDataSource
) : ArticleRepository {
override fun getArticles(): Flow<List<Article>> {
return localSource.isCached().flatMapConcat { isCached ->
if (isCached) localSource.getArticles()
else remoteSource.getArticles().also { localSource.cache(it) }
}
}
}
策略: 优先缓存 → 减少网络请求,提升响应速度
2.2.2 数据源动态切换
class DataStoreFactory(
private val cache: CacheDataStore,
private val remote: RemoteDataStore
) {
fun getDataStore(isCacheValid: Boolean): DataStore {
return if (isCacheValid) cache else remote
}
}
应用场景: 根据网络状态自动切换本地/云端数据源
2.3 表现层(Presentation Layer)状态管理
2.3.1 ViewModel + LiveData实现单向数据流
class ArticleViewModel(
private val getArticlesUseCase: GetArticlesUseCase
) : ViewModel() {
private val _state = MutableLiveData<Resource<List<ArticleView>>>()
val state: LiveData<Resource<List<ArticleView>>> get() = _state
fun loadArticles() {
viewModelScope.launch {
_state.value = Resource.Loading()
try {
val articles = getArticlesUseCase.execute()
_state.value = Resource.Success(articles.map { it.toView() })
} catch (e: Exception) {
_state.value = Resource.Error(e.message)
}
}
}
}
设计要点:
Resource
封装加载/成功/错误状态ArticleView
为UI专用模型,避免暴露领域模型细节
2.3.2 MVI模式进阶实现
data class ArticleState(
val isLoading: Boolean = false,
val articles: List<ArticleView> = emptyList(),
val error: String? = null
)
class ArticleViewModel : ViewModel() {
private val _state = MutableStateFlow(ArticleState())
val state: StateFlow<ArticleState> = _state.asStateFlow()
fun dispatch(action: Action) {
when (action) {
is Action.Load -> loadArticles()
}
}
private fun loadArticles() { /* 更新_state */ }
}
优势: 状态集中管理,避免分散LiveData导致的逻辑碎片化
🏢 三、企业级实践案例解析
3.1 外卖架构演进
阶段 | 架构特点 | Clean Architecture对应点 |
---|---|---|
O2O阶段(2012-2015) | 服务耦合度高 | 未严格分层 |
平台化阶段(2018-至今) | 微服务独立部署 | 模块化(订单/支付/用户服务) |
数据层 | 统一API网关 | Repository接口标准化 |
核心改进: 通过领域驱动设计划分界限上下文,团队并行开发
3.2 清晰架构设计
- 端口与适配器模式:
- 主适配器:处理用户输入(如点击事件)
- 从适配器:实现外部服务(如支付SDK)
- 控制反转:业务模块依赖接口而非实现 → 替换数据源无需修改领域层
💡 四、高频面试题深度剖析
4.1 理论类问题
Q1:为什么ViewModel能避免内存泄漏?
答:ViewModel不与Activity/Fragment绑定,而是关联ViewModelStore
。当配置变更时,Activity重建但ViewModelStore
保留,ViewModel实例未被销毁;当Activity永久销毁时,ViewModelStore
清除并触发onCleared()
释放资源。
Q2:如何确保领域层纯净?
答:三原则验证:
- 无Android SDK导入语句
- 所有外部依赖通过接口抽象(如
ThreadExecutor
替代CoroutineDispatcher
) - 单元测试可在JVM环境运行(无需Robolectric)
4.2 代码实战类问题
Q3:实现Token过期自动刷新
class AuthInterceptor(private val tokenRepo: TokenRepository) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
if (tokenRepo.isTokenExpired()) {
synchronized(this) {
tokenRepo.refreshToken() // 同步刷新
}
}
return chain.proceed(request.newBuilder()
.header("Authorization", tokenRepo.getToken())
.build())
}
}
关键点:
- 同步块防止并发刷新
- Token状态由领域层
TokenRepository
管理
4.3 架构设计类问题
Q4:如何设计跨模块数据共享?
方案:
- 共享ViewModel:
// Fragment间共享 val sharedVM: SharedViewModel by activityViewModels()
- 事件总线(谨慎使用):
interface EventBus { fun publish(event: Event) fun observe(owner: LifecycleOwner, observer: (Event) -> Unit) }
- 单例仓库(推荐):
class GlobalDataRepository @Inject constructor() { private val _data = MutableStateFlow<Data?>(null) val data: StateFlow<Data?> get() = _data.asStateFlow() }
🧪 五、单元测试策略
5.1 领域层测试(纯JUnit)
class LoginUseCaseTest {
@Test
fun `invalid email should throw exception`() = runTest {
// 模拟Repository
val mockRepo = mockk<AuthRepository>()
val useCase = LoginUseCase(mockRepo)
// 验证异常
assertThrows<ValidationException> { useCase("invalid-email", "password") }
}
}
工具链: JUnit 5 + MockK + Turbine(Flow测试)
5.2 UI层测试(Espresso + Hilt)
@HiltAndroidTest
class LoginFragmentTest {
@get:Rule val hiltRule = HiltAndroidRule(this)
@Test
fun loginSuccess_shouldNavigateToHome() {
// 模拟ViewModel
val mockVM = mockk<LoginViewModel>()
every { mockVM.login(any(), any()) } returns flowOf(Result.Success(true))
// 启动Fragment
launchFragmentInHiltContainer<LoginFragment>()
// 操作UI并验证
onView(withId(R.id.btn_login)).perform(click())
onView(withId(R.id.home_layout)).check(matches(isDisplayed()))
}
}
🔍 六、常见陷阱
陷阱现象 | 后果 | 解决方案 |
---|---|---|
在UseCase中直接使用Context | 内存泄漏+无法单元测试 | 通过接口抽象Context操作(如LocationProvider ) |
数据层模型传递到UI层 | UI逻辑污染领域层 | 定义ViewData 类并添加Mapper 转换 |
ViewModel包含业务逻辑 | 职责过重+难以复用 | 将业务逻辑移至UseCase,ViewModel仅协调UI状态 |
🎯 总结
面试要点
分层价值
- 可测试性:领域层独立于Android SDK(JUnit覆盖率>90%)
- 团队协作:模块化拆分支持百人团队并行开发
- 技术可持续性:替换框架(如Retrofit→Ktor)仅需修改数据层
面试回答公式
理论阐述 → 代码示例 → 企业实践 → 个人经验
示例回答:
"ViewModel的生命周期管理通过ViewModelStore实现,在配置变更时保留实例(代码见4.1),借此解决订单页面数据丢失问题,我们在项目中结合SavedStateHandle处理进程重建..."架构演进趋势
- 模块化:从分层到组件化(独立交付支付/用户模块)
- 响应式:Flow/Channels替代LiveData实现跨模块通信
- 多平台:共享领域层(KMM跨Android/iOS)