从网络请求到业务模型:理解数据如何从后端流向业务层

Clean Architecture

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             // 字段名改了,类型也改了
}

影响范围

  • 你需要找到所有使用titlelive_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统一数据入口,屏蔽实现细节

完整的数据流架构

最终形成的分层架构

架构图

613700cb631f523b8e1414eb76100279.svg ### 数据流方向 ```plain 后端 JSON ↓ (HTTP Response) Network Layer (AdApi) ↓ (Gson 解析) Data Layer (DTO: AdItemResponse) ↓ (Mapper 转换) Mapper Layer (AdMapper) ↓ (得到业务模型) Domain Layer (Domain Model: Ad) ↓ (Repository 提供) 业务代码使用 ``` ## Demo 演示 **场景:获取广告列表并筛选直播中的广告** ```kotlin // ========== Network Layer ========== 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)

}

// ======== 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 插楼流程

719 各个 Bean 对应的 JSON 层级

类型内容对应的 JSON
AdListBean网络响应最外层,包含广告列表+策略json { "errno": "0", "timestamp": "1770195063000", "data": { ... }
AdListItemBean单条广告的壳,含 id/nid/layout + datajson "data": { "306": { "items": [{}], ... } }
AdListItemDataBean单条广告的实际数据,130+ 字段,大量 Stringjson “data”: { “306”: { “items”: [{ { “id”: “ad1_1944316529648536976”, “nid”: “ad1_1944316529648536976”, “layout”: “ad_flow_video”, “tag”: “广告”, “data”: { … } }], … } }
AdPolicyBeanPV 级别的楼层控制策略json “data”: { “306”: { “items”: [{}], “ad_policy”: { … } } }
AdItemModel广告业务模型,强类型,带少量业务方法
AdRunTime广告运行时可变状态(点击/曝光/磁贴状态等)
FlowItem<T>广告模块里的叫法,拆仓库来的
ItemModel<T>列表通用容器,装视频/广告/直播都行
RunTimeStatus列表层运行时状态(曝光时间/位置等),ItemModel 持有
AdData广告数据从AdItemModel裁剪来的
AdDataStateLiveData 包装的 AdData,供 UI 组件 observe

更新: 2026-03-05 14:37:36
原文: https://www.yuque.com/dongpozhouzi-mshe3/zhm85g/ig9sh2xfoggdrqvw


相关笔记