IDDD 實現領域驅動設計-一個簡單業務用例的回顧和理解


上一篇:《IDDD 實現領域驅動設計-由貧血導致的失憶症

這篇博文是對《實現領域驅動設計》第一章后半部分內容的理解。


Domain Experts-領域專家

這節點內容是昨天的一個討論引發的思考。

什么是領域專家?簡單來說,就是對某一業務領域精通的人,這個人可以是醫生、學者、作家、藝術家等等,不管是什么職業,什么身份,只要對某一業務領域精通,都可以稱之為領域專家。這樣說可能會讓你感到茫然,我舉一個例子,比如你們軟件公司要開發一套快遞行業的業務系統,然后你需要到實際企業去了解業務流程等等,暫時把這個實際企業想象成很小(非三通一達),那么你到這個企業第一時間找的是誰呢?准確來說,應該是這個公司的 CEO,因為只有他最最了解他們公司的業務,畢竟是他創辦的公司,CEO 不了解,還有誰還了解呢,那么,這個公司的 CEO 就可以看作是領域專家。CEO 一般是蠻忙的,有很多的瑣事需要處理,所以,在你和他聊天了解業務的時候,最好是先准備一杯咖啡!

當我們開發人員自己開發一套系統的時候,在開發團隊之間,領域專家的概念就慢慢淡化了,為什么?因為領域專家變成了我們開發人員自己,自己給自己布置業務,然后自己再去完成,這樣雖然很高效,因為沒有非技術人員的參與溝通,但是這樣就會造成一些問題,比如,開發人員在思考業務流程的時候,會按照開發人員的思路去理解,比如,一個簡單的業務操作描述,開發人員會首先想到的什么呢?一個表單和一個 Button,然后就是對這個表單和 Button 操作的具體實現了,等項目開發完成后,需要交付真正的客戶去檢驗,客戶讓你演示這個業務操作,然后你就開始對表單和 Button 進行操作了,說這就是業務操作,但是,客戶突然來一句:我們不要表單和 Button 操作,UI 需要重新搞,這時候,你就傻眼了,因為你所有的內容代碼實現都是圍繞着表單和 Button。說了這么多,到底是什么意思呢?在這個過程中,你並不了解這個業務操作背后所蘊含的業務含義,首先,業務不是 UI,UI 只不過是業務的一部分體現,有時候,業務僅僅只是領域專家的一段描述,開發人員需要對這個業務描述,進行一點一點的抽離,把術語和操作分離開,然后再和領域專家進行深入的探討,這個過程可能會花很多的時間,但是是非常重要的,做完這些前期工作,你再去實現業務操作,你會發現,不管 UI 如何變化,這個業務操作的本質是沒有發生變化的,也就是說你的內部代碼不需要進行修改,UI 修改那就交給前端工程師就可以了,和你沒太大關系。總的來說,就是不要讓 UI 驅動你開發,而是讓業務驅動你開發。

對上面的內容,我還需要補充一點,就是開發人員需要領域專家,開發人員和領域專家的身份最好不要重疊,要不然會造成一系列的問題,還有就是,在整個領域驅動設計的過程中,開發人員和領域專家的地位是相同的,不要有任何的輕視心態,要用平等的心態去溝通交流。領域專家的概念,讓我想到一個很相似的事,就是蘋果在開發一個產品的時候,會請很多的非技術人員參與,這些人遍布各行各業,醫生、學者、作家、藝術家等等,蘋果為什么要請他們,就是想讓他們參與產品的設計,因為他們就是產品的使用者,他們提出的想法就是實實在在的用戶建議,這個產品開發過程,其實就可以看作是領域(產品)驅動設計,這些參與產品設計的非技術人員,就可以看作是領域(產品)專家。

一個簡單業務用例的回顧和理解

這個簡單業務用例描述是這樣的:一個 Scrum 模型,我們需要將一個待定項(Backlog Item)提交到沖刺(Sprint)中去。

這是最簡答的描述,沒有經過和領域專家進行深入溝通的,Scrum 是敏捷開發中的概念,這個就不說明了,因為我也不懂,你只需要知道上面的操作就可以了,一般的實現方式(屬性訪問):

public class BacklogItem extends Entity {
    private SprintId sprintId;
    private BacklogItemStatusType status;
    ...
    public void setSprintId(SprintId sprintId) {
        this.sprintId = sprintId;
    }

    public void setStatus(BacklogItemStatusType status) {
        this.status = status;
    }
    ...
}

客戶端調用:

// client commits the backlog item to a sprint
// by setting its sprintId and status

backlogItem.setSprintId(sprintId);
backlogItem.setStatus(BacklogItemStatusType.COMMITTED);

上面的實現過程,完全和上一篇 saveCustomer 的實現方式一樣,這樣做沒什么不可以,因為我也這樣干過,只是你會總感覺有哪些不對勁的地方,首先,在實現待定項提交到沖刺這個操作的時候,你首先查看的是 BacklogItem 中的屬性,然后就是對這個屬性進行設置,在這個過程中,你忘記了你實現的是一個行為操作,而不是一個屬性賦值操作,這樣說來,是不是有點腳本模式開發,還有就是如果客戶端第二個屬性賦值 setStatus 出現了錯誤,因為第一個 setSprintId 已經成功完成,這個該怎么進行處理,即使有處理,這個操作也完全放在了客戶端去完成,像 saveCustomer 一樣,如果再增加一個屬性賦值操作,你的實現將越改越亂,最重要的是,再客戶端暴露了 BacklogItem 模型的具體結構,這個應該是要避免的。

我們再來看另一種實現方式:

public class BacklogItem extends Entity {
    private SprintId sprintId;
    private BacklogItemStatusType status;
    ...

    public void commitTo(Sprint aSprint) {
        if (!this.isScheduledForRelease()) {
            throw new IllegalStateException(
                "Must be scheduled for release to commit to sprint.");
        }
        
        if (this.isCommittedToSprint()) {
            if (!aSprint.sprintId().equals(this.sprintId())) {
                this.uncommitFromSprint();
            }
        }
        
        this.elevateStatusWith(BacklogItemStatusType.COMMITTED);
        
        this.setSprintId(aSprint.sprintId());
        
        DomainEventPublisher
            .instance()
            .publish(new BacklogItemCommitted(
                    this.tenant(),
                    this.backlogItemId(),
                    this.sprintId()));
    }
    ...
}

客戶端調用:

// client commits the backlog item to a sprint
// by using a domain-specific behavior

backlogItem.commitTo(sprint);

將第一種是實現方式出現的問題,再和第二種方式進行比較,你會發現,第二種實現方式完全避免掉了,在開始的時候,我們說了,這是一個最簡單的業務操作描述,沒有和領域專家進行深入探討和交流,如果進行探討和交流的話,最后詳細、准確的業務操作描述,應該是這樣:

  • 允許將每一個待定項提交到沖刺中,只有在一個待定項位於發布計划(Release)中時才能進行提交,如果一個待定項已經提交到了另外一個沖刺中,那么需要先將其回收,提交完成時,通知相關客戶方。

對於一個詳細、准確的業務操作描述,如何進行確定下來,作者進行了如下總結:

  1. 對於你目前正在工作的業務領域,思考一下模型中的通用術語和業務操作。
  2. 將術語寫在白板上。
  3. 然后,將項目中所用到的短語也寫下來。
  4. 與真正的領域專家交流一下,看看哪些詞匯是可以改善的(記得帶上咖啡哦)。

我們再來分析一下上面第二種實現方式,希望可以抽離出一些對自己有所幫助的理解,首先,讀上面的業務操作描述,然后再和實現代碼進行對比,你會發現,它們之間的關系是完全契合的,在上一篇中,我們說過,設計就是代碼,代碼就是設計,這種設計就是一種通用語言,開發人員和領域專家都能懂的通用語言。

在第二種實現的方式中,有兩個關鍵詞:commitTo 和 DomainEventPublisher,DomainEventPublisher 是領域事件(Domain Event),這個不要和領域服務(Domain Service)混淆,領域事件我沒有使用過,后面再進行學習,你暫時可以把它看作是操作完成后的消息推送者。commitTo 是 BacklogItem 模型中的一個行為,意為提交,你可能會這樣想:待定項怎么會有行為呢?它又不是人,我覺得這個很有意思,記得在之前做消息模型設計的時候,一直不確定的一點是發消息這個操作該如何設計?是消息實體的一個行為操作,還是發件人的一個行為操作,又或者是獨立出來的一個領域服務(最后結果),在這個設計確定的過程中,我們會進行多次討論,但有一點需要進行明確的是,不只是具有“生命”的實體,才具有行為操作,就像消息模型中的操作人,你自然會聯想到現實生活中的發件人、收件人等等,認為只有人才會有一些行為操作,但是實際上,在軟件系統中,一切的模型都有可能是行為操作,你要摒棄現實生活對你的影響,就像上面待定項的提交操作,如果是我設計的話,我會創建一個領域服務進行行為操作,因為,在我的認知中,待定項不具有行為操作,但顯然並不是這樣,為什么要這樣設計?現在還說不出個所以然,以后再慢慢體會。

DDD 並不笨重(測試驅動)

DDD(領域驅動設計)和 TDD(測試驅動開發),這兩者有什么關系?我記得在之前的博文中有提到這一點,我的觀點是,DDD 和 TDD 可以之間可以產生一些微妙的化學反應,並不一定要強制的去區分它們之間的關系,比如,如果你的 DDD 項目中,使用了 TDD,並不能說明你的項目就不是 DDD 模式了,其實,TDD 可以對 DDD 進行一些補充,或者可以讓你的項目,在使用 DDD 的時候,變得如魚得水。關於它們兩者的關系,作者簡單說明了一下觀點:DDD 也傾向於“測試先行,逐步改進”的設計思路,他們可能有細微的區別,但是基本思路是一樣的,DDD 采用的是一種“敏捷的”方式進行軟件開發的。

可以采取的步驟:

  1. 編寫測試代碼以模擬客戶代碼是如何使用該領域對象的。
  2. 創建該領域對象以使測試代碼能夠編譯通過。
  3. 同時對測試和領域對象進行重構,直到測試代碼能夠正確地模擬客戶代碼,同時領域對象擁有能夠表明業務行為的方法簽名。
  4. 實現領域對象的行為,直到測試通過為止,再對實現代碼進行重構。
  5. 向你的團隊成員展示代碼,包括領域專家,以保證領域對象能夠正確地反映通用語言。

具體再說明一下,像上面的待定項提交業務操作,可以完全先寫一個測試代碼,如下:

[test]
public void  backlogItemCommit() {
    ...
}

這個測試代碼,其實就是領域專家想要的,他不管你是如何具體實現的,他關心的是有沒有這個業務操作,以及這個業務操作完成的結果,也就是說,測試代碼可以很好的反應領域專家所描述的業務操作,那有人可能就會說了:你這就不是 DDD 了,而是 TDD,表明看上去,好像確實如此,但是不能說寫個測試代碼就是 TDD 開發,而去測試代碼並不能反映領域模型,他只是一種輔助方式,你可以把它看作是通用語言的一種,可以幫助你和領域專家進行溝通,也可以加快你的開發速度,又或者可以幫助你完善你的領域模型設計。對應某一業務操作的測試代碼,也不是一成不變的,它需要開發人員和領域專家的持續溝通和改進,測試代碼就是他們進行通用語言的一種表現形勢,使用測試代碼的好處就是,它可以很好的表現業務需求,當然你也可以使用 UI,這些都不過是通用語言的一種罷了。

在讀《DDD 並不笨重》這一小節點內容的時候,我是很有感觸和共鳴的,因為我在之前短消息開發的時候,就曾這樣搞過,比如,新建一個與 Domain 對應的 Domain.Tests 項目,這個 Domain.Tests 就是你和領域專家進行溝通的一個橋梁。

對於這個節點內容,可能每個人都有自己的理解,如果大家有不同的想法,歡迎探討交流,就記錄到這!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM