主角通常壓軸登場。本章來談 Clean Architecture(簡潔架構)裡最核心的 Entity,以及負責存取資料的 Repository。
一個 Entity 的誕生#
Entity 承載系統最核心的邏輯,但「最核心的邏輯」到底是什麼?常見的做法是把資料庫的 Table 一對一翻成程式物件,並稱之為 Entity。
這樣不能說全錯,但方向上把資料庫誤當成系統的核心了。資料庫的工作是儲存資料,對系統而言只是「放東西的地方」。Robert C. Martin(鮑伯大叔)認為這些雖然重要,但屬於「重要的細節」:Oracle、MySQL 靠資料庫賺錢,但你靠你的 Solution 賺錢。所以 DB Table 不該決定 Entity 的形貌,反過來才合理。
Entity 的形貌應該由 Domain Model 決定。Domain Model 描繪 Problem Domain,Entity 是 Solution 的一部分,由 Domain Model 來決定 Entity 樣貌才比較合理。
更具體一點:去問 Use Case。提出 Solution 時會與 Stakeholders 討論關鍵場景,例如申請獎學金、選課、退選;這些場景在 Clean Architecture 中由 Use Case 負責實現。在 Use Case 的實作過程中,會對關鍵角色產生「期待」,這些期待便定義了 Entity 該做的事。隨著 Use Case 越來越多,Entity 的樣貌也會逐漸完整。
Entity 是有「行為」的。物件導向的封裝精神,正是少拿對方資料、多請對方做事。資料儲存方式變了,呼叫方完全無感,自然解耦。
重構:Entity 是這樣演化出來的#
上一章保留了 Service 的一個壞味道:Feature Envy。原本程式:
private void checkDeadline(Scholarship scholarship) throws ClientSideErrorException {
LocalDate deadline = scholarship.getDeadline();
LocalDate now = LocalDate.now();
if (now.isAfter(deadline)) {
throw new ClientSideErrorException("application over time", 374);
}
}
private void checkProgramIsPhD(Student student) throws ClientSideErrorException {
if (!student.getProgram().equals("PhD")) {
throw new ClientSideErrorException("this scholarship is for PhD students only", 375);
}
}checkDeadline 用到 Scholarship 的 deadline,是典型的 Feature Envy;checkProgramIsPhD 雖然較不明顯,但「PhD 才能申請」其實是這份獎學金自身的規則,理應屬於 Scholarship。利用 Move Method 把這兩個工作搬進 Scholarship,Service 只留下流程:
private void checkDeadline(Scholarship scholarship) throws ClientSideErrorException {
if (scholarship.checkDeadline()) {
throw new ClientSideErrorException("application over time", 374);
}
}
private void checkProgramIsPhD(Scholarship scholarship, Student student) throws ClientSideErrorException {
if (!scholarship.checkQualification(student)) {
throw new ClientSideErrorException("this scholarship is for PhD students only", 375);
}
}順手把方法重新命名為 checkQualification,「檢查學生是否符合此獎學金申請資格」就成為 Scholarship 提供給 Use Case 使用的核心邏輯。
Use Case 與 Entity 的權責現在很清楚:
- Entity:根據 Domain Model 的定義檢查 Student 是否符合資格。
- Use Case:在適當時機叫 Scholarship 來檢查。
延伸一個重點:到這個階段,我們完全還沒決定 DB 的形貌,甚至還沒決定是否需要一個 DB。這正是敏捷開發推崇的「延遲決定」精神。
為 Service 服務的 Repository#
Repository 在系統中扮演「檔案管理員」的角色。Controller、Service、Entity 都做完了,介面需求也清楚了,這時設計 Repository 自然水到渠成。
Repository 的主要任務是處理 I/O 細節:資料放在 DB 就操作 DB、放在第三方 API 就去呼叫 API。它是「為 Service 服務、代為處理 I/O 細節」的無名英雄。有了 Repository,Service 就無需理會儲存細節,未來把 MySQL 換成 Oracle 或加上 Redis Cache,Service 都不必知道。
DIP 與 Repository#
依鮑伯大叔的規劃,Repository 應放在 Interface Adapter 層,直接與框架和 I/O 接觸。但 Service 在較內層的 Use Case 層,要操作 Repository 又不能違反「依賴原則 - 內層不依賴外層」,這時就需要依賴反轉原則(Dependency Inversion Principle, DIP)。
具體做法:Service 是需求方,由它自己定義介面,讓外層的 Repository 去實作。外層依賴內層完全合法;介面本身與 Service 一起放在 Use Case 層,同層依賴也沒問題。
結論#
走過 Controller、Service、Entity、Repository,整套申請獎學金的 Command 也跟著完整地走了一遍。鮑伯大叔提出的三大原則——分層原則、依賴原則、跨層原則——都在這個過程中演練了一遍。
按部就班會比較麻煩,但為了不讓不乾淨的程式碼拖慢開發節奏,這份麻煩是值得的,習慣之後也就自然了。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10272388