在使用 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」的方式,不僅能有效解決命名衝突,也能讓整體架構更模組化、更易讀、更好維護。