Android清洁架构中映射域的正确位置

8
我和我的同事正在辩论将实体对象或远程DTO对象映射到简单的域对象的正确位置。
我们的结构如下所示。
源(包括DAO)>仓库(包括源)>用例(包括仓库)
我的同事认为应该在源内完成到域的映射,以便将域对象传递给下一层。
class SomeSourceImpl(private val dao: Dao) : SomeSource {
    override fun get(): Observable<DomainModel> {
        return dao.getResponse().map { it.mapToDomain() }
    }
}

我的同事认为根据Uncle Bob的说法,这是由于依赖规则导致的。

这个规则指出,源代码依赖关系只能向内指。内部圈子中的任何内容都不能知道外部圈子中的任何东西。特别地,外部圈子中声明的任何名称都不得被内部圈子中的代码所提及。这包括函数、类、变量或任何其他命名的软件实体。

我非常不同意在源代码中直接映射到域的方法,因为这样存储库就会变得贫血,我们因此采用了贫血存储库无用的反模式,它们只是盲目地传播来自源的所有内容。(现在你可能会说源代码也是贫血的,我们可以简单地删除它们并将dao对象直接包含到存储库中,但在我们的情况下这是不可行的)。

我建议源代码返回原始的数据库实体(或远程实体,如果我们进入rest调用),因为对于源代码来说,返回原始数据以供后续处理是有意义的。这是仓库的工作,从源代码获取结果,然后将其映射到域,最后将该域对象传播到使用案例中,类似于这样。
class SomeRepoImpl(private val someSource: SomeSource) : SomeRepo {
    override fun get(haId: String): Observable<DomainModel> {
        return otherAssetSource.get().map { it.mapToDomain() }
    }

我也在Github上找到了一些示例,它们将域名映射到其存储库内而不是源文件中。

这里

这里

这里

这里也有一个iOS的版本。

在清洁架构原则中,将实体映射到域对象的位置应遵循严格规则吗?


2
较低层不能依赖于较高层的代码,因此将其映射到域层中的一个域。 "内圈中的任何东西都不能对外圈中的某些事物有任何了解。" 源是内圈,域是外圈。 - Artur Gniewowski
我检查了许多仓库,从中我观察到映射器类位于数据层,而映射是在存储库的具体实现中完成的,该实现也位于数据层。因此,在数据层中,您将拥有本地、远程、映射器和存储库文件夹。 - Bitwise DEVS
3个回答

8

引用规则:

源代码依赖关系只能指向内部。

我猜这取决于架构。让我以一个例子来解释一下:

架构:

DOMAIN <- DATA <- PRESENTATION

位置:

DATA -> LOCAL  
|  
v  
REMOTE

注意: DOMAIN 代表最内层的圆圈,PRESENTATION 代表最外层的圆圈。

现在 DOMAIN 是一个纯 Kotlin 模块,没有任何 Android 依赖。让我们定义一个仓库:

interface ProfileRepository {
    
    fun getProfile(): Profile?

    fun updateProfile(profile: Profile): Profile
}

我们将其实现在数据层中(这是一个Android库):
class ProfileRepositoryImpl(
    private val networkManager: NetworkManager,
    private val remoteDataSource: ProfileRemoteDataSource,
    private val localDataSource: ProfileLocalDataSource
): ProfileRepository {
     
    override fun getProfile(): Profile? {
        return if(networkManager.isNetworkAvailable) {
            localDataSource.insert(remoteDataSource.get())
        } else {
            localDataSource.get()
        }
    }

    override fun updateProfile(profile: Profile): Profile {
        val updatedProfile = remoteDataSource.update(profile)
        return localDataSource.insert(updatedProfile)
    }
}

class ProfileRemoteDataSource(
    private val api: ProfileApi,
    private val mapper: Mapper<ProfileDto, Profile>
) {
   
    fun get(): Profile {
        return mapper.toModel(api.getProfile())
    }

    fun update(profile: Profile): Profile {
        val dto = api.updateProfile(
            mapper.fromModel(profile)
        )
        return mapper.toModel(dto)
    }
}

class ProfileLocalDataSource(
    private val dao: ProfileDao,
    private val mapper: Mapper<ProfileEntity, Profile>
) {

    fun insert(profile: Profile): Profile {
        dao.insert(mapper.fromModel(profile))
        return requireNotNull(get())
    }

    fun get(): Profile? {
        return dao.get()?.let(mapper::toModel)
    }
}

interface Mapper<T : Any, Model : Any> {
    fun toModel(value: T): Model
    fun fromModel(value: Model): T
}

LOCAL 模块是一个 Android 库,独立于任何依赖项,并公开 DAOEntity 对象:

interface ProfileDao {
    fun insert(profile: ProfileEntity)
    fun get(): ProfileEntity?
}

同样地,对于REMOTE模块:

interface ProfileApi {
    fun get(): ProfileDto
    fun update(profile: ProfileDto): ProfileDto
}

因此,对我来说,让Source类返回DTOEntity对象就没有意义。仓库类将看起来像这样:

class ProfileRepositoryImpl(
    private val networkManager: NetworkManager,
    private val remoteDataSource: ProfileRemoteDataSource,
    private val remoteDataMapper: Mapper<ProfileDto, Profile>,
    private val localDataSource: ProfileLocalDataSource,
    private val localDataMapper: Mapper<ProfileEntity, Profile>
) : ProfileRepository {

    override fun getProfile(): Profile? {
        if (networkManager.isNetworkAvailable) {
            val dto = remoteDataSource.get()
            val profile = remoteDataMapper.toModel(dto)
            val entity = localDataMapper.fromModel(profile)

            localDataSource.insert(entity)
        }

        return localDataSource.get()?.let(localDataMapper::toModel)
    }

    override fun updateProfile(profile: Profile): Profile {
        val request = remoteDataMapper.fromModel(profile)
        val dto = remoteDataSource.update(request)
        
        val updatedProfile = remoteDataMapper.toModel(dto)
        
        val entity = localDataMapper.fromModel(updatedProfile)
        localDataSource.insert(entity)

        return localDataMapper.toModel(
            requireNotNull(localDataSource.get())
        )
    }
}

在你的例子中,你只考虑了GET操作。在这里,对于UPDATE操作,我们还需要映射DOMAIN对象。因此,如果在Repo类中进行对象映射,随着我们添加更多的功能,Repo类会变得非常混乱。
我认为这将取决于系统的整体架构。

如果获取、错误处理和映射都在数据源中完成,那么仓库的目的是什么? - mars8
3
在我的情况下,目的是处理来自各种来源的数据。此外,在我的大多数用例中,某些数据源在多个存储库中使用,因此我会在数据源中进行映射,而不是在多个存储库中进行映射。 - Xid
1
@mars8,我会在Xid的评论基础上提出我的观点。仓库充当多个数据源的中介,即根据上述评论处理这些数据源的数据。通过在正确的位置进行映射等某些操作/任务,可以使您的代码易于维护和测试。 - jvoyatz

1

我正在学习这个主题,但我的方法是使用在大多数项目中的Clean Architecture。

领域层是最内层,因此它不依赖于其他层。因此,第一个决定是让映射器留在数据和表示层,或者我用于这两个目的的任何层。

然后,我有一个接口来定义如何定义我的映射器。

interface EntityMapper<M : Model, ME : ModelEntity> {
    fun mapToDomain(entity: ME): M

    fun mapToEntity(model: M): ME
}

接下来,在数据层中我有一些类用于将数据模型映射到域模型。一个例子如下:

class ItemEntityMapper @Inject constructor(
    private val ownerEntityMapper: OwnerEntityMapper
) : EntityMapper<Item, ItemEntity> {
    override fun mapToDomain(entity: ItemEntity) = Item(
        id = entity.id,
        name = entity.name,
        fullName = entity.fullName,
        description = entity.description,
        url = entity.url,
        stars = entity.stars,
        owner = entity.ownerEntity?.let { ownerEntityMapper.mapToDomain(it) }
    )

    override fun mapToEntity(model: Item) = ItemEntity(
        id = model.id,
        name = model.name,
        fullName = model.fullName,
        description = model.description,
        url = model.url,
        stars = model.stars,
        ownerEntity = model.owner?.let { ownerEntityMapper.mapToEntity(it) }
    )
}

我更喜欢使用面向对象的类而不是函数式 Kotlin,以便于 DI。


0
在Android的背景下,应用架构指南上的开发者文档中有这个带有给定脚注的图表。箭头表示类之间的依赖关系。领域层依赖于数据层的类。较低/内层不能了解较高/外层的任何信息。这意味着映射器不能在较低/内层中。这样做会在数据层中引用领域模型,从而违反依赖规则。
因此,在Android的上下文中,作为较高层的领域应该保存映射器。

enter image description here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接