11、数据层到Domain层的映射:Mapper模式与数据转换
做Android开发久了,你会发现一个有意思的现象:数据层拿到的模型,跟业务层想用的模型,往往不是一回事。
举个例子。后端返回的用户信息里,有个字段叫 user_status,值是 0、1、2。但在你的Domain层里,你希望它是一个枚举 UserStatus.ACTIVE、UserStatus.BANNED。怎么办?直接改后端接口?不现实。在数据层里到处写 if-else?那代码就烂了。
这时候就需要一个专门的转换层——Mapper。说白了,它就是数据模型和领域模型之间的翻译官。
为什么需要Mapper?
我在项目里见过不少团队,直接把数据层的 Entity 往 ViewModel 里传。一开始觉得挺方便,但迭代三个月后,问题就来了:
- 后端改了字段名,整个App到处都要改
- 数据层里塞满了业务逻辑
- 单元测试变得极其困难
Clean Architecture 的核心原则之一就是依赖倒置。Domain层不应该知道数据层的存在。Mapper 就是这层隔离的守护者。
Mapper的三种常见形式
嗯,这里我根据实际项目经验,把Mapper分成了三类。你可以根据场景灵活选择。
| 类型 | 适用场景 | 复杂度 |
|---|---|---|
| 接口式Mapper | 简单的一对一映射 | 低 |
| 扩展函数式Mapper | Kotlin项目,追求简洁 | 低 |
| 抽象基类Mapper | 复杂映射,需要复用逻辑 | 中 |
接口式Mapper:最正统的方式
先定义一个接口,明确输入输出:
interface Mapper<in From, out To> {
fun map(from: From): To
}
然后实现它:
class UserEntityToDomainMapper : Mapper<UserEntity, User> {
override fun map(from: UserEntity): User {
return User(
id = from.id,
name = from.name,
email = from.email,
status = mapStatus(from.status)
)
}
private fun mapStatus(status: Int): UserStatus {
return when (status) {
1 -> UserStatus.ACTIVE
2 -> UserStatus.INACTIVE
else -> UserStatus.BANNED
}
}
}
这种方式的好处是类型安全,而且每个Mapper的职责非常清晰。我在大型项目中比较推荐这种写法,因为后期维护时,你一眼就能看出每个字段是怎么转换的。
扩展函数式Mapper:Kotlin的优雅写法
如果你觉得写一个类太啰嗦,Kotlin的扩展函数可以帮你简化:
fun UserEntity.toDomain(): User {
return User(
id = id,
name = name,
email = email,
status = when (status) {
1 -> UserStatus.ACTIVE
2 -> UserStatus.INACTIVE
else -> UserStatus.BANNED
}
)
}
调用时就更简洁了:
val domainUser = entity.toDomain()
抽象基类Mapper:处理复杂场景
有时候,映射不是简单的字段赋值。比如你需要把列表里的每个元素都映射一遍,或者需要处理空值、默认值。这时候抽象基类就派上用场了:
abstract class BaseMapper<in From, out To> {
abstract fun map(from: From): To
fun mapList(fromList: List<From>): List<To> {
return fromList.map { map(it) }
}
fun mapNullable(from: From?): To? {
return from?.let { map(it) }
}
}
子类只需要实现 map 方法,列表和空值处理自动就有了:
class UserMapper : BaseMapper<UserEntity, User>() {
override fun map(from: UserEntity): User {
// 具体映射逻辑
}
}
// 使用
val users: List<User> = userMapper.mapList(entityList)
我曾经在一个电商项目里,用这种方式处理了二十多种实体映射。每个Mapper只需要关注核心转换逻辑,公共的列表处理、空值处理都由基类搞定。代码量减少了将近40%。
避坑指南:我曾经踩过的坑
讲几个我实际遇到过的坑,你写Mapper的时候可以留意一下。
Mapper只做数据转换,不要在里面做校验、计算、过滤。比如「如果用户年龄大于18岁,返回VIP状态」——这种逻辑应该放在Domain层的UseCase里,而不是Mapper里。
如果你同时需要 Entity → Domain 和 Domain → Entity 的映射,一定要保证它们互逆。我见过一个项目,Entity转Domain时把时间戳转成了日期字符串,但Domain转Entity时忘了转回来,结果数据保存后全乱了。
如果列表很大(比如上千条),每次映射都创建新对象,GC压力会很大。考虑使用对象池,或者用
Sequence 做懒加载映射。
数据转换的完整流程
下面这张图展示了数据从网络层到Domain层的完整流转过程:
实际项目中的Mapper组织方式
我个人习惯在数据层里创建一个 mapper 包,把所有Mapper放在一起。结构大概是这样的:
data/
├── mapper/
│ ├── UserEntityMapper.kt
│ ├── OrderEntityMapper.kt
│ └── ProductEntityMapper.kt
├── remote/
│ ├── api/
│ └── dto/
├── local/
│ ├── dao/
│ └── entity/
└── repository/
└── UserRepositoryImpl.kt
每个Mapper只负责一种实体的转换。如果某个Mapper变得很复杂,我会考虑把它拆成多个小Mapper。比如 UserProfileMapper 和 UserSettingsMapper,而不是一个巨大的 UserMapper。
总结
Mapper模式不是什么高深的技术,但它体现了Clean Architecture的核心思想——关注点分离。数据层的模型怎么变,Domain层都不受影响。你只需要修改对应的Mapper就行了。
说白了,Mapper就是一道防火墙。它把数据层的「脏乱差」挡在外面,让Domain层始终保持干净、纯粹。你在项目里用好了它,后期维护会轻松很多。
公众号:蓝海资料掘金营,微信deep3321