在使用 Kotlin 撰寫多個 API 對應資料模型時,若同一專案中出現多個相同命名的 `data class`(如 `Team`、`Cup`、`Pivot`),就會發生 **「Redeclaration」** 錯誤。 這篇文章將說明這個問題的原因、幾種常見解法,以及推薦的最佳實作方式。 --- ## 問題描述:命名衝突 假設你有兩個 API: - `/teams/list` - `/teams/create` ### Create ```kotlin package com.rainbowt0506.example.data.model.response.team /** { "data": { "name": "草莓一族", "level": 1, "order": 0, "is_top": 0, "logo": "https://main-admin.rainbowt0506.app/storage/teams/PCefhKvkLZHIiHwXYV8txQFYgL5yT3ghWWga6tgB.jpg", "id": 21761, "cups": [ { "id": 8441, "name": "2025", "pivot": { "team_id": 21761, "cup_id": 8441 } } ] }, "status": 200, "message": "Success" } */ data class CreateTeamResponse( val `data`: Team, val message: String, val status: Int ) data class Team( val cups: List<Cup>, val id: Int, val is_top: Int, val level: Int, val logo: String, val name: String, val order: Int ) data class Cup( val id: Int, val name: String, val pivot: Pivot ) data class Pivot( val cup_id: Int, val team_id: Int ) ``` ### List ```kotlin package com.rainbowt0506.example.data.model.response.team import java.io.File /** { "data": { "teams": [ { "id": 19710, "name": "starbuck", "logo": "https://main-admin.rainbowt0506.app/storage/teams/hoPHfO8fY6ELH8VkyfM6By95fC4hqFsCTExjaNIP.png", "level": 2, "order": 0, "is_top": 1745033227, "deleted_at": null, "cups": [ { "id": 8441, "name": "2025", "level": 0, "order": 0, "is_top": 0, "deleted_at": null, "pivot": { "team_id": 19710, "cup_id": 8441 } } ] }, { "id": 18652, "name": "統一", "logo": "https://main-admin.rainbowt0506.app/storage/teams/E1oGXtNJR8EY7vGIHNgUua0PPoqaEYeic0yWS239.jpg", "level": 4, "order": 0, "is_top": 0, "deleted_at": null, "cups": [] }, { "id": 18651, "name": "富邦", "logo": "https://main-admin.rainbowt0506.app/storage/teams/hriNB6jVEUb4FoFA8KfpI0pzIdcQ6pKxgnE0FoLu.jpg", "level": 4, "order": 0, "is_top": 1745033236, "deleted_at": null, "cups": [] } ] }, "status": 200, "message": "Success" } */ data class TeamResponse( val teamList: TeamList, val message: String, val status: Int ) data class TeamList( val teams: List<Team> ) data class Team( val cups: List<Cup>, val deleted_at: Int, val id: Int, val is_top: Int, val level: Int, val logo: String, val name: String, val order: Int ) data class Cup( val deleted_at: Int, val id: Int, val is_top: Int, val level: Int, val name: String, val order: Int, val pivot: Pivot ) data class Pivot( val cup_id: Int, val team_id: Int ) data class TeamUpdate( val id: Int, val name: String? = null, val level: Int? = null, val order: Int? = null, val isTop: Int? = null, val cupIds: List<Int>? = null, val logoFile: File? = null ) ``` --- 這兩個 API 的 response 都會回傳 `Team`、`Cup`、`Pivot` 等結構,但欄位內容或格式略有不同,導致我們需要建立各自對應的 `data class`。 然而,若這些 class 被定義在相同 package 下,如: ```kotlin package com.rainbowt0506.example.data.model.response.team data class Team(...) data class Cup(...) data class Pivot(...) ``` 當你嘗試為另一個 API 定義相同名稱的 class 時,就會遇到: ``` Redeclaration: Class 'Team' is already defined in the scope ``` --- ## 解法一覽 | 方法 | 說明 | 優點 | 缺點 | 推薦程度 | |------|------|------|------|----------| | **1. 拆分 package** | 為不同 API 建立不同 package | 清楚、有組織、易維護 | 初期需要調整結構 | ⭐⭐⭐⭐⭐ | | 2. 改類名(加前綴/後綴) | 如 `ListTeam`, `CreateTeam` | 實作快,不需調整目錄 | 類名變長,可讀性下降 | ⭐⭐⭐ | | 3. 合併成一份 class | 所有欄位變 optional | 省事一時 | 高耦合、易錯、不易維護 | ⭐ | --- ## 其他常見但不推薦的做法 ### 2. 改類名(加前綴/後綴) 這是在專案初期常見的快速解法,只需要修改 class 名稱本身,不需要變動 package 結構。 #### 範例 ```kotlin data class ListTeam(...) data class CreateTeam(...) data class ListCup(...) data class CreateCup(...) ``` #### 缺點 - **命名失去一致性**:本來 `Team` 是個泛用名詞,現在卻要根據 API 加上前綴,降低語意的純粹性。 - **可讀性下降**:開發者必須時時關注「這是哪個 API 的 Team?」,增加心智負擔。 - **容易失控**:當 API 增多,出現 `EditTeam`、`DetailTeam`、`TransferTeam` 等命名時,整體結構開始混亂。 - **IDE 搜尋不便**:搜尋 `Team` 找不到全部定義,無法直覺了解整體模型。 這也是許多團隊會在初期採用,但後來轉向其他方案的原因。 --- ### 3. 合併成一份 class(不推薦) 有些開發者會將不同 API 對應的資料合併成一份 class,透過 `nullable` 屬性或預設值來支援多種格式。 #### 範例 ```kotlin data class Team( val id: Int, val name: String?, val level: Int?, val order: Int?, val isTop: Int?, val logo: String?, val cups: List<Cup>?, // 有些 API 沒有 val deletedAt: Int? // 有些 API 沒有 ) ``` #### 缺點 - **耦合度過高**:一個 `Team` 類型要負責所有 API 可能出現的欄位,職責過多。 - **不易維護**:API 結構一改,就得回頭改這個共用類別,容易引發 side effect。 - **資料不明確**:哪些欄位是一定有、哪些是選擇性?光看 class 無法分辨。 - **測試困難**:無法針對不同 API 寫獨立的單元測試模型。 這種方式雖然短期內看起來省事,但長期來看維護成本高,容易出 bug。 ## ✅ 推薦做法:不同 API 放不同 package 這是最乾淨也最推薦的做法,對於後續擴充與維護極為友善。 ### 資料夾結構建議 ``` com.rainbowt0506.example.data.model.response.team ├── list/ │ └── TeamResponse.kt │ └── Team.kt │ └── Cup.kt │ └── Pivot.kt ├── create/ │ └── CreateTeamResponse.kt │ └── Team.kt │ └── Cup.kt │ └── Pivot.kt ``` ### **為什麼每個 API 一個 package 是推薦做法?** #### 1. **防止類別衝突** 就像你遇到的 `Team`、`Cup`、`Pivot` 重名問題,在獨立 package 裡根本不會出現。 #### 2. **清楚的上下文** 若有需要同時使用兩個 package 下的 `Team`: 看 package 名稱就知道這個資料類型是給哪個 API 用的: ```kotlin com.rainbowt0506.example.data.model.response.team.list.Team com.rainbowt0506.example.data.model.response.team.create.Team ``` → 不需要靠類名改名(如 ListTeam、CreateTeam)就能知道是哪裡來的。 如此即可避免任何命名衝突,同時保有清晰語意。 #### 3. **維護方便** 未來如果 API 結構改了,只會影響自己那個 package 的 class,不會殃及其他。 #### 4. **支援多版本 API** 假設你之後支援 API v1、v2,package 分開後超級好管理: ``` .team.v1.list .team.v2.list ``` --- ## 疑問 ### 1. 「資料模型轉換(Model Mapping)」:三種常見的模型橋接策略 #### ✅ **解法一:建立 UI 專用的 ViewModel / DTO(推薦)** 建立一個只給 UI 使用的資料結構,不跟 API 資料耦合,責任清楚。 ```kotlin data class TeamEditUiModel( val id: Int, val name: String, val logoUrl: String, val isSelected: Boolean = false, val cups: List<Int> = emptyList(), val level: Int? = null, val isTop: Boolean = false ) ``` 轉換方式: ```kotlin fun Team.toUiModel(): TeamEditUiModel = TeamEditUiModel( id = id, name = name, logoUrl = logo, level = level, isTop = is_top != 0, cups = cups.map { it.id } ) ``` - **優點**:模組清晰、可測試、維護容易。 - **缺點**:初期可能需要寫一些 mapper 轉換邏輯。 --- #### **解法二:共用同一個 `Team` 類別,加 nullable 或 UI 專用欄位(不建議)** ```kotlin data class Team( val id: Int, val name: String, val logo: String, val cups: List<Cup>?, val isSelected: Boolean? // UI 要求加入 ) ``` - **優點**:快速上手,減少資料類別數量。 - **缺點**:class 變得模糊、肥大、難測試,會破壞模型邊界。 --- #### ❗ **解法三:強轉型、過度使用泛型或 Map(踩雷,==我遇過!!!==)** 某些團隊會用 `Map<String, Any>` 或泛型模型來「偷渡」不同資料: ```kotlin val teamData: Map<String, Any> = mapOf( "id" to 123, "name" to "starbuck", "logo" to "url...", "isSelected" to true ) ``` 或者: ```kotlin class GenericModel(val data: Any) ``` - **優點**:快速又無視型別。 - **缺點**: - 型別安全完全破壞,IDE 無法協助檢查錯誤。 - 測試與維護極困難。 - 會讓未來接手的工程師想翻桌。 --- ## 結語 當你的專案開始整合越來越多 API,資料模型的結構設計就變得非常關鍵。使用「每個 API 一個 package」的方式,不僅能有效解決命名衝突,也能讓整體架構更模組化、更易讀、更好維護。