iT邦幫忙

2026 iThome 鐵人賽

DAY 29
1

配合分支:day29/start,Repo: 按我

雖然昨天將測試補完,不過出現了一些討人厭的地方。像是製作測試用的RecordingDiaryStore,只希望讀取,不要寫入。但因為契約,需要實作兩者。

interface 不需要包山包海,實務上來說 interface 可以切得更細,不必去實作沒必要的契約。第二點是 class 可以一次實作多個 interface 契約(這招叫做組合 Composition)。所以今天就來調整 DiaryStore,將讀取抽出去,改叫 DiaryReader。讓需要「讀取」日記的 class 只需要實作 Reader 這個契約就好了。

移除 DiaryStore 的讀取:

interface DiaryStore {
    - fun load(): List<DiaryEntry>?  // <= 移除

    fun save(entries: List<DiaryEntry>)
}

接著新增一個全新的 interface,用來作讀取的契約。

interface DiaryReader {
    fun load(): List<DiaryEntry>?
}

interface 也可以進行繼承,將 DiaryStore 繼承 DiaryReader,這樣原來實作 DiaryStore 的 class 就不用調整。

現在更新 DiaryExporter 以前在建構子塞 DiaryStore 進來,現在調整成 DiaryReader。調整成 diaryReader 去讀取內容即可,這樣測試那邊建立的假 class 也可以變的更乾淨。

https://ithelp.ithome.com.tw/upload/images/20260917/20141615jEdbRcQi7i.png

就不需要額外實作一個尷尬的 save()

補充

介面隔離原則(Interface Segregation Principle, ISP)

今天做的事情也有個名字,叫做介面隔離原則。經典的說法是:不要強迫別人實作用不到的方法(clients should not be forced to depend on methods they do not use)。翻成白話就是:人家只是要借個 load(),就不要逼他連 save() 都一起寫下去。

回頭看昨天的 ReadOnlyDiaryStore 為何尷尬?DiaryExporter 從頭到尾只搓過 load(),它建構子傳入的是 DiaryStore,而 DiaryStore 規定要有 load()save() 兩個方法。於是測試用的假 class 明明只想餵假資料,卻被逼著多寫一個 save(),還得在裡頭丟 Error 出去,才能確定沒人偷叫它。用不到的是 DiaryExporter,卻由假 class 來收拾殘局。

今天把 load() 再抽出去 DiaryReader,再讓 DiaryStore 去繼承它,分工就變成:

  • DiaryExporter 傳入 DiaryReader,看得到的只有 load()
  • PhotoDiaryViewModel 要新增日記、改心情,會寫入,繼續拿 DiaryStore
  • LocalDiaryStorage 一行都沒改,它實作的 DiaryStore 本來就同時是 DiaryReader,同一個物件兩邊都能塞

想驗證 DiaryExporter 手上真的只剩讀取?把建構子的 DiaryReader 偷偷改回 DiaryStore,再跑一次測試,ReadOnlyDiaryStore 那邊會直接 Failed,因為它已經沒有 save() 可以用了。改回去才會 Pass。

跟昨天的 OCP 有什麼不一樣?

Day 27 和今天都在抽 interface,但方向不同:

  • OCP 主要處理:需求來了要改哪裡? 加第三種格式只新增一個 class,舊的不動。
  • ISP 主要處理:拿的人到底要會多少? DiaryExporter 只需要會讀,那就只給它讀。

不是要「interface 越多越好」。
需要有人真的只用一半,才把那一半切出來給他。

給新手專區

聽到「interface 要切細」,第一個反應可能是:那我每個 Function 都開一個 interface 不就無敵了?拜託住手。今天沒有順手弄出 DiaryWriter,因為整個 App 沒有任何人「只寫不讀」,切出來就是沒人聽的滯銷唱片(不可能不知道 CD 📀 吧⋯⋯),純粹超前部署。

什麼時候切才有理?跟昨天的 OCP 一樣,要等事情真的發生:有人拿著整個 DiaryStore 卻只用 load(),而且多出來的 save() 已經開始礙事(像昨天那個丟 Error 的假 class)。在那之前,load()save() 擠在同一個 interface 裡完全 OK,從 Day 18 用到 Day 28 不是也都沒事?


上一篇
[Day28] 想要更多安全感?加測試
下一篇
[Day30] 終章。當 AI 來敲門
系列文
我的第一個手作りAndroid App!快樂學習物件導向程式設計!30
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
chiaominchang222
iT邦新手 4 級 ‧ 2026-09-17 21:23:05

不行了閱讀不了半個字了

0
AndyAWD
iT邦研究生 5 級 ‧ 2026-09-18 00:05:04

最後一天!

我要留言

立即登入留言