[CleanArchitecture] Google官方的Nowinandroid是如何抽出抽象层(Domain Layer)的
Posted datian1234
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[CleanArchitecture] Google官方的Nowinandroid是如何抽出抽象层(Domain Layer)的相关的知识,希望对你有一定的参考价值。
Google官方的安卓应用Nowinandroid使用了目前很主流的技术,其中在架构分层方面使用到了干净架构即CleanArchitecture,该架构配合MVVM模式可以大大提升可读性、拓展性以及可移植性,本文主要学习Google是如何抽出抽象层的。
补充:干净架构的分层如下
随着业务的不断迭代,工程会变得越来越庞大,这时候引入抽象层是很有必要的,抽象层负责管理app中的业务逻辑,除了提升可读性外,还可以将多个地方共用的逻辑抽取并封装成usecases,便于在多个ViewModel中使用。
什么是Use case?
Use case意思为用例,一般来说就是函数(也可以是一个class,内部只有一个简单的public method),一个用例代表一个逻辑或者操作,用例执行后会组合或者拉取Data Layer、其它用例的数据,比如读取用户数据即可作为一个用例。
用例的命名一般按照逻辑名_UseCase的格式,便于阅读理解,google使用的是动词+名词+UseCase格式,如:
class GetUserNewsResourcesUseCase @Inject constructor(
private val newsRepository: NewsRepository,
private val userDataRepository: UserDataRepository
)
/**
* Returns a list of UserNewsResources which match the supplied set of topic ids.
*
* @param filterTopicIds - A set of topic ids used to filter the list of news resources. If
* this is empty the list of news resources will not be filtered.
*/
operator fun invoke(
filterTopicIds: Set<String> = emptySet()
): Flow<List<UserNewsResource>> =
if (filterTopicIds.isEmpty())
newsRepository.getNewsResources()
else
newsRepository.getNewsResources(filterTopicIds = filterTopicIds)
.mapToUserNewsResources(userDataRepository.userData)
可以看到该GetUserNewsResourcesUseCase是一个class,重写了其invoke方法,invoke方法会返回用例执行后的返回值,除此之外没有其它方法。
抽取出抽象层的流程
总体可以分享为以下几步:
- 找出ViewModel中复杂的和重复的业务逻辑
- 创建对应逻辑的use cases
- 将逻辑移动到use cases
- 重构ViewModel,构建的时候传入的是use cases而不是repositories
- 编写use cases的单元测试
- 找出ViewModel中复杂的和重复的业务逻辑
Nowinandroid的app分为三个大模块:For you、Saved(Bookmarks)、Interests,对应三个tab页面,它们的ViewModel中的业务逻辑如下图所示:
可以看到在observes栏有很多相同颜色的逻辑,为了逻辑复用,直接将相同的逻辑抽出来封装成use case(Candidate UseCase栏),可以使得ViewModel更简洁明了。
- 将逻辑移动到use cases,重构ViewModel
BookmarksVM、ForYouVM、TopicVM都用到了相同的UseCase-GetSavableNewsResourcesUseCase,来看下这个UseCase怎么封装。
class GetUserNewsResourcesUseCase @Inject constructor(
private val newsRepository: NewsRepository,
private val userDataRepository: UserDataRepository
)
/**
* Returns a list of UserNewsResources which match the supplied set of topic ids.
*
* @param filterTopicIds - A set of topic ids used to filter the list of news resources. If
* this is empty the list of news resources will not be filtered.
*/
operator fun invoke(
filterTopicIds: Set<String> = emptySet()
): Flow<List<UserNewsResource>> =
if (filterTopicIds.isEmpty())
newsRepository.getNewsResources()
else
newsRepository.getNewsResources(filterTopicIds = filterTopicIds)
.mapToUserNewsResources(userDataRepository.userData)
private fun Flow<List<NewsResource>>.mapToUserNewsResources(
userDataStream: Flow<UserData>
): Flow<List<UserNewsResource>> =
filterNot it.isEmpty()
.combine(userDataStream) newsResources, userData ->
//组合数据源
newsResources.mapToUserNewsResources(userData)
很简单,构造的时候传入了多个数据源Repository,用例执行的时候将多个数据源的数据组合起来并返回,现在BookmarksVM中的逻辑就很简单明了了,执行用例拿到数据做筛选并转换成热流给UI层使用,其它使用到相同逻辑的ViewModel也是如此。
@HiltViewModel
class BookmarksViewModel @Inject constructor(
private val userDataRepository: UserDataRepository,
getSaveableNewsResources: GetUserNewsResourcesUseCase
) : ViewModel()
val feedUiState: StateFlow<NewsFeedUiState> = getSaveableNewsResources() //执行用例
.filterNot it.isEmpty()
.map newsResources -> newsResources.filter(UserNewsResource::isSaved) // Only show bookmarked news resources.
.map<List<UserNewsResource>, NewsFeedUiState>(NewsFeedUiState::Success)
.onStart emit(Loading)
.stateIn(
//转换成StateFlow
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = Loading
)
fun removeFromSavedResources(newsResourceId: String)
viewModelScope.launch
userDataRepository.updateNewsResourceBookmark(newsResourceId, false)
- 抽象层代码单独存放在一个module
可以看到module里面主要是包含usecases和model,这里的model类代表用例执行后的结果,如果你的用例返回结果结合了多个数据源,那么model类即是做一个结果封装,反之如果用例的数据来源是单一的,那么它使用到的model结构其实跟data层的结构是一样的,但为了保持接结构分层,一般也会单独建一个model。
-
编写UseCase的单元测试
一个工程有了好的代码分层还不够,单元测试也是很重要的,UseCase作为一个逻辑用例,编写代码的时候要关注是否是可测试的,其中有一点比较重要的原则是:单一职责原则,即所测试单元的职责是单一的(如只进行数据的拉取结合操作),并不负责创建数据源repository,数据源由构建函数传入,这样保证了数据源是可以mock的。class GetUserNewsResourcesUseCaseTest @get:Rule val mainDispatcherRule = MainDispatcherRule() private val newsRepository = TestNewsRepository() private val userDataRepository = TestUserDataRepository() val useCase = GetUserNewsResourcesUseCase(newsRepository, userDataRepository) @Test fun whenNoFilters_allNewsResourcesAreReturned() = runTest // Obtain the user news resources stream. val userNewsResources = useCase() // Send some news resources and user data into the data repositories. newsRepository.sendNewsResources(sampleNewsResources) // Construct the test user data with bookmarks and followed topics. val userData = emptyUserData.copy( bookmarkedNewsResources = setOf(sampleNewsResources[0].id, sampleNewsResources[2].id), followedTopics = setOf(sampleTopic1.id) ) userDataRepository.setUserData(userData) // Check that the correct news resources are returned with their bookmarked state. assertEquals( sampleNewsResources.mapToUserNewsResources(userData), userNewsResources.first() )
总结
- 抽象层单独放到一个module,里面存放用例(usecase)以及model
- usecase代表一个业务逻辑,一般是一个函数或者只有invoke方法的类,执行后返回结果
- 抽取抽象层的步骤一般为:找出ViewModel中复杂的和重复的业务逻辑,将逻辑移到usecase里,ViewModel构建的时候传入usecase
作者:linversion
链接:https://juejin.cn/post/7189535902375346234
以上是关于[CleanArchitecture] Google官方的Nowinandroid是如何抽出抽象层(Domain Layer)的的主要内容,如果未能解决你的问题,请参考以下文章