【Android 架構系列 04】核心防禦:Domain 與 Data Layer 設計——防止 AI 污染業務邏輯
01. 前言:建立「同心圓」防線
這是系列文的第四篇。在上一篇,我們利用 Hilt 解決了物件的「組裝」問題。現在,我們的架構已經有了骨架(SOLID)和關節(DI)。接下來,我們要開始填充最重要的「大腦」與「血液」。
這篇將深入探討 Clean Architecture 的核心戰略:如何利用 Domain Layer 與 Data Layer 的分層設計,防止 AI 把外部的「髒資料」直接運送到你的核心業務邏輯中。
如果把你的 App 比喻為一座城堡:
- UI (Presentation Layer) 是城門和城牆,負責接待使用者。
- Data (Data Layer) 是補給線,負責從外部(API/DB)運送物資。
- Domain (Domain Layer) 則是城堡最深處的國王大廳,這裡制定了所有的規則與決策。
在 AI 輔助開發的場景下,最危險的事情就是讓 AI 直接把補給線上的「生肉」(未經處理的 API 資料)直接扔進國王大廳,甚至扔到城門口(UI)。
如果沒有明確的邊界,AI 非常喜歡寫出 if (apiResponse.article.source?.name != null) 這種防禦性程式碼散落在 UI 各處。這篇將教你如何設計 Domain 與 Data 層,確保 AI 只能在受控的範圍內工作。
02. Domain Layer:AI 的「絕對安全區」
這是整個架構中最神聖的一層。
鐵律: 這裡只有純 Kotlin 程式碼,嚴禁出現 import android.* (除了少數如 @Parcelize 以外)。
為什麼要對 AI 施加這種限制?因為 AI 非常容易產生「依賴幻覺」。如果你允許它引用 Android SDK,它可能就會在業務邏輯裡偷偷塞入 Context 來讀取字串資源,或者用 Toast 顯示錯誤,這會導致單元測試變得寸步難行。
A. 實體 (Entities):定義你的世界
不要使用後端回傳的 JSON 物件 (DTO) 作為你的核心資料結構。後端的欄位會變、命名會爛(例如 is_vip_flag_01),你的 App 不應該被這些髒東西汙染。
Prompt 策略:
"請在 Domain 層定義一個 Article data class。這應該是 App 內部使用的純淨物件,不包含任何 Gson/Moshi 註解,也不要繼承任何框架類別。"
// domain/model/Article.kt
// 這是我們 App 自己定義的「新聞」,不管後端 API 怎麼改,這個物件盡量不動
data class Article(
val id: String,
val title: String,
val imageUrl: String?, // 允許為 null,UI 層再決定怎麼顯示
val publishedAt: Date, // 這裡用強型別 Date,不要用 String
val sourceName: String
)
B. UseCase:原子化的業務邏輯
這是 AI 協作的神器。我們將每個業務規則拆解成極小的 UseCase 類別。
傳統寫法 (Bad): 在 ViewModel 裡寫 if (article.sourceName == "Google News" && !article.isRead)。
AI 友善寫法 (Good): 獨立為 FilterImportantNewsUseCase。
為什麼這對 AI 很重要?
當你要求 AI:「修改過濾邏輯,現在來源包含 'BBC' 的也算重要新聞」時:
- 沒有 UseCase: 你要把整包 ViewModel 丟給它,它可能會不小心改壞 UI 狀態流。
- 有 UseCase: 你只丟給它這個不到 20 行的檔案。上下文極度乾淨,AI 犯錯機率趨近於零。
實戰 Prompt:
"請實作 GetNewsUseCase。邏輯:從 Repository 取得新聞後,過濾掉沒有標題的文章,並依照時間排序。"
// domain/usecase/GetNewsUseCase.kt
class GetNewsUseCase @Inject constructor(
private val repository: NewsRepository
) {
// operator fun invoke 讓 UseCase 可以像函式一樣被呼叫
suspend operator fun invoke(): Result<List<Article>> {
return repository.getNews().map { articles ->
// AI 在這裡可以專注寫邏輯,完全不用管 UI 怎麼顯示
articles
.filter { it.title.isNotBlank() }
.sortedByDescending { it.publishedAt }
}
}
}
03. Data Layer:強制 AI 進行「資料清洗」
Data Layer 是負責髒活的地方(打 API、讀資料庫)。這裡充滿了 null、不規範的命名和未知的異常。
筆者最強調的 AI 防禦機制是:Mapper (轉換器)。
關鍵元件:Mapper (DTO -> Domain)
AI 在寫程式碼時常有一個壞習慣:為了省事,直接把 API 回傳的 Response 物件一路傳到 UI 層。這就是「技術債」的開始。
我們必須在 Data Layer 設立「海關」,強制 AI 進行資料清洗。
實戰場景: NewsAPI 回傳的 author 可能是 null,publishedAt 是 ISO 字串,需要轉成 Date 物件。
Prompt 策略:
"請實作一個 Extension Function,將 ArticleDto 轉換為 Domain 層的 Article 物件。 規則:
- 如果 title 是 null,過濾掉或給預設值。
- 將 String 時間格式解析為 Date 物件。
- 確保回傳的 Domain 物件欄位符合定義。"
AI 生成的代碼:
// data/mapper/NewsMapper.kt
// DTO: 後端給的髒資料
data class ArticleDto(
@SerializedName("title") val title: String?,
@SerializedName("urlToImage") val imageUrl: String?,
@SerializedName("publishedAt") val dateStr: String?,
@SerializedName("source") val source: SourceDto?
)
// Mapper: AI 的消毒工作
fun ArticleDto.toDomain(): Article {
return Article(
// AI 自動幫你處理了 null safety
id = UUID.randomUUID().toString(), // 補齊缺少的 ID
title = this.title ?: "無標題",
imageUrl = this.imageUrl,
// 處理日期解析邏輯,這裡如果不寫 Mapper,就會散落在 UI 層
publishedAt = DateUtils.parseIso8601(this.dateStr) ?: Date(),
sourceName = this.source?.name ?: "未知來源"
)
}
筆者點評: 透過 Mapper,我們把所有「處理髒資料」的醜陋程式碼都鎖死在 Data Layer。 當後端 API 爆炸時(例如日期格式改了),你只需要叫 AI:「去修 NewsMapper」,而不需要驚動國王大廳 (Domain) 或城牆 (UI)。
Repository 實作:串接一切
最後,Repository 的實作就是把 API 和 Mapper 串起來的地方。
Prompt:
"請實作 NewsRepository 介面。
- 從 NewsApi 取得 DTO。
- 使用 toDomain() 轉換。
- 使用 runCatching 處理所有網路異常,回傳 Result 型別。"
// data/repository/NewsRepositoryImpl.kt
class NewsRepositoryImpl @Inject constructor(
private val api: NewsApi
) : NewsRepository {
override suspend fun getNews(): Result<List<Article>> {
return runCatching {
val response = api.getTopHeadlines("tw", apiKey)
// 關鍵:在這裡進行轉換,如果不成功會直接拋出 Exception 被 runCatching 捕獲
response.articles.map { it.toDomain() }
}
}
}
04. 總結:架構即是「隔離」
在這一篇,我們完成了 App 的核心建設:
- Domain Layer: 我們建立了純淨的業務邏輯區,讓 AI 透過 UseCase 精準執行指令,避免依賴幻覺。
- Data Layer: 我們建立了嚴格的海關 Mapper,強迫 AI 在資料進入核心前完成清洗。
現在,我們的核心邏輯已經固若金湯。無論 API 怎麼變,Domain 層的 Article 和 UseCase 都不受影響。
但現實世界沒那麼簡單。App 不只要從 API 拿資料,還要有 離線瀏覽 功能。當我們同時有資料庫和 API 兩個來源時,AI 最容易寫出「資料不同步」的 Bug。
在下一篇,我們將進入深水區,探討 Offline-First (離線優先) 架構,看看如何讓 AI 寫出「單一真相來源 (Single Source of Truth)」的代碼。
下集預告:【Part 5】離線優先 (Offline-First):當 AI 遇上快取——如何設計 Single Source of Truth 避免資料打架

關於作者
Ken Huang
熱衷於嘗試各類新技術的軟體開發者,現階段主力為 Android / iOS 雙平台開發,同時持續深耕前端與後端技術,以成為全端工程師與軟體架構師為目標。
最廣為人知的代表作為 BePTT。開發哲學是「以做身體健康為前提」,致力於在工作與生活平衡的基礎上,打造出擁有絕佳使用體驗的軟體服務。
這裡是用於紀錄與分享開發經驗的空間,希望能透過我的實戰筆記,幫助開發者解決疑難雜症並提升效率。
系列文章目錄
- 【Android 架構系列 02】與 AI 的契約:重新解讀 SOLID 原則
- 【Android 架構系列 03】依賴注入 (DI):讓 Hilt 成為 AI 的「接線生」
- 【Android 架構系列 04】核心防禦:Domain 與 Data Layer 設計——防止 AI 污染業務邏輯 (本文)
- 【Android 架構系列 05】離線優先 (Offline-First):當 AI 遇上快取——如何設計 Single Source of Truth 避免資料打架
- 【Android 架構系列 06】錯誤處理 (Error Handling):別讓 AI 吞掉你的 Exception