l

2024年9月18日 星期三

重構既有系統,邁向整潔架構 (6):第三回合,在使用案例層讀寫分離

September 18 17:52~18:52

▲這一集換Eiffel上場

 

工商服務

想了解本系列文章完整內容,請參考重構既有系統:邁向整潔架構實作班。課程介紹與報名網址在此:https://teddysoft.tw/courses/refactor-to-ca/

***

前言

上一集<重構既有系統,邁向整潔架構 (5):第二回合,套用DDD戰術模式>在基本領域模型中套用領域驅動設計(DDD)的戰術模式(Tactical Design Pattern),找出聚合(Aggregate),達到封裝與決定交易邊界的目的。這一集要處理程式碼最多的TaskList。

***

分而治之

TaskList程式碼如圖1、圖2所示,這時候可以將execute mehtod所呼叫的哪幾個private method升等成Class,以簡化TaskList的長度,並且可以觀察原本這些private method與TaskList之間的耦合關係。

 

▲圖1:TaskList程式碼,1/2

 

▲圖2:TaskList程式碼,2/2

***

Use Cases Layer新住戶

Teddy將這些被升等成類別的private method放到usecase package,如圖3所示。現在Clean Architecture已經慢慢成形,最重要的兩層(entity與usecase)已經都有「住戶」了。

 

▲圖3:將TaskList的private method升等成類別

***

 

區分Command與Query

為了讓use cases layer的成員責任更佳明確,Teddy進一步套用CQRS設計模式。同時,利用use case來「吶喊」系統功能;請參考圖4。

 

▲圖4:在 use cases layer吶喊系統功能,並將讀寫分離。

***

接下來看一個Command的程式範例,圖5為AddTaskUseCase介面,這是一個in port,讓第三層的Rest Controller呼叫。

 

▲圖5:AddTaskUseCase介面

 

Teddy把實作Use Case介面的物件稱為Service,圖6為AddTaskService程式碼。

 

▲圖6:實作AddTaskUseCase的AddTaskService

 

Query程式與Command類似,差別在於兩者實作的介面不同,請參考圖7與圖8。

 

▲圖7:ShowUseCase介面

 

▲圖8:實作ShowUseCase的ShowService

***

剩下的execute

重構至此還沒討論圖1中的execute method,它的用途是用來呼叫use cases layer的那些Command與Query。Teddy一樣幫它座艙升等為類別,請參考圖9。但是這個升等之後的Execute class不像use case,比較像分派工作給不同use case的dispatcher。在架構上現在還妾身不明,Teddy先把它直接丟在use cases layer最外層。

 

請考圖9,Use case layer重構至此已經差不多了。其中有一些細節,像是Repository、Mapper、DTO、Presenter這些設計模式,全部寫出來內容太多。請容許Teddy偷懶一下,在此省略。有興趣又有錢的鄉民,歡迎報名參加重構既有系統:邁向整潔架構實作班,課程中會有詳細說明。

 

▲圖9:重構後的完整usecase package目錄結構

***

下集預告

重構至此Clean Architecture的四層已經完成了兩層,下一集要處理interface adapters layer。

***

友藏內心獨白:人口越來越多,物件村快變成物件「市」了。

2024年9月17日 星期二

重構既有系統,邁向整潔架構 (5):第二回合,套用DDD戰術模式

September 17 16:20~17:57

▲被封裝在聚合內部的咪咪

 

工商服務

想了解本系列文章完整內容,請參考重構既有系統:邁向整潔架構實作班。課程介紹與報名網址在此:https://teddysoft.tw/courses/refactor-to-ca/

***

前言

上一集<重構既有系統,邁向整潔架構 (4):第一回合,分層與移除基本型別依戀>已經形成了基本的領域模型,這一集要在領域模型中進一步套用領域驅動設計(DDD)的戰術模式(Tactical Design Pattern),找出聚合(Aggregate),達到封裝與決定交易邊界的目的。

***

領域模型: 決定Entity, Value Object與Aggregate

圖1是上一集重構後的領域模型,ToDoList, Project, Task是Entity,Project Name是Value Object。為了簡化起見,Teddy將整個領域模型包成一個ToDoList Aggregate。

在DDD中,Aggregate與Repository是一對一的關係,一個Aggregate透過一個Repository存入資料庫。決定了Aggregate的邊界,之後如果有儲存領域模型狀態的需求(絕大多數的軟體系統都會有持久化的需求),只需要實作ToDoListRepository即可。

 

▲圖1:領域模型的四個類別

 

***

 

將Entity Id升級成Value Object

DDD的Entity是一個有「唯一識別符號(unique ID)」的物件,原本的ToDoList缺少這個id,因此新增ToDoListId value object作為它的id。Project可以用ProjectName當作它的id,至於Task有一個long id屬性可以當作它的id,但是考慮到以後Task id有可能是使用者自己指定的字串,因此一併幫它新增TaskId value object作為它的id。

增加兩個Value Object之後的領域模型如圖2。

▲圖2:領域模型現在有六個類別

***

 

封裝聚合

DDD的Aggregate是一個交易邊界,同時也是一個封裝單位。操作Aggregate內部物件的動作必須透過AggregateRoot,以避免客戶端破壞Aggregate Invariant。如果Aggregate回傳它內部Entity給客戶端,客戶端不可以直接修改這個Entity,以避免客戶端繞過Aggregate Root修改了Aggregate。

換句話說,Aggregate如果回傳內部Entity參考給外部物件,則這個Entity應該是唯讀物件

舉個例子,圖3是ToDoList(Aggregate Root)的getProjects()方法,回傳其內部的List<Project>。客戶端拿到這個List<Project>物件之後,有兩個途徑可能繞過ToDoList而破壞封裝:

  1. 直接在List中增加一筆Project。
  2. 直接修改某個Project的內容,例如在Project身上新增一個Task。

第一點可以透過回傳一個「不可修改的List」來避免,至於第2點就只能靠ToDoList將Project轉成自己設計的ReadOnlyProject來避免。

 

▲圖3:ToDoList::getProjects() 程式碼

 

ReadOnlyProject的實作很簡單,請參考圖4。它直接繼承Project,然後覆寫所有會改變狀態的methods,直接丟出UnsupportedOperationException。

▲圖4:ReadOnlyProject程式碼(部分)

 

Project與Task都需要一個唯讀版本,重構後的領域模型現在有8個類別,請參考圖5。

▲圖5:領域模型成長到8個類別

***

 

下集預告

經過一番努力,重構至此領域模型終於有物件導向領域模型的樣子。但是一開始看起來很「礙眼」的TaskList程式依然沒變,還是原本那個150行、看起來亂亂的樣子。沒關係,下一集Teddy再來對付它。

***

友藏內心獨白:重構也要價值驅動。

2024年9月16日 星期一

重構既有系統,邁向整潔架構 (4):第一回合,分層與移除基本型別依戀

September 16 17:48~18:40;19:49~20:39

▲從下層爬往上層的咪咪 (by 常玉)

 

工商服務

想了解本系列文章完整內容,請參考重構既有系統:邁向整潔架構實作班。課程介紹與報名網址在此:https://teddysoft.tw/courses/refactor-to-ca/

***

前言

前三集聊了:
  1. 重構既有系統,邁向整潔架構 (1):為什麼透過重構改善軟體架構很困難
  2. 重構既有系統,邁向整潔架構 (2):逆轉重構流程
  3. 重構既有系統,邁向整潔架構 (3):Task List Kata簡介
這一集終於要動手重構。
***

分層原則

Teddy在第二集提到要採用由上而下的方式來重構,首先套用Clean Architecture,確定架構分為四層,如圖1所示。
 
▲圖1:套用Clean Architecture的分層原則
 
其中Task這支只有不到30行的程式很明顯地屬於Entities Layer,將它移到entity package。150行的TaskList目前還不清楚屬於哪一層,先不移動它。
 
***
 
物件村沒人怎麼辦?
Teddy的重構目標是在架構上採用Clean Architecture,在Entities Layer與Use Cases Layer則是套用DDD(領域驅動設計)的戰略模式語言(Entity, Value Object, Service, Aggregate, Repository等)。現在「物件村(domain model)」只有一個Task,顯得有點孤單,要想辦法增加物件村的人口。
 
物件導向設計,顧名思義就是要透過物件(領域模型)來表達問題領域的業務邏輯。這個Task List Kata重構範例只有兩個物件,這種現象有一個可能性,就是設計者對於「基本型別依戀」,捨不得使用物件,只用了String, Int這些基本型別。因此,接下來Teddy要採用「只管移除基本型別依戀」的方式,來增加物件村的人口。
 
***
 
無腦移除基本型別依戀
請參考圖2,第17行Map<String, List<Task>> tasks,將它「座艙升等」成Tasks物件,如圖3所示。
 
▲圖2:TaskList程式碼片段
 
 
 
▲圖3:移除基本型別依戀,新增Tasks物件
 
***
套用Value Object
圖3中的Map<String, List<Task>> tasks,其中Map的key代表project name,因此將其座艙升等成Value Object,請參考圖4。
 
▲圖4:ProjectName value object,用Java record代表
 
***
 
引入共通語言(Ubiquitous Language)
請參考圖5,此時Tasks類別的第6行變成Map<ProjectName, List<Task>> tasks,這還是一個基本型別依戀。請問一個擁有一組Project Name對應到List<Task>的「概念」要怎麼稱呼它?在此Teddy將其稱為Project。因此我們進一步將Map<ProjectName, List<Task>> tasks重構成List<Project> projects,參考圖6。
 
▲圖5:Tasks類別
 
 
▲圖6:Project類別
 
 
最後,Teddy把Tasks類別改名成ToDoList,現在物件村已經有四個物件,請參考圖7。
 
▲圖7:物件村從1個住戶增加為四個住戶
 
***
 
下集預告
有了物件村的住戶(領域模型),下一集要在領域模型中套用DDD戰術模式(tactical design patterns),好好了解一下什麼叫做Aggregate, Entity, Value Object。

***

友藏內心獨白:只有一個物件變成全村的希望,這樣壓力太大。

 

2024年9月13日 星期五

重構既有系統,邁向整潔架構 (3):Task List Kata簡介

September 13 20:30~22:07

 

前言

上一集<重構既有系統,邁向整潔架構 (2):逆轉重構流程>Teddy提到只要「大膽假設」不管你是開發什麼系統,就是要套用Clean Architecture就對了。如此一來,便可將重構從「由下而上」的設計方法轉變成「由上而下」的設計過程。

接下來這幾集,Teddy將以Task List Kata為例,說明如何將既有系統的架構重構成Clean Architecture。

***

Task List Kata介紹

Task List Kata是一個公開的重構練習,程式碼可參考:https://kata-log.rocks/task-list-kata。如圖1所示,它有8種不同的語言版本,Teddy使用Java版本。

 

▲圖1:Task List Kata支援的語言

 

如圖2所示,Teddy將Task List Kata複製到自己的repository,並將package name改成tw.teddysoft.tasks。

 

▲圖2:Task List Kata專案目錄

 

Task List Kata原本只有三支程:

  • Task:參考圖3,這是一支只有不到30行的程式,很簡單,好像沒什麼需要重構。
  • TaskList:參考圖4、圖5,這是一支約150行的程式,看起來很亂,感覺是重構的重點。
  • ApplicationTest︰整合測試,當重構進行時可以執行這個測試案例確定沒有感變舊有的程式行為。

 

▲圖3:Task.java程式碼

 

▲圖4:TaskList.java程式碼, 1/2

 

▲圖5:TaskList.java程式碼, 2/2

***

鄉民們怎麼重構

首先,了解需求。Task List Taka的介紹網頁提到 (用ChatGPT翻譯成中文):

 

***

任務清單

這是一個對基元(primitives)過度依賴的程式範例。

基元是任何具有技術性質且與您的業務領域無關的概念。這包括整數、字符、字串和集合(列表、集合、映射等),還有執行緒、讀取器、寫入器、解析器、異常處理等任何純粹專注於技術問題的事物。相比之下,這個專案中的業務概念,如「任務」、「專案」等,應被視為您領域模型的一部分。領域模型是您所運營的業務的語言,將其應用於代碼庫有助於避免使用不同的語言,從而幫助避免誤解。根據我們的經驗,誤解是造成漏洞的最大原因。

練習

嘗試實現以下功能,同時逐步重構以去除基元。在完全重構代碼以移除基元之前,儘量不要實現任何新行為,也就是說,只有在您即將更改的代碼已被重構後,才進行變更。不要重構無關的代碼。

一組判斷基元是否已移除的標準是,只允許基元出現在構造函數的參數列表、本地變量和私有字段中。基元不應該被傳遞給方法或從方法中返回。唯一的例外是基礎設施代碼——與終端、網絡、資料庫等進行通信的代碼。基礎設施需要將數據序列化為基元,但應視為特殊情況處理。您甚至可以將基礎設施視為一個獨立的領域,具有技術性質,其中基元是該領域的核心概念。

您應該嘗試將測試包圍在您正在重構的行為周圍。一開始,這些測試大多是高級系統測試,但隨著進展,您應該會撰寫更多的單元測試。

***

上述說明已經提示了重構方向:「優先考慮去除primitives」,也就是去除重構中提到Primitive Obsession(基本型別依戀)壞味道。但是一般人看到這個練習,直覺的想法是:「把大的拆成小的」。很多人會先把TaskList的private methods像是showaddsetDonehelperror等「座艙升等」,各自變成一個class,並套用Command設計模式,讓execute可以執行不同的命令。

接著,為了產生這些不同的命令,又套了Simple Factory。然後呢…….嗯,就沒有然後了。此時TaskList程式長度剩下原本的1/3,抽離出來的Command也符合單一責任原則,程式碼也很短,感覺好像沒什麼明顯地方需要重構了。

***

 

換你想

在正式談Teddy如何重構Task List Kata之前,請鄉民們也想想看,如果是你,你會如何重構?

***

工商服務

想了解本系列文章完整內容,請參考重構既有系統:邁向整潔架構實作班。課程介紹與報名網址在此:https://teddysoft.tw/courses/refactor-to-ca/

***

友藏內心獨白:搞錯順序只會徒然浪費時間。

2024年9月10日 星期二

重構既有系統,邁向整潔架構 (2):逆轉重構流程

September 10 07:38~08:08;11:30~11:49

      

▲圖1:Ada逆練九陰真經

 

前言

上一集<重構既有系統,邁向整潔架構 (1):為什麼透過重構改善軟體架構很困難>Teddy提到重構是一種「由下而上」的設計,而設計本身應該採用「由上而下」的過程,這也是為什麼透過重構改善軟體架構會如此困難的原因。

為了簡化重構軟體架構的難度,要想辦法將重構流程改成「由上而下」。

 

***

怎麼做?

要「逆轉重構流程」,其實很簡單,就是先確定架構目標就可以了。例如,假設鄉民是Uncle Bob的粉絲,不管手邊有什麼軟體,就是要想辦法無條件套用Clean Architecture就對了。如此一來,你就有了軟體架構重構的目標:Clean Architecture。

現在Teddy把架構重構的問題轉換成如何套用Clean Architecture,Teddy用Pattern Language的方式來解釋這個套用的過程,請參考圖1。有五個模式支撐Clean Architecture:

 

▲圖1:Clean Architecture底下有五個模式

 

上述五個模式的套用順序,先從Four Layers開始,將系統分成以下四層(四個不同的packages):

  • entity
  • usecase
  • adapter
  • io
      

有了大方向之後,接下來就是把你的既有系統想辦法塞入Clean Architecture這四層之中,如圖2。

 

      

▲圖2:Clean Architecture的四個階層

***

下一集Teddy將會以Task List Kata為例,說明如何將既有系統的架構重構成Clean Architecture。

***

 

工商服務

想了解本系列文章完整內容,請參考重構既有系統:邁向整潔架構實作班。課程介紹與報名網址在此:https://teddysoft.tw/courses/refactor-to-ca/

***

友藏內心獨白:道理很簡單,要做到卻不簡單。

2024年9月5日 星期四

重構既有系統,邁向整潔架構 (1):為什麼透過重構改善軟體架構很困難

September 5 21:02~22:06

▲Ada:我不會逆轉重構但我可以逆轉自己!

 

前言

今年四月Teddy開了一門新課程:【重構既有系統:邁向整潔架構實作班】。以往的開課流程Teddy總是先寫部落格文章,再設計課程,但這門課Teddy卻是直接設計課程而沒寫部落格文章。這系列文章算是「補寫」原本應該出現的文章,第一集先談為什麼軟體架構層級的重構很困難?

***

重構是一種設計

大部分鄉民應該都知道重構是在不改變程式碼外在行為的前提之下,藉由改變它的結構達到增進設計品質的一種方法。既然重構可以「改善設計」,因此可以將重構視為一種設計方法

Teddy將設計方法區分為兩種:

  • 由上而下:先決定設計目標,然後由上而下展開子目標,最後展開到一個可以施工的工作單元。例如,傳統的物件導向分析與設計就是一種由上而下的方法。先確定專案願景,然後展開需求、設計領域模型、軟體架構。
  • 由下而上:不確定最終目標是什麼,先從手邊可接觸到的「東西」著手設計,再慢慢將這些「東西」兜成更大的「東西」,透過這種方法完成設計工作。重構本質上就屬於這種設計方法。

***

 

設計應該是由上而下的過程

根據建築師Christopher Alexander的看法,設計是一種由上而下個過程。這就好比敏捷社群常說「以終為始」是一樣的道理,你希望達到什麼目標,就以這個目標當作設計的出發點。這樣講鄉民們可能還是覺得有點抽象,Teddy舉領域驅動設計中的戰術設計模式(tactical design patterns)為例,請參考圖1,Model-Driven Design模式是這個模式語言的「起點」,也是這整個設計最「頂」的模式,也等於這個模式語言的「目標」。

翻成白話文就是:領域驅動設計的戰術設計就是Model-Driven Design。如果看到這模式鄉民們就懂了,就開悟了,底下的其他模式也就無需再看。

 

▲圖1:領域驅動設計的戰術設計模式語言

 

***

再舉一個日常生活的日子,假設你晚上要請朋友吃飯,你要先決定「去那裡吃飯」?例如吃路邊攤、快炒、日式料理、韓式料理或是西餐等。如果是吃快炒,接下來才是決定要點什麼菜、什麼湯和飲料。這就是由上而下的設計思維。

***

由上而下那又怎樣?

重構作為一種由下而上的設計方法,用來達成區域改善是很有效的方法。例如,用Rename重構幫變數、method、class、package取個可以代表它們意圖的好名子。有沒有改善設計,當然有。改善後的設計對軟體架構的影響是什麼?不知道(不明顯)!

同理,用Extract Method把Long Method變短同時又用有意義的method name來代表程式碼的意圖。有沒有改善設計,當然有。改善後的設計對軟體架構的影響是什麼?不知道(不明顯)!

這就是透過重構要達到改善軟體架構很困難的原因。要採用由下而上的方式改善軟體架構並非不可行,但這類似演化的作法,需要一段很長、很長、很長的時間,以至於在一般專案中變得不切實際。因為鄉民們沒有那麼長的時間可以透過演化的方式「長出」一個好架構出來。

***

怎麼辦?

答案就在前文中。其實很簡單,就是要想辦法將重構改成由上而下的設計方法。只要做到這點,透過重構改善軟體架構就變得不再那麼遙不可及。

今天先聊到這個,下一集Teddy再來談如何「逆轉重構」,把它改成由上而下的設計方法。

***

工商服務

重構既有系統:邁向整潔架構實作班課程介紹與報名網址:https://teddysoft.tw/courses/refactor-to-ca/,有興趣的鄉民歡迎參考。

***

友藏內心獨白:好久沒出來亮相了。