从网络请求到业务模型:理解数据如何从后端流向业务层
Clean Architecture
问题的起点
最简单的需求
假设我们要做一个功能:从服务器获取广告数据,然后在代码中使用。
最直接的想法是什么?
// 最简单的实现
fun loadData() {
// 1. 发起网络请求
val json = httpClient.get("https://api.baidu.com/ad_list")
// 2. 手动解析 JSON
val jsonObject = JSONObject(json)
val title = jsonObject.getString("title")
val liveStatus = jsonObject.getString("live_status")
// 3. 业务逻辑
val isLiving = liveStatus == "1"
if (isLiving) {
println("$title 正在直播")
}
}这样写有什么问题?
场景 1:后端字段名变了
// 以前的接口
{
"title": "广告标题",
"live_status": "1"
}
// 后端升级后
{
"ad_title": "广告标题", // 字段名改了
"live_state": 1 // 字段名改了,类型也改了
}影响范围:
- 你需要找到所有使用
title和live_status的地方 - 可能有 10 个类、20 个方法
- 改一个字段,要改 100 多处代码
场景 2:同样的数据要在多个地方使用
// 类 A 要用
class AdManager {
fun loadData() {
val json = httpClient.get("/api/ad_list")
val jsonObject = JSONObject(json)
val title = jsonObject.getString("title")
// 解析逻辑...
}
}
// 类 B 也要用
class AdCache {
fun loadData() {
val json = httpClient.get("/api/ad_list")
val jsonObject = JSONObject(json)
val title = jsonObject.getString("title")
// 同样的解析逻辑,复制粘贴?
}
}问题:
- 同样的解析逻辑写了 N 遍
- 维护成本高(改一处要改 N 处)
- 容易出错(某个地方忘记改了)
场景 3:如何测试?
@Test
fun testLoadData() {
// 怎么测试?
// 需要真实的网络环境
// 需要真实的服务器返回数据
// 后端接口挂了,测试就跑不了
}场景 4:业务逻辑散落各处
class AdManager {
val liveStatus = jsonObject.getString("live_status")
val isLiving = liveStatus == "1" // 业务规则
}
class AdCache {
val liveStatus = jsonObject.getString("live_status")
val isLiving = liveStatus == "1" // 重复的业务规则
}
class AdReporter {
val liveStatus = jsonObject.getString("live_status")
val isLiving = liveStatus == "1" // 又重复了
}
// 相同的业务规则复制了 N 遍
// 万一规则变了:"1" 改成 "living"?
// 又要改 N 处问题总结
| 问题 | 影响 |
|---|---|
| 协议变化影响范围大 | 改一个字段要改几十处 |
| 代码重复 | 同样的解析逻辑写 N 遍 |
| 难以测试 | 依赖真实网络环境 |
| 业务逻辑分散 | 相同规则散落各处 |
| 类型不安全 | 字符串到处飞,容易出错 |
这些问题怎么解决?👇
数据流的演变
比如在一个 feed 流或视频流中,后端实际返回的 JSON 结构
{
"items": [
{
"title": "广告1",
"live_status": "1"
},
{
"title": "广告2",
"live_status": "4"
}
]
}阶段 1:原始方案(混在一起)
fun loadData() {
// 所有逻辑混在一起
val json = httpClient.get("/api/ad_list")
// 手动解析 JSON(繁琐)
val jsonObject = JSONObject(json) // 1. 解析最外层对象
val itemsArray = jsonObject.getJSONArray("items") // 2. 获取 items 数组
val firstItem = itemsArray.getJSONObject(0) // 3. 获取第一个元素
val title = firstItem.getString("title") // 4. 获取标题
val liveStatus = firstItem.getString("live_status") // 5. 获取状态
// 使用数据
println("标题: $title, 状态: $liveStatus")
}问题:所有逻辑混在一起,难以维护(网络、解析、使用都在一个方法里)
阶段 2:抽取网络层(第一次分层)
// 网络层:负责请求
class AdApi {
fun getAdList(): String {
return httpClient.get("/api/ad_list")
}
}
// 使用层
fun loadData() {
val api = AdApi()
val json = api.getAdList() // 网络请求交给 Api
// 解析还是手动的
val jsonObject = JSONObject(json) // 1. 解析最外层对象
val itemsArray = jsonObject.getJSONArray("items") // 2. 获取 items 数组
val firstItem = itemsArray.getJSONObject(0) // 3. 获取第一个元素
val title = firstItem.getString("title") // 4. 获取字段
val liveStatus = firstItem.getString("live_status")
}改进:
- ✅ 网络请求逻辑独立出来了
- ✅ 其他地方可以复用
AdApi - ❌ 但 JSON 解析还是到处都要写
- ❌ 而且解析很繁琐(4 步才能拿到数据)
阶段 1-2:抽取 Api 层 + 引入 DTO
最初所有逻辑混在一起。先把网络请求抽为 Api 层复用,再引入 DTO 让 Gson 自动解析 JSON,消除手动 parse。
阶段 3:引入 DTO(数据传输对象)
// 网络层:负责请求
class AdApi {
fun getAdList(): String {
return httpClient.get("/api/ad_list")
}
}
// 使用层
fun loadData() {
val api = AdApi()
val json = api.getAdList() // 网络请求交给 Api
// 解析还是手动的
val jsonObject = JSONObject(json) // 1. 解析最外层对象
val itemsArray = jsonObject.getJSONArray("items") // 2. 获取 items 数组
val firstItem = itemsArray.getJSONObject(0) // 3. 获取第一个元素
val title = firstItem.getString("title") // 4. 获取字段
val liveStatus = firstItem.getString("live_status")
}改进:
- ✅ 网络请求逻辑独立出来了
- ✅ 其他地方可以复用
AdApi - ❌ 但 JSON 解析还是到处都要写
- ❌ 而且解析很繁琐(4 步才能拿到数据)
阶段 4:引入 Domain Model(业务模型)
// DTO:对应 JSON 结构(数据层)
data class AdItemResponse(
val title: String,
val live_status: String // 后端返回字符串 "1"
)
// Domain Model:业务模型(业务层)
data class Ad(
val title: String,
val liveState: Int, // 转换为 Int 类型
val isLiving: Boolean // 业务规则封装
)
// 转换函数,转换成业务模型
fun toAd(response: AdItemResponse): Ad {
val state = response.live_status.toIntOrNull() ?: -1
return Ad(
title = response.title,
liveState = state,
isLiving = state == 1 // 业务规则:1 表示直播中
)
}
// 使用层
fun loadData() {
val api = AdApi()
val response = api.getAdList()
// DTO → Domain Model
val ads = response.items.map { toAd(it) }
// 使用业务模型
ads.forEach { ad ->
if (ad.isLiving) {
println("${ad.title} 正在直播")
}
}
}改进:
- ✅ 业务规则封装在
Ad模型里 - ✅ 类型转换(String → Int)封装好了
- ✅ 使用方直接用
ad.isLiving,不用关心规则 - ✅ 如果规则变了,只改
Ad模型
阶段 5:引入 Mapper(职责更清晰)
// Mapper:专门负责转换
class AdMapper {
fun map(response: AdItemResponse): Ad {
val state = response.live_status.toIntOrNull() ?: -1
return Ad(
title = response.title,
liveState = state,
isLiving = state == 1
)
}
fun mapList(responses: List<AdItemResponse>): List<Ad> {
return responses.map { map(it) }
}
}
// 使用层:职责更单一
fun loadData() {
val api = AdApi()
val mapper = AdMapper()
val response = api.getAdList() // 1. 网络请求
val ads = mapper.mapList(response.items) // 2. 数据转换
// 3. 业务逻辑
ads.forEach { ad ->
if (ad.isLiving) {
println("${ad.title} 正在直播")
}
}
}改进:
- ✅ Mapper 专门负责转换逻辑
- ✅ 职责更清晰:Api 负责网络,Mapper 负责转换
- ✅ 转换逻辑可以复用
- ✅ Mapper 可以单独测试
阶段 3-5:引入 Domain Model + Mapper
DTO 直接映射 JSON,字段全是 String,不适合写业务逻辑。引入 Domain Model 封装业务规则,Mapper 专门负责转换。
阶段 6:引入 Repository(统一入口)
// Repository:统一数据获取入口
class AdRepository(
private val api: AdApi,
private val mapper: AdMapper
) {
fun getAdList(): List<Ad> {
val response = api.getAdList() // 调用 API
return mapper.mapList(response.items) // 转换数据
}
}
// 使用层:更简洁
fun loadData() {
val repository = AdRepository(api, mapper)
val ads = repository.getAdList() // 一行搞定
// 专注于业务逻辑
ads.forEach { ad ->
if (ad.isLiving) {
println("${ad.title} 正在直播")
}
}
}改进:
- ✅ Repository 统一管理数据获取
- ✅ 使用方不用关心 API 和 Mapper 的细节
- ✅ 可以在 Repository 里加缓存、错误处理等
阶段 6:引入 Repository(统一入口)
Repository 组合 Api + Mapper,业务方一行代码获取数据,无需关心内部细节。
演变过程总结
每一次演变都在解决问题:
| 引入 | 解决的问题 |
|---|---|
| Api 层 | 复用网络请求,分离 IO |
| DTO | 自动化 JSON 解析 |
| Domain Model | 封装业务规则,类型安全 |
| Mapper | 转换逻辑独立,可单测 |
| Repository | 统一数据入口,屏蔽实现细节 |
完整的数据流架构
最终形成的分层架构
架构图
object ApiClient {
private val retrofit = Retrofit.Builder()
.baseUrl(“https://api.baidu.com”)
.addConverterFactory(GsonConverterFactory.create())
.build()
val adApi: AdApi = retrofit.create(AdApi::class.java)
}
// ======== Data Layer ========
data class AdListResponse(
@SerializedName(“items”)
val items: List
)
data class AdItemResponse(
@SerializedName(“title”)
val title: String,
@SerializedName("live_status")
val liveStatus: String,
@SerializedName("video_url")
val videoUrl: String
)
// ======== Mapper Layer ========
class AdMapper {
fun map(dto: AdItemResponse): Ad {
return Ad(
title = dto.title,
liveState = dto.liveStatus.toIntOrNull() ?: -1,
videoUrl = dto.videoUrl
)
}
fun mapList(dtos: List<AdItemResponse>): List<Ad> {
return dtos.map { map(it) }
}
}
// ======== Domain Layer ========
data class Ad(
val title: String,
val liveState: Int,
val videoUrl: String
) {
val isLiving: Boolean
get() = liveState == 1
val statusText: String
get() = when (liveState) {
1 -> "直播中"
else -> "非直播"
}
}
class AdRepository(
private val api: AdApi,
private val mapper: AdMapper
) {
suspend fun getAdList(): List
val response = api.getAdList()
return mapper.mapList(response.items)
}
suspend fun getLivingAds(): List<Ad> {
val ads = getAdList()
return ads.filter { it.isLiving }
}
}
// ======== 业务代码 ========
class AdManager(
private val repository: AdRepository
) {
suspend fun loadLivingAds() {
val livingAds = repository.getLivingAds()
livingAds.forEach { ad ->
println("${ad.title} - ${ad.statusText}")
}
}
}
// ======== 使用 ========
suspend fun main() {
val api = ApiClient.adApi
val mapper = AdMapper()
val repository = AdRepository(api, mapper)
val manager = AdManager(repository)
manager.loadLivingAds()
}
### 数据流动过程
```plain
1. manager.loadLivingAds()
↓
2. repository.getLivingAds()
↓
3. api.getAdList() → 网络请求
↓
4. 后端返回 JSON:
{ "items": [
{ "title": "广告1", "live_status": "1", "video_url": "..." },
{ "title": "广告2", "live_status": "4", "video_url": "..." }
]}
↓
5. Gson 自动解析为 AdListResponse
↓
6. mapper.mapList() 转换为 List<Ad>
[ Ad(title="广告1", liveState=1, isLiving=true),
Ad(title="广告2", liveState=4, isLiving=false) ]
↓
7. filter { it.isLiving } 筛选
[ Ad(title="广告1", liveState=1, isLiving=true) ]
↓
8. 返回给业务层使用
Network Layer - 网络层
定义 API 接口,发起 HTTP 请求,返回 DTO。
interface AdApi {
@GET("/api/ad_list")
suspend fun getAdList(): AdListResponse
}
object ApiClient {
private val retrofit = Retrofit.Builder()
.baseUrl("https://api.baidu.com")
.addConverterFactory(GsonConverterFactory.create())
.build()
val adApi: AdApi = retrofit.create(AdApi::class.java)
}接口与实现分离,方便 Mock:
// ✅ 接口定义
interface AdApi {
suspend fun getAdList(): AdListResponse
}
// ✅ 生产实现
class RetrofitAdApi : AdApi {
override suspend fun getAdList(): AdListResponse { /* Retrofit 实现 */ }
}
// ✅ 测试实现
class MockAdApi : AdApi {
override suspend fun getAdList(): AdListResponse {
return AdListResponse(items = listOf(
AdItemResponse("测试广告", "1", "http://video.mp4")
))
}
}Data Layer - 数据层
DTO 直接对应后端 JSON 结构,不含业务逻辑。
data class AdListResponse(
@SerializedName("items")
val items: List<AdItemResponse>
)
data class AdItemResponse(
@SerializedName("title")
val title: String,
@SerializedName("live_status")
val liveStatus: String, // 注意:后端返回字符串
@SerializedName("video_url")
val videoUrl: String
)Mapper Layer - 映射层
纯函数,职责单一,只做 DTO → Domain Model 的转换。
class AdMapper {
fun map(dto: AdItemResponse): Ad {
return Ad(
title = dto.title,
liveState = dto.liveStatus.toIntOrNull() ?: -1,
videoUrl = dto.videoUrl
)
}
fun mapList(dtos: List<AdItemResponse>): List<Ad> {
return dtos.map { map(it) }
}
}// ✅ 纯函数,只负责转换
class AdMapper {
fun map(dto: AdItemResponse): Ad {
return Ad(...)
}
}
// ❌ 不要有副作用
class AdMapper {
private var lastResult: Ad? = null
fun map(dto: AdItemResponse): Ad {
val result = Ad(...)
lastResult = result // 副作用
return result
}
}
// ❌ 不要混入其他职责
class AdMapper {
fun map(dto: AdItemResponse): Ad {
val ad = Ad(...)
database.save(ad) // ❌
analytics.track() // ❌
Logger.d("$ad") // ❌
return ad
}
}Domain Layer - 业务层
Domain Model
封装业务规则和计算属性,独立于数据来源和 UI。
data class Ad(
val title: String,
val liveState: Int, // ✅ 已由 Mapper 从 String 转为 Int
val videoUrl: String
) {
val isLiving: Boolean get() = liveState == 1
val statusText: String
get() = when (liveState) {
1 -> "直播中"
else -> "非直播"
}
}规则封装在模型里,而不是散落在使用方:
// ✅ 封装在模型
if (ad.isLiving) { /* 显示直播标签 */ }
// ❌ 散落在各处,到处写魔法数字
if (ad.liveState == 1) { /* 显示直播标签 */ }Repository
统一数据获取入口,组合 Api + Mapper。
class AdRepository(
private val api: AdApi,
private val mapper: AdMapper
) {
suspend fun getAdList(): List<Ad> {
val response = api.getAdList()
return mapper.mapList(response.items)
}
suspend fun getLivingAds(): List<Ad> {
val ads = getAdList()
return ads.filter { it.isLiving }
}
}// ✅ 通过 Repository 获取
val ads = repository.getLivingAds()
// ❌ 直接用 Api + Mapper
val response = api.getAdList()
val ads = mapper.mapList(response.items).filter { it.isLiving }扩展示例:加缓存
class AdRepository(
private val api: AdApi,
private val mapper: AdMapper,
private val cache: AdCache
) {
suspend fun getAdList(): List<Ad> {
val cachedAds = cache.getAds()
if (cachedAds.isNotEmpty()) return cachedAds
val response = api.getAdList()
val ads = mapper.mapList(response.items)
cache.saveAds(ads)
return ads
}
}扩展示例:加错误处理
suspend fun getAdList(): Result<List<Ad>> {
return try {
val response = api.getAdList()
Result.Success(mapper.mapList(response.items))
} catch (e: IOException) {
Result.Error("网络错误")
} catch (e: Exception) {
Result.Error("未知错误")
}
}
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
}为什么要这样分层
核心:关注点分离,每层只管自己的事。
| Layer | 职责 | 不做什么 |
|---|---|---|
| Network | 定义接口、发起请求 | 不解析、不含业务逻辑 |
| Data (DTO) | 对应 JSON 结构 | 不含业务逻辑、不做类型转换 |
| Mapper | 数据转换 | 不访问网络、不含业务逻辑 |
| Domain | 业务模型和规则 | 不关心数据来源和格式 |
隔离变化
// 后端从 v1 升级到 v2: { "live_status": "1" } → { "live_state": 1 }
// ❌ 不分层:100 处要改
class ClassA { val state = json.getString("live_status").toInt() }
class ClassB { val state = json.getString("live_status").toInt() }
// ✅ 分层:只改 Mapper 一处
class AdMapper {
fun map(dto: AdItemResponse): Ad {
liveState = dto.liveState ?: -1 // 只改这里
}
}易于测试
@Test fun testMapper() {
val dto = AdItemResponse(title = "测试", liveStatus = "1", videoUrl = "url")
val ad = mapper.map(dto)
assertEquals(1, ad.liveState)
assertTrue(ad.isLiving)
}
@Test fun testAdModel() {
val ad = Ad(title = "测试", liveState = 1, videoUrl = "url")
assertTrue(ad.isLiving)
assertEquals("直播中", ad.statusText)
}
@Test fun testRepository() {
val repository = AdRepository(MockAdApi(), AdMapper())
assertEquals(10, repository.getAdList().size)
}代码复用
class AdManager {
private val repository = AdRepository(api, mapper)
fun loadAds() { val ads = repository.getAdList() }
}
class AdCache {
private val repository = AdRepository(api, mapper)
fun refreshCache() { val ads = repository.getAdList() }
}
val ads = dtos.map { mapper.map(it) }业务规则集中
// ✅ 所有地方用同一套规则
data class Ad(val liveState: Int) {
val isLiving: Boolean get() = liveState == 1
}
if (ad.isLiving) { ... }
// ❌ 规则散落,容易不一致
class ClassA { if (liveState == 1) { ... } }
class ClassB { if (liveState == "1") { ... } } // 类型错误
class ClassC { if (liveState == 1 || liveState == 3) { ... } } // 逻辑不同团队协作
后端改接口,前端只改 DTO + Mapper,业务层不受影响。
719 网络请求到业务方调用
719 各个 Bean 对应的 JSON 层级
| 类型 | 内容 | 对应的 JSON |
|---|---|---|
AdListBean | 网络响应最外层,包含广告列表+策略 | json { "errno": "0", "timestamp": "1770195063000", "data": { ... } |
AdListItemBean | 单条广告的壳,含 id/nid/layout + data | json "data": { "306": { "items": [{}], ... } } |
AdListItemDataBean | 单条广告的实际数据,130+ 字段,大量 String | json “data”: { “306”: { “items”: [{ { “id”: “ad1_1944316529648536976”, “nid”: “ad1_1944316529648536976”, “layout”: “ad_flow_video”, “tag”: “广告”, “data”: { … } }], … } } |
AdPolicyBean | PV 级别的楼层控制策略 | json “data”: { “306”: { “items”: [{}], “ad_policy”: { … } } } |
AdItemModel | 广告业务模型,强类型,带少量业务方法 | |
AdRunTime | 广告运行时可变状态(点击/曝光/磁贴状态等) | |
FlowItem<T> | 广告模块里的叫法,拆仓库来的 | |
ItemModel<T> | 列表通用容器,装视频/广告/直播都行 | |
RunTimeStatus | 列表层运行时状态(曝光时间/位置等),ItemModel 持有 | |
AdData | 广告数据从AdItemModel裁剪来的 | |
AdDataState | LiveData 包装的 AdData,供 UI 组件 observe |
更新: 2026-03-05 14:37:36
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/ig9sh2xfoggdrqvw