LINE Corporation 於2023年10月1日成爲 LY Corporation。LY Corporation 的新部落格在這裏。LY Corporation Tech Blog

Blog


Domain Driven Design 的初體驗

以通訊軟體為核心,LINE 持續發展圍繞用戶生活的各種服務,同時也抱持著開放的態度,積極與不同的平台或開發工具串聯。因此,我們鼓勵、更贊助 LINE 的工程師參與各式各樣的外部研討會,激發更多創意或合作的可能性,並於會後撰寫見聞,分享給 LINE Engineering Blog 的讀者們。

《LINE 強力徵才中!》與我們一起 Close the Distance 串聯智慧新世界 >> 詳細職缺訊息

相見恨晚的 Clean Architecture 與 Domain Driven Design

大家好,我是 Robert,目前在 LINE Taiwan 擔任 Technical Project Management (TPM) 的工作。對於 Agile、XP、軟體架構、Domain Driven Design (DDD) 等都很有興趣。

當初會接觸到 Domain Driven Design,最主要是在接觸由 Uncle Bob 所提出的 Clean Architecture 的過程中,發現透過結構化的軟體設計,讓分層更明確,程式碼容易測試並且更好維護。早年在開發 Android Application 時,最常遇到的問題就是沒有好的設計架構可以參考,寫出來的模組其功能並不明確,模組與模組間彼此糾纏不清,商業邏輯與 Android Framework 無法適當的抽離,最終造成軟體難以測試,必須大量的仰賴大量的人工測試。

在學習 Clean Architecture 的歷程中,發現這些觀念並不是橫空出世,而是從過去的結構化架構,例如最早的 Layered Architecture、Hexagonal ArchitectureOnion Architecture 等,為基礎加以演進,又或者是將層與層之間定義的劃分的更為明確。同時,在研習 Clean Architecture 這本書與相關的技術文章時,都提到核心的區域是由 Use Case 與 Entity 所組成。但書中對於這些核心的業務邏輯的設計方式,並沒有太多著墨。這觸發了我好奇心,於是開始展開我的探索 “如何設計核心業務邏輯” 的冒險之路。偶然的機會下,接觸到了 Domain Driven Design,發現它似乎提供了一個可行的方向。

公司對於員工參與國內外的社群與研討會 (Conference) 一直是採取大力支持的態度,同時也鼓勵員工能夠多多與外界相互交流與分享。利用這一次參加在北京所舉辦的領域設計工作坊,將我所見所學,加上在書本中、部落格文章的學習心得記錄下來,與大家分享。

出處

領域驅動設計工作坊 (Domain Driven Design Workshop)

這個工作坊中內容主要是由兩大部分所組成:DDD 的基本觀念介紹、講解與事件風暴工作坊。一開始,講師闡述了軟體開發所面臨的挑戰與困境,如何利用 DDD 所提出的戰略設計 (Strategic Design) 與戰術設計 (Tactical Design) 這一套有別於過去以資料(庫)設計為中心的思考方法,解決軟體開發的複雜性問題。課程中,用一個有趣的範例,帶著大家演練事件風暴 (Event Storming),藉以了解戰略設計與戰術設計的一些重要精神。

軟體開發所面臨的挑戰

難以理解的程式碼與混亂不堪的模型 (Domain Model)

對於曾經有大型軟體專案開發經驗的人來說,工作流程不外乎從理解業務目標開始,透過與業務需求單位的訪談,確定需求。需求確定後,以規格書 (SPEC.) 的方式交付給開發單位,軟體開發人員應用軟體開發的技巧,將複雜的商業邏輯轉換成程式碼。在過程中,我們可以發現,軟體的複雜程度與專案的成敗有著高度密切關係。軟體設計就是嘗試將複雜的商業知識與流程,透過軟體開發人員有系統的方式轉換成容易理解與維護的程式碼,最終達到企業與組織的預期目標。

試想,在數十萬行或者更多的程式碼中,如何將新的業務邏輯融入現有的程式碼中,讓它既能夠不損害現有的功能,又能夠完成新的目標,這是非常挑戰的工作。以目前最廣泛採用的 Scrum 開發方式來說,產品經理 (Product Owner) 將需求以 User Story 的方式展現,然後 Development Team 將 User Story 切成可以執行的 Task 或 Subtask。每個 Task 或 Subtask 都有一個特定的任務需要完成:它可能是一個使用者會接觸到的 UI 介面、也有可能是一個與其他服務協同完成的一個 API、核心的邏輯業務或者是分散式儲存的系統等等技術難題。

在開始動手實作前,你必須理解你的 Task 為何。當你充份了解你的 Task 後,你打開你的 IDE 或 Editor,你面臨到可能是這段程式碼是幾個月前的你所寫的,或是這是別人所產生的程式碼。身為軟體開發人員,你必須理解這段程式碼所表達的商業意涵為何,很多時候你會發現你花了很長一段時間閱讀程式碼。當你要一展長才,將你所規劃設計的程式碼融入到現有的軟體設計中,你會發現你的程式碼看起來似乎是有一些簡單的分層:以後端 (Backend) 程式碼為例,你可能會有 Controller Layer 用來處理 REST API 的需求,然後將需求轉發派遣到下一層 Service Layer。Service Layer 可能經過一些複雜的處理 (這處理可能混雜了技術與商業邏輯,甚至你也不清楚這到底是技術還是商業邏輯),然後交給資料庫進行資料的存取,最後完成一項特定的任務。

如果有機會檢視過去為了完成的工作而修改的軟體模組 (Module),你可能驚覺到你的軟體設計可能是發散式變化 (Divergent Change),或者是散彈式修改 (Shotgun Surgery)。不管是哪一種,都意味著軟體設計的技巧有很多可以加強改善的地方。 

發散式變化

出處

散彈式修改

出處

大泥球 (Big Ball of Mud)

更殘酷的事實是,隨著時間的推移,你會發現當業務愈來愈繁忙,需求愈來愈多,似乎加更多的人手已經不是一個好的解決方法。新增一個業務功能的時間愈來愈長,軟體的交付速度與業務需求成長無法匹配,直到有技術人員提出要重新系統改寫的神話故事,開啟另一個輪迴。又或者是撐到最後,軟體開發人員相繼離開這個專案。人力更加短缺,以至於業務失去了競爭能力,企業與組織陷入負面的漩渦無以為繼。

當我們檢視這樣的專案時,最常發現的就是大泥球設計。表面上軟體開發人員似乎針對不同的需求功能進行軟體分層架構,但層與層之間的定義又不夠明確,彼此之間相互依賴。商業邏輯與技術考量 (Technical Concern) 混雜,再加上難以執行的自動化測試。如果我們利用工具分析,很可能畫出大泥球般的軟體模組間相互依賴的關係圖。

出處

協同式建立領域模型

大約在今年上半年有榮幸參與了公司內部軟體開發流程改善計畫,並且能夠以公費的方式參與 Scrum Master 的培訓 (這也是 LINE 很吸引人的地方,只要是對工作或者是流程有所幫助,公司都很願意投資在人員身上,達到公司與員工一同成長的目標,歡迎加入我們)。在Agile的開發模式中,提倡軟體開發人員、客戶以及相關人員為達到交付顧客價值的目標而組織成為一個團隊一起努力。

在 Domain Driven Development 的相關書籍中,你也可以看到如 Collaboration、Crunching Knowledge、Distilling the Model 等字眼不斷被提及。開發團隊透過各種方式緊密地與領域專家一起建立與精鍊領域模型,而這些觀念不就是在敏捷宣言 (Manifesto for Agile Software Development) 裡面所提倡的 “Customer Collaboration” 價值相呼應? 這激起了我的好奇心,怎麼這些觀念跟敏捷開發的碎碎念 (Jargon) 非常相似。直到閱讀了 Domain Driven Design 的這本聖經,才領略到了他們之間的關係。作者 Eric Evans 在書中提到,雖然書中是在探討如何建立領域模型,但是難免還是會碰觸到軟體開發流程的議題。而這套建模的方式雖並不僅限於敏捷開發模式,但他的確受到敏捷與 XP (Extrem Programming) 很大的啟發。

出處

由此可見,愈是複雜的系統,愈是需要開發人員與領域專家一起通力合作,利用協作的方式,首先聚焦我們所要解決的問題 (Problem Space)。當釐清了想要解決的問題後,透過所有人腦力激盪 (Brain Storming) 的方式初步產生潛在可行的解決方案 (Solution Space)。依照公司能力、預算等限制條件,從潛在的解決方案中挑選最俱有價值的方案進行驗證。為了確認我們的產品與服務能夠正確無誤地落實,所有人員必須一起建立領域模型,透過統一語言 (Ubiqulous Language) 的溝通方式達到分享的領域知識 (Shared Knowledge)。

對於開發人員來說,必須以領域問題為核心出發,建立一套以領域為中心、分層隔離、由外向內相依的方式,區分核心商業業務邏輯層與技術考量為外層的結構化系統架構。由於這是透過共同語言所產生的領域模型,所有人都能夠理解企業的核心業務,並隨著時間與需求的改變,大家一同維護此領域模型。

開發者也基於此共同認知的領域模型,轉換成可執行的程式碼,達成業務目標。在這樣的建立領域模型過程中,核心的程式碼展現的是領域模型的共同語言。不僅是領域專家能夠理解領域模型,而且開發人員更是基於此模型開發系統的重要模組。而模組與函式的命名也是基於大家能夠理解的共同語言,減少軟體開發人員進行業務邏輯與程式命名之間的轉換,簡化軟體開發的複雜度,達成程式碼即是高可讀的業務邏輯展現。最後再加上領域模型的輔助,讓開發人員能夠充分理解複雜的企業邏輯,聚焦在核心的業務邏輯,同時也能因應技術改變與作業的維護與擴充,而不斷演進。

領域驅動設計工作坊 - 事件風暴 (Event Storming)

在瞭解了領域驅動設計的概念與重要性後,在接下來一天半的時間裡,教練給予下面的例子作為事件風暴的練習題目。

老子有錢,我要幹掉 “功夫趴趴熊” 外送服務!

電梯簡報 (Elevator Pitch)

聽過精實創業 (Lean Starup)、創投、天使投資等流行用語 (Buzzword) 的朋友來說,相信對於電梯演講不會太陌生。也就是説當你有一個好的想法,想要付諸實現前,可以經由一些簡單的手法幫助你審視自己的產品或服務是否具有市場上的競爭優勢。

當你有機會在電梯與你的潛在金主巧遇,利用短短的不到一分鐘的時間,表達出你的客戶或使用者為何? 他們目前遭遇到的問題是什麼? 你的產品或服務如何滿足這塊市場缺口? 又你的競爭對手是誰? 你如何贏過他們而獲取豐富的獲利?

出處

識別領域事件

有了好的想法,需要一個有系統的方式付諸實現。在一開始,透過領域專家 (Domain Expert) 與開發人員的一起努力,找出所有領域事件 (Domain Event)、規則 (Rule)。這個領域事件必須是與我們系統或服務相關,而且是領域專家所關注的。透過這些事件的發生,對系統產生重要的影響。這些事件的發生,對於我們的商業邏輯有重大的關係而且我們必須將這些有意義的事件以任何方式記錄下來。如果沒有這些事件和記錄,我們的服務就無法順利進行。

這階段的工作就是將這些事件加以識別,然後依照發生時間的先後順序予以排列。規則是對於一些複雜的業務邏輯或者次要的商業運作方式加以記錄,簡化對於主要業務流程的干擾。對於開發人員來說,可能是程式碼的分支條件或是套用策略模式 (Strategy Pattern) 加以抽象,減少程式複雜度,使我們可以更加關注主要業務流程。

識別決策命令 (Decision Command)

在這個步驟中,我們可以從是誰 (可能是人或外部系統)、什麼條件 (定時如每天、每次等) 的觀點找出直接觸發的動作,將它標示為決策命令。在過程中,大家比較容易犯的錯誤就是以實作技術的角度思考。切記,這是領域設計,請務必以業務邏輯為出發,並且以統一語言加以描述,達到大家都能理解的領域模型。領域事件、決策命令可以是這個領域專家所說出的業內行話。如果在過程中發現了一些新的領域事件,或者釐清了一些重要資訊,不需要過度擔心,請把它做適當的調整與優化,這也是大家一起協作的主要目的之一。

識別領域名詞

領域名詞通常是在特定的上下文 (Context) 中一些重要的概念。在工作坊中,最重要的是能夠快速的產生與識別與事件、命令最相關的名詞。如果過程中發現語意的模糊時,必須透過重新命名的方式加以釐清,達成統一語言。另外,這過程強調快速發散、盡快識別,我們會在後續的過程中進行整理與收斂。最後提醒一點,課程中有提到:相同的名稱在不同的上下文會有不同的解讀。例如商品的庫存數量,在倉儲的上下文會是商品的實際數量 ; 然而在上架商品的上下文,它會隨著供應商的補貨能力而加以調整。例如,庫存數量是實際數量加上 20%,而不會是實際倉儲的數量。像這樣的領域知識就是在每次的工作坊中透過領域專家的口中予以顯性化並加以識別釐清,甚至產生新的領域名詞。

識別界線上下文 (Boundary Context)

當我們在跟領域專家討論業務需求時,透過業務上下文的邊界,讓大家能使用一致的語言,避免產生不必要的誤會。簡單來說,這個過程就像在做業務邏輯的歸類,統一大家的語言。

界定界線上下文之間的依賴關係

透過整理界線上下文之間的相互依賴關係,我們可以檢視是否有遺漏、未澄清的上下文,又或者這些上下文可合併,或需要進一步分離。我們也需要注意是否有循環依賴、或過長的依賴關係等,這些對於之後的建模甚至軟體設計會有很嚴重的影響。

劃分問題子域 (Subdomain)

劃分方式可區分為核心域 (Core Domain)、支撐域 (Supporting Subdomain)、通用域 (Generic Subdomain),用來決定企業與組織內部資源投入時的重要考量。核心域顧名思義就是企業的兢爭優勢的來源,因此需要投入最好的資源與人力。支撐域是用來輔助核心領域,但重要程度又不如核心域。此領域通常具有高度客製化的需求,因此很難在市面上找到現成的解決方案,因此需要投入資源。最後,通用域處理的問題子域是很容易在業界找到解決方案,透過購買,簡單的客製化或者是外包就能滿足需求。

領域模型

在工作坊中,我們利用有限的時間,挑選一兩個核心域套用領域模型。領域模型包含了:聚合根 (Aggregate Root)、實體 (Entity)、值物件 (Value Object)、領域服務 (Domain Service)、工廠 (Factory)、Repository。

  • 聚合根 (Aggregate Root):主要是負責封裝業務邏輯與狀態,裡面包含所有與聚合相關的業務操作,並確保業務邏輯的一致性。
  • 實體 (Entity):具有生命週期,也就是狀態會隨的業務的進行,其內部屬性會有所變更。另外,它有一個很重要的特性:具有唯一性。舉例來說,在我們抽象會員的實體來說,可能會有姓名完全一樣,但卻代表不同的身份,因為他們有不同的身分證字號。這時,身份字號就是一個好的唯一標示。
  • 值物件 (Value Object)此 Pattern 主要是用來作為描述真實世界的物件,其物件在我們的問題域時我們並不關心是否具有唯一性。舉例來說,在電子商務中,錢、地址等。在抽象業務邏輯時,我們並不關心一千元上面的序號為何,錢的數值即可滿足我們的需求。在工作坊進行時,學員與老師之間曾經有一段小小的火花。有學員認為地址具有唯一性,他提及到當地址儲存到資料庫時,需要進行唯一標示以減少資料的重複,各位看倌覺得呢?留給各位練習看看。

領域服務、工廠、Repository 並沒有在此工作坊中有太多的著墨。不過有興趣的人,可以參照 Domain Driven Design 這本書,即可了解它們所要解決的問題。

出處

結論

兩天的工作坊看似時間很長,事實上,在練習的過程中,很快就結束了。在工作坊中,你會體驗到如何透過工作坊協作的方式,大家在一起釐清複雜的商業邏輯,讓隱晦的描述或者是含糊不清的商業運作方式,透過眾人的專業知識、腦力激盪,解決最複雜且重要的部分。領域專家、軟體開發人員在工作坊中密集的溝通、彼此相互學習,為共同的業務目標而努力。當開發人員充分理解業務運作方式,運用共同建立的領域模型,使得領域模型轉換成程式碼的過程會變得相對容易。另外,軟體開發人員也減少很多命名方式的困擾,將大量的領域模型中所建構的統一語言對應到程式碼中,達成程式碼的高可讀性。

另外,順帶一提,目前主流的協作方式多半是採用事件風暴 (Event Storming) 工作坊,透過工作坊實踐 Domain Driven Design 所提倡、以業務為核心的方式設計與開發。這部分需依照不同的公司組織,文化,而有所不同。但不變的是組織內部需要不斷的探險與調整,找出一個適合內部的運作方式。切記,協作、問題領域劃分、統一語言等才是核心價值,而不是追求制式化的會議與工作坊,而造成本末倒置。

工作坊中也提到,Domain Driven Design 並不是萬能的銀彈 (Silver Bullet)。如果本身組織、團隊與個人之間原本就缺乏互助合作的經驗與精神,部門之間為了各式各樣的KPI而形成各有各的地盤的穀倉 (Silo) 組織,建議在嘗試引入 Domain Driven Design 前,也許可以先考量如何減少部門隔離與達成成員間彼此相互合作的球隊組織,效果會更好。

對於只想碰技術的軟體開發人員,個人覺得可以考慮以戰術設計為 Domain Driven Design 入門。透過採用 Clean Architecture 所提到的分層概念將業務邏輯與技術考量分開,由外而內的依賴關係與相依反轉 (Dependency Inversion) 等為入門。當以上都做到熟練了之後,進一步針對核心的業務邏輯加以改善或重構。常見的問題徵狀包含了貧血的領域模型 (Anemic Domain Model)、肥腫的交易手稿模型 (Transaction Script Model),例如,XXXService 裡面的程式碼幾乎承載了所有的商業邏輯等的設計壞味道。當大家漸漸地察覺設計的壞味道後,這時可以嘗試的進一步導入戰術設計所提出的一些建議解決方法如實體、值物件、領域服務、工廠、Repository 等設計模型。在導入戰術設計的過程,徐徐注入 Domain Driven Design 中最核心的戰略設計,一同邁向真正的以領域為核心的設計模式,透過協作的方式交付最有價值的產品與服務,最終達成企業營利目標。