問題:代碼如何和領域模型保持一致廉油?C語言能清晰的表達領域模型嗎碰声?
第一個問題:代碼和模型保持一致跟啤,需要掌握用編程語言實現(xiàn)模型的“標準方式”,并具備寫出自注釋代碼的能力起趾。
第二個問題:能,但是要掌握模塊化C
的設計和編碼方式警儒。
以下作為補充训裆,提供給感興趣的同學眶根。
我們知道模型是一種抽象,面向對象建模得到的模型結果是經(jīng)過選擇的對解決問題有價值的概念边琉、關系和約束的集合属百。
確實,面向對象編程語言可以方便的表達領域模型变姨。例如通過類可以表達領域概念族扰;通過interface表達接口和服務;通過private可以封裝屬性和行為定欧;通過構造和析構函數(shù)進行對象生命周期管理渔呵;通過繼承和引用可以表達泛化和組合關系等。因此選擇面向對象語言來實現(xiàn)領域模型是非常自然的砍鸠。
遺憾的是并不是所有實踐領域建模的同學對面向對象編程都是良好掌握的扩氢!見過不少同學能把所有單實例和多實例的領域概念都用單例表達;無論泛化還是組合關系都能用共有繼承來實現(xiàn)爷辱;無論引用對象的生命周期早或者晚于自己都用指針來表示... 录豺; 在這種實現(xiàn)下,即使做了領域建模饭弓,用了面向對象編程語言双饥,模型和代碼也沒有直接關系。
其實每種面向對象語言都有表達模型的“標準方式”弟断,很早的時候建模工具就已支持自動從模型生成指定編程語言的骨架代碼【た蓿現(xiàn)實中我們很少用這個功能,主要有以下原因:
1)大部分情況下夫嗓,模型圖都很難表達所有的實現(xiàn)語義(做不到迟螺、或者成本太高)。所以自動生成的代碼是不完備的舍咖,還需要人工修改代碼以補充缺失的實現(xiàn)細節(jié)矩父;
2)針對圖的編輯、重用排霉,重構窍株、版本管理,往往不如直接搞代碼來的高效攻柠;
3)每當模型變化后球订,從模型圖重新生成的代碼又要和已實現(xiàn)代碼進行merge,合并成本大瑰钮,效率低冒滩;
4)最后,對于像C/C++這樣比較底層或者復雜的語言來說浪谴,從模型到代碼的自動生成效果會更差开睡,不具備實用性因苹。
因此在現(xiàn)實的情況下,為了追求效率篇恒,程序員們絕大多數(shù)時候還是直接用代碼實現(xiàn)和演進模型扶檐。
但是手動實現(xiàn),不代表可以隨意實現(xiàn)胁艰!遵循一些從模型到代碼的最佳實踐款筑,或者叫做“實現(xiàn)模式”,會讓代碼更加清晰的表達模型腾么,甚至做到“望文生義”奈梳,降低代碼和模型的同步成本。
表達模型的實現(xiàn)模式哮翘,不同的編程語言會有區(qū)別颈嚼。以下是我總結的C++實現(xiàn)模型常用的實現(xiàn)模式。限于篇幅就不再展開了饭寺,對C++比較了解的同學應該都看的懂阻课。
那么用C語言能否很好的實現(xiàn)領域模型呢?
如前面所說艰匙,用編程語言表達模型限煞,需要為對應的編程語言建立起一套表達模型的“實現(xiàn)模式”。
雖然C語言被認為是一門過程化語言员凝,但并不是說C語言就沒有表達領域概念和關系的能力署驻。Robert C. Martin在《Clean Architecture》中甚至認為"C語言的限制其實更少,可以做出更靈活的設計選擇"健霹。
Anyway旺上,我們不去爭論編程語言的優(yōu)劣,我們來看看如何在C中表達領域概念和關系糖埋。
相比用C做過程化設計宣吱,現(xiàn)代化C編程更推崇使用模塊化C
的設計方法。模塊化C
要求用一個“.h”和一個“.c”文件組合實現(xiàn)一個概念(類似一個面向對象中的類)瞳别≌骱颍“.h”文件中包含該概念對應的結構體或者句柄,還有該概念支持的API聲明祟敛;而“.c”文件中包含API的函數(shù)實現(xiàn)疤坝,以及用static
修飾的內(nèi)部共享狀態(tài)與私有函數(shù)實現(xiàn)。
如下代碼表達了Storage
的概念馆铁,可以看到里面包含Storage
的類型定義與API跑揉。所有API的第一個參數(shù)是Storage
自身,加const
表示該API是只讀的叼架,否則是可寫API畔裕。
#include "base/status.h"
typedef struct Storage
{
int capacity;
int type;
} Storage;
/* Read-only */
double storage_charge(const Storage* storage, int months);
int storage_level(const Storage* storage, int months);
/* Writable */
Status storage_promote(Storage* storage);
模塊化C
中一般用結構體的包含關系或者指針引用表達模型中概念之間的組合關系衣撬。而模型中的泛化關系則需要用到C語言的“函數(shù)指針結構體”的設計技巧乖订,具體在編碼的時候還需要區(qū)分泛化關系背后的調用是無狀態(tài)還有有狀態(tài)的扮饶。
如下代碼示例如何通過action_create
創(chuàng)建具有泛化關系的Action
:
#include "point.h"
typedef enum {
ALERT_ACTION, CLEAN_ACTION, MAX_ACTION,
} ActionType;
/* Abstract Interface */
typedef struct Action {
void* data;
void (*exec)(void* data, const Point* point);
void (*destroy)(void* data);
} Action;
/* Factory Function */
Action action_create(ActionType type, const Point* points, int numOfPoints);
介紹了如何用C語言表達概念以及概念間關系后,我們來看看生命周期管理乍构。
領域驅動設計強調對領域對象的生命周期進行顯示的建模和管理甜无。
在不考慮持久化的情況下,領域對象生命周期一般起始于構造函數(shù)哥遮,結束于析構函數(shù)岂丘。但是在C語言中結構體沒有顯示的構造和析構過程,所以生命周期管理一般對應于結構體內(nèi)存的分配與回收眠饮。
在嵌入式場景下奥帘,經(jīng)常使用全局變量按照業(yè)務規(guī)格在靜態(tài)內(nèi)存區(qū)預占內(nèi)存,這導致了程序員很容易把領域對象的生命周期管理和用于內(nèi)存預占的全局變量耦合在一起仪召。
全局變量是缺乏清晰的生命周期語義的寨蹋,它起始于進程初始化,銷毀于進程退出扔茅。而領域對象的生命周期的開始和結束是卻是有清晰的業(yè)務指示的已旧。如果代碼對領域對象的所有訪問都直接使用它對應的全局變量,就會導致領域對象生命周期管理和內(nèi)存管理混淆在一起召娜。再加上全局變量帶來的代碼耦合問題运褪,最終會導致代碼難以理解和維護。
對于這個問題玖瘸,我們可以借鑒領域驅動設計中提出的Factory
和Repository
的概念來承擔領域對象的生命周期管理職責秸讹,并對領域對象的內(nèi)存管理方式進行封裝,對外屏蔽領域對象的創(chuàng)建和存儲的技術細節(jié)雅倒。其它所有需要使用領域對象的代碼都應該通過Factory
或者Repository
獲得領域對象的句柄或者結構體指針璃诀,這樣核心的模型代碼就和內(nèi)存管理方式等基礎設施進行了解耦,也避免了和全局變量的耦合屯断,提升了代碼的可維護性與可理解性文虏。
更多關于模塊化C
以及如何用C語言表達模型的方式,可以借鑒《C現(xiàn)代編程》和《嵌入式C設計模式》中的內(nèi)容殖演,希望隨后有機會能就這個話題更系統(tǒng)性的總結一下氧秘。
追求代碼本身就是模型的直接映射是領域驅動設計強調的一個核心。如果一個更好的解決方案只是存在于設計文檔中趴久,并沒有在代碼中實現(xiàn)猿挚,那么它是沒有產(chǎn)生實際價值的。因此陪竿,保持持續(xù)重構,當有更好的解決方案的時候膳算,就找機會重構進代碼里。只有被代碼真實反映的模型才是當前軟件中真正的模型弛作!