基礎問題不簡單 | 怎麼合理使用值物件,讓你的程式碼更清晰、更安全?
寫在前面:
你好,今天我想與你聊聊如何在Go語言中落 地DDD。這部分內容,預計三分鐘左右可看完。就此,大體上了解下就行。
這裡面,其實主要說了我是基於什麼問題背景和思考,才作此分享的,你可以更有針對性 地結合具體的業務問題來思考。
尤其是實踐過程中的業務難點等,後面我將帶著大家, 最終交付一套Go語言層面切實可行的 比較完整且具有實際指導意義的方法論,助力解決實際業務中的開發問題。
正文部分,我打算分成9講與大家共同探討,並於後續幾周陸續釋出,預計每週可以看1~2兩篇。今天釋出的是前言,以及第一講(其開頭也有放前言部分,若看了這部分, 可直接跳過下拉至正文部分看)。
好,就先提這些。我們言歸正傳,接著聊。
DDD中文又叫領域驅動設計,是我們解決複雜業務問題時非常有效的一個手段,但其本身過於陡峭的學習曲線,也讓很多初學者知難而退。
網上雖然充斥著很多關於DDD的學習資料,但 大多隻是偏向於基本概念的介紹,缺少一個完整的落地實踐。
筆者所在的技術團隊也曾做過一些戰略設計,透過這些設計過程,很多同學對DDD有了更深入的瞭解。
在戰略設計後,我們識別出了很多的值物件、實體、領域事件等等元素,但仍然讓大家比較困惑的是,這眾多的領域元素要如何跟具體的程式碼對應起來呢?如果做不到程式碼跟領域模型的同步更迭,那麼DDD對於開發的意義又是什麼呢?
我第一次接觸DDD應該是在2014年,這一年正好是《實現領域驅動設計》一書在國內出版,依稀還記得當時公司群裡異常興奮的討論。
從 03 年 Eric Evans 的 Domain Driven Design 到14年 Vaughn Vernon 的 Implementing DDD, 整整過去了10多年的時間,業界才算有了一個真正意義上的指導DDD落地的思想。
IDDD一書出版的時候,也正是Java語言大行其道的時候,再加上書中的一些程式碼示例也是基於Java的,這就造成了人們的認知一度認為只有Java才是最適合實踐DDD的。
近些年來,Go語言被越來越多的公司及個人採納,很多人也開始了在Go語言中落地 DDD 的嘗試。
但其實,DDD本身只是一種思想,就像耗子叔曾經在《從物件導向的設計模式看軟體設計》一文中提到的,說起設計模式也並非就一定是OO的。
因此,實踐DDD其實無所謂使用什麼語言,但同時我們也要看到Go與Java在語言層面的差異,一些實現細節就必須做出調整。
是否有一套切實可行的程式碼結構與規範,來指導實際業務中的開發呢? 遺憾的是,目前在 網上貌似是找不到在Go語言層面 比較完整且具有實際指導意義的資料。
所以從今天開始,我會透過一系列的文章,來介紹DDD如何在Go語言中落地。希望 透過儘量系統且詳細的描述,來降低你實踐DDD的門檻,同時 能夠 幫助你解決一些在實際開發中可能遇到的問題。
最後,我還會 以一個虛構的系統作為案例,透過對這個Demo的講解,幫助你更徹底地掌握DDD的核心思想以及落地操作。
在具體的內容安排上,未來會涵蓋下圖中的一些主題。 現在你也可以停下一兩分鐘看看框架圖裡,你更為感興趣的 部分,在此先道聲感謝,謝謝你對本系列內容的關注與支援,期待我們留言區裡可以進一步交流:
不過在這之前,因為本系列文章的重點,不在於對DDD相關概念的講解,以及戰略建 模的分享。
因此,在繼續閱讀後續的內容前,你最好對DDD有一些基本的瞭解,如果還不清楚,建議可以在網上先搜尋一些資料閱讀。
好,前言部分,我們就先聊到這裡,非常感謝你耐心的閱讀。
責編 | 韓楠
約 4265 字 | 8 分鐘閱讀
以下,Enjoy~
接下來,就讓我們從最簡單也是最基礎的領域元素 - 值物件說起吧。
正式說值物件之前,我們先來思考這樣一個問題, 什麼樣的程式碼算是好程式碼呢?
相信大家或多或少,都接手過一些遺留的業務系統,這些系統的程式碼大多都能正常工作,或許執行的還是某個關鍵業務。
某一天,你接到了一個產品需求,經過評估,你認為只需改動很少的幾行程式碼就可以實現。
於是,你很快就完成了開發,並進行了充分的自測,你認為肯定不會出錯。
但不幸的是,上線後還是引發了事故。
剛提到的場景,相信對於很多做業務的同學都深有感觸。
這種問題之所以常見,很多時候是因為我們的程式碼不夠清晰,沒有很好地表達業務,看程式碼的人就只能靠猜。比如我們定義一個註冊的方法,需要用到使用者名稱、手機號和密碼,很多同學可能會將這個方法定義成這個樣 子:func Register(string, string, string) User。
那麼問題就來了,作為使用方,三個引數要按什麼順序傳入呢?
這也是程式碼不夠清晰的表現。
在DDD中, 值物件透過將相關聯的屬性組合在一起,構成了一個完整的概念整體,同時使用業務域中的統一語言,可以將一些隱性的概念顯性化。 正確地使用值物件進行建模,不但能夠大幅地提升程式碼的清晰度、可讀性,在一定程度上也會降低系統出錯的機率。
01 ⎪ 先從怎麼理解值物件說起
值物件本質上就是一個屬性的集合,但這些屬性並不是隨便湊到一起的,它們通常是為了某個共同的目的或概 念而存在著。
除此以外,值物件還具有下面兩個顯著的特點:
• 不變性 ,值物件在建立出來後是不應該被修改的,如果必須修改物件的某個屬性,則需要整體替換成一個新的值物件;
• 無身份標識 ,這也是與實體相比非常重要的一個不同點。缺少了唯一標識,怎麼判斷兩個值物件是否相等呢?就要看它們所包含的屬性是否全部都是相等的了。這就好比我們在現實生活中使用現金,我們關心的只是貨幣的面值,是100的還是50的,而不會關心這個紙幣的編號是多少。
同時,值物件應當具有一定的行為,但同時也要避免過於複雜。
02 ⎪ 實現值物件
▶︎ 比較嚴謹(教條)的實現方案
我們以一個描述貨幣 價值的值物件為例,來看下程式碼:
• 值物件需要使用大駝峰,也就是這個 MonetaryValue 應該是全域性可見的,這樣一來在領域層之外也是可以訪問的。
• 值物件裡的各成員應該小寫,這樣做有兩方面的原因:
a.一方面可以避免在包外對屬性值的直接修改。假設有個更新數額的操作,因為沒法直接在原值上賦值,例如這樣的程式碼val.amount = 2 也就行不通了。這麼做的好處是可以避免錯誤的賦值導致的一些問題。
b.另一方面,在包外雖然可以對 MonetaryValue 例項化,但是因為成員不能賦值,也就強迫了使用者必須呼叫 NewMonetaryValue 方法,從而避免了構造出一個不符合規範的物件 。
• NewMonetaryValue 是一個工廠函式,並一次性傳入構建該值物件所需的所有引數,在這個函式中可以對引數進行合法性校驗,這樣保證了所有建立出來的物件都是合法的。
• 包外如果有對值物件內部成員進行訪問的需要,可以定義一個同名的、採用大駝峰定義的方法,比如這裡的 Amount 方法。
比如,如果我們需要對這個值物件進行json序列化與反序列化。這個時候,因為所有的成員都是未匯出的,會導致 Golang 預設的 json 庫直接將其忽略,這明顯與我們的期望是不符的。
因此, 我們在進行編碼的時候,還需要充分考慮實現的成本, 例如上述需要序列化的場景,直接定義成如下形式可能更合適些:
在一個值物件中可能持有另外的值物件,比如這裡的 Currency,雖然是作為一個列舉使用,但本質上仍然是一個值物件。
當值物件中包含了其他非基本型別(例如指標、struct、slice、map等)的屬性時,就要特別注意了,即使我們在值物件裡沒有對這類成員進行修改,但是仍然無法保證外部不會修改它們的值。
看下面的例子:
雖然 BadDemo 在內部沒有任何方法對 s 進行增刪操作,但是,如果外部對 ss 進行了修改,比如 ss[0] = "abcd",同樣會反應到 s 上。
這樣一來也就破壞了值物件的不變性, 這種破壞性,有的時候可能會給你帶來不可預知的Bug,並且不是特容易發覺。
▶︎ 透過新建來對值物件進行修改
值物件也是能夠擁 有一些行為的,比如上面的 MonetaryValue,可能有一個Add 方法:
這裡的 Add 方法跟我們通常的實現可能不太一樣,主要在於方法的返回值。
值物件因為要保證不變性,因此, 我們不能直接對值物件的內部屬性進行修改,而是採用了新建一個值物件的方式。
另外,Add 方法的接收者是一個值接收者,而非指標接收者,使用指標接收者的問題在於可能不小心就修改了內部屬性,而造成一些隱式的錯誤。當然這裡也可以不用這麼絕對,對於大型的值物件,如果使用值接收者會帶來一定的效能損耗,這個時候也可以使用指標接收者。
▶︎ 為什麼要保證值物件的不變性
費了這麼大勁,又是限制接收者型別,又是強調必須返回一個新的值物件,為什麼呢?
我們考慮下面這個場景。
比如說,我們現在在開發一個多人遊戲,每個人在一開始的時候都有固定的等值籌碼,隨著遊戲的進行,你可能花費一些籌碼,又或者賺取一些籌碼。
我們在初始化籌碼時,程式碼可能如下:
之後 player A 透過賣出裝備而賺取了更多籌碼,假設說這裡是按照直接修改值物件內部屬性的方式來實現:
那麼問題就出現了,我們雖然只給 player A 增加了籌碼,但是發現所有 player 都莫名多了10籌碼。
一起來思考下,問題的原因是什麼,其實就在於,我們共享了這個值物件,但是沒有保證它的不可變性。而返回新的值物件的方式,則不存在這個問題:
03 ⎪ 實現列舉
列舉,通常被認為是值物件的一種特化形式,它也屬於領域中的元素。
▶︎ 以值物件的形式來實現列舉
比如有一個叫 SomeStatus 的列舉,對應有兩個列舉值 SomeStatusOne 和 SomeStatusTwo, 那麼可以採用如下的形式來定義:
上述程式碼大部分都遵循了值物件的實現方法,但是也有兩點不同:
• 定義一個預設的零值,並提供了一個判斷當前列舉是否為零值的方法。零值的作用,是保證在使用到列舉的地方都不會有 nil 的出現,這種保證可以一定程度地避免程式 panic 的發生。
• 所有的列舉值,放到一個 Slice 或 Map 中,便於在建立列舉時進行合法性校驗。
可以看到,這種實現方式還是比較麻煩,但是能夠很大程度地在程式碼層面 保證程式的正確性。而且,列舉值的變動頻率一般都不會太高,所以成本也僅僅是一次性的。
▶︎ 使用原始型別表示列舉
另外一種偷懶的形式,類似下面這種:
跟上面值物件的形式相比,的確是簡單了不少,但是 最大問題在於程式碼的不可控性。
比如下面這樣一個函式:
UpdateStatus 方法接收一個 AnyStatus 型別的引數,之所以這裡是一個 AnyStatus 型別而不是一個 int 型別,大機率是希望呼叫者 能傳遞一個合法的 AnyStatus 進來,但是這一點是得不到保證的。
呼叫方可以傳入任意一個整數值,而編譯器並不會報錯:
如果希望程式碼足夠的嚴謹,那麼在 UpdateStatus 方法內部就不得不對傳入的值進行校驗。這種校驗邏輯也會散落到所有用到 AnyStatus 的地方。
另外,從全域性來看,任何人在任何地方,都可以直接呼叫類似這樣的程式碼 AnyStatus(6) 來生成一個列舉值。很顯然,這是一個無效的列舉值,也是一個完全不應該存在於領域的物件。
綜合看前面兩種方式,各有優缺,可以根據各自的情況,在團隊內大家做到統一即可。
04 ⎪ 總結
今天,我向你介紹了值物件在Go語言中的實現方式,以及如何正確地定義列舉。
值物件這個概念並不複雜,但是在實現的過程中涉及到的細節會比較多,這也跟它自身的一些特徵是分不開的。
值物件最重要的特徵是不變性,我們所有的實現,都是圍繞這一點展開的。也正因為這種不變性,讓我們在業務中可以放心地對其進行復用。
當你在決定一個領域概念 是否要建模成一個值物件時,就要考慮是否具有 下面的一些特徵:
在程式碼落地的過程中,你還要注意:
• 值物件的建立必須透過一個簡單的工廠函式來實現,這樣可以避免不符合業務約束的對 象產生;
• 對值物件的修改不能直接修改屬性,需要構造一個新的值物件出來;
• 值物件裡的成員最好不要包含Map、Slice、指標等結構,否則值物件的不變性可能會遭到破壞;
• 如果需要比較兩個值物件,可以定義一個 Equals 函式,在函式里判斷是否所有屬性都相等;
• Golang 裡的一些原始型別,比如 int64、string 等,都可以看做是最簡單的值物件來使用。
如果說,是一磚一瓦構建起了高樓和大廈,那麼,值物件就是DDD裡的磚和瓦。因此,只有保證了值物件實現的正確性,才能在更高的維度去構建實體、聚合根等領域元素。
▶︎ 延伸思考
最後,留給你一個思考題。
為了程式碼足夠健壯,我們將值物件裡的屬性設計成了未匯出的,當值物件需要持久化到資料庫時,該如何做呢?從資料庫中讀出資料要重建值物件,又要怎麼做呢?
話不多說,下一節,繼續和大家聊聊如何實現實體、聚合根。
這一篇,到這裡我們就要結束了,非常感謝你耐心的閱讀。
很期待我與你能夠有更多思想上的共鳴、碰撞。如果願意分享,這一講也歡迎轉發給你的朋友,和他一起討論。
同時,若你對這一講的內容,有相關思考或是疑問,可以多多提出來,留言區裡,我們多交流。
後續分享再見。
THE END
轉載請聯絡ITPUB官方公眾號獲得授權
—————————————————————————————————
歡迎各領域技術人員投稿
投稿郵箱 | hannan@it168.com
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70016482/viewspace-2904545/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- KubeVela 1.4:讓應用交付更安全、上手更簡單、過程更透明
- 【JS】裝飾器讓你的程式碼更簡潔JS
- 使用 Macro 讓你的程式碼更簡潔,更具有可讀性Mac
- 讓你的Python程式碼更乾淨只需簡單一步Python
- Nutanix:將IT基礎架構“隱形”,讓雲更簡單架構
- ASP網頁模板的應用: 讓程式和介面分離,讓ASP指令碼更清晰,更換介面更容易 (轉)網頁指令碼
- 巧用偽元素和偽類讓我們的html結構更清晰合理HTML
- 基礎才是重中之重~ConcurrentDictionary讓你的多執行緒程式碼更優美執行緒
- 使用Async,讓你的Node.js程式碼更優雅Node.js
- 低程式碼平臺+阿里雲端儲存:讓業務開發更簡單,資料儲存更安全阿里
- 幾個簡單的技巧讓你寫出的vue.js程式碼更優雅Vue.js
- [翻譯] 讓你的程式碼更簡短,更整潔,更易讀的ES6小技巧
- 阿里讓你更清楚的認識自己的Java基礎阿里Java
- 使用解構賦值與擴充套件運算子,讓你的程式碼更優雅賦值套件
- 使用原生 cookieStore 方法,讓 Cookie 操作更簡單Cookie
- 通過 Laravel 訊息通知使用 EasySms 簡訊服務,讓你的程式碼更簡潔Laravel
- Kotlin-for-Android : 讓你的Android程式碼更簡潔KotlinAndroid
- 讓你的DEVONthink UI 介面更簡潔?devUI
- 阿里讓你更清楚的認識自己的Python基礎阿里Python
- 嘗試讓查詢更簡單
- Go Interface 的優雅使用,讓程式碼更整潔更容易測試Go
- MultiItem進階 使用DataBinding 讓 RecyclerView程式碼更簡潔清爽View
- 經驗總結 | 重構讓你的程式碼更優美和簡潔
- Spring Boot 整合 Lombok 讓程式碼更簡潔Spring BootLombok
- 求一個JS問題更簡單的寫法JS
- 網路安全法6.1起施行,怎樣讓你的網站更安全?網站
- 智慧家居讓生活更方便但也帶來新的安全問題
- 如何讓你的作業系統更安全二作業系統
- Java11正式釋出了,讓你的程式碼更完美?Java
- 一些技巧讓你的 Laravel 程式碼更優雅Laravel
- C#6新特性,讓你的程式碼更乾淨C#
- Coding Monthly | 讓開發更簡單!
- 風變程式設計,讓程式設計學習更簡單!程式設計
- 更簡的併發程式碼,更強的併發控制
- 讓 json 解析更簡單高效的 GJSONJSON
- [譯]ES6提示和技巧,使您的程式碼更清晰,更短,更容易閱讀
- 讓你的伺服器更安全些(Linux篇)伺服器Linux
- JavaScript中使用bind()方法讓程式碼更乾淨JavaScript