主角通常壓軸登場。本章來談 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 用到 Scholarshipdeadline,是典型的 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