解構領域驅動設計(一):為什麼領域驅動設計能夠解決軟體複雜性
1 為什麼我要研究領域驅動設計
1.1 設計方法各樣且程式碼無法反映設計
我大概從2017年10月份開始研究DDD,當時在一家物流資訊化的公司任職架構師,研究DDD的初衷在於為團隊尋找一種軟體設計的方法論。作為架構師,經常參與設計評審,包括:需求評審、設計評審、程式碼評審。在評審過程中,有一點感受非常深,就是評審過程非常痛苦且幾乎沒有效率和成果。讓我痛苦的地方有:
- 每一個系統分析師都是基於自己的方式來進行設計功能,有的用類圖、有的基於流程圖,有的詳細、有的粗放,更麻煩的是,大家對業務背景的理解程度完全不同,很難找出設計的不合理性。
- 評審程式碼時,我幾乎很難將其與設計對應起來,看設計我已經夠痛苦了,還要被這些程式碼再虐待一遍,實在痛苦至極,這樣的程式碼評審也就變成了程式碼規範性、程式碼設計優雅度的評審,很難找出程式碼業務邏輯的問題。讓程式碼正確的反應設計,是當時評審過程中碰到的一個更大的問題。
1.2 程式碼質量很難有效提升
在承擔架構師之前,我的另一個職責是技術管理,做的工作是與軟體質量相關的。當時加入一個大概2000萬規模的專案,有大約100開發人員參與,開發週期大概1年。加入該團隊在開發的過程中,發現了兩個問題:
- 每一個BA(可以理解為PD)設計的產品介面操作習慣都不一樣,所有的開發人員做出來的介面的操作也完全不同。但是,這是一個面向物流行業的資訊化軟體,操作習慣的一致性很重要。
- 程式碼非常混亂,沒有任何的規範可言,看程式碼簡直想吐。
基於第一個問題,我定義了統一的介面規範,這個介面規範通過和公司的PMO合作將其融入到工程過程中,作為開發人員必須遵循的規範。第二個問題,我則花費了很多的時間來嘗試解決(大概有2年時間都與程式碼質量做鬥爭),最終與尋找統一的設計方法殊途同歸。
如何讓我們的程式碼變得更加乾淨,我在執行的過程中,按照以下步驟一步一步的執行。
- 定義了統一的程式碼規範,基於介面規範的基礎上,統一定義了模板工程,這些模板工程都有很好的程式碼基因。
- 定義了程式碼規範的培訓教程,包括基本的書寫規範、《程式碼整潔之道》、《重構技巧》。
- 定義了程式碼規範、程式碼評審制度,寫入PMO定義的過程工作,作為開發人員遵循的制度。
- 通過程式碼評審提升質量太慢,為了大規模快速推廣,引入了SonarQube,定義了軟體程式碼質量的度量方法,軟體的程式碼質量分數由:圈複雜度、重複率、程式碼規模問題、SonarQube掃描的問題數四個維度來衡量。在度量方法之上,定義了程式碼質量管理制度,每週掃描軟體獲得詳細的程式碼質量報告,傳送給相應的產品負責人,將程式碼質量管理制度也融入PMO的工程過程裡面,全公司進行推廣,由產品負責人負責本部分的程式碼質量提升。
基於以上的程式碼質量管理方法,我認為已經是做的相當不錯,但是非常遺憾的是,當我抽樣評審產品的程式碼時,我依然感到無比沮喪,軟體的程式碼還是太複雜、太難看懂了,與《程式碼整潔之道》的要求相差太遠了,我耗費了1年多的工作幾乎毫無成果可言。因此,我在深深思考,在編碼層面,定義了規範、做了優雅編碼培訓、定義了編寫優秀程式碼的相關制度,就為了讓開發人員把程式碼寫好,使程式碼看起來更加清晰,軟體更加容易維護,為什麼還是無法實現?
2 軟體複雜性的根源
貧血模型是軟體複雜性的根源。貧血模型本質是面向資料的設計,面向過程的編碼。基於貧血模型的分層架構,通常分為UI層、業務邏輯層、資料訪問層、貧血模型層,貧血模型與資料模型一致,業務邏輯主要集中在業務邏輯層,業務邏輯層非常厚重。業務邏輯實現過程中,混雜了上層UI展現邏輯、資料庫訪問、快取等各種邏輯,業務邏輯很凌亂的分散在各個層和關聯物件,被非業務邏輯的程式碼隔離。通過業務邏輯層來還原真實的業務邏輯非常的困難,因此,很難從程式碼反映其設計,並且,複雜度會隨著需求的變更變得更加複雜。
以下是一段示例基於貧血模型開發的程式碼。
public OrderDto signOrder(Order order) { Assert.notNull(order, "OrderDto can not be null."); OrderDto result = new OrderDto(); result.setIsOperationSuccess(true); if (null == order.getId()) { result.setIsOperationSuccess(false); result.setOperationMassage("id不能為空。"); return result; } OrderCondition orderCondition = new OrderCondition(); orderCondition.setId(order.getId()); order = orderMapper.selectOne(orderCondition); if (null == order) { result.setIsOperationSuccess(false); result.setOperationMassage("該訂單不存在。"); return result; } if (order.getOrderStatus() != Integer.valueOf(StatusEnum.ORDER_STATUS.ORDER_WAIT_RECEIVE.getCode())) { result.setIsOperationSuccess(false); result.setOperationMassage("訂單號:{" + order.getOrderNo() + "}不是待收貨狀態,不能進行簽收。"); return result; } // 該訂單下的所有商品的實收數(發貨數量)必須都大於0 boolean validDeliveryCount = true; Double orderTotalAmount = 0d; List<OrderGoodsDto> orderGoodsList = orderGoodsBiz.selectOrderGoodsByOrderId(order.getId()); List<OrderGoods> orderGoodsListForUpdate = new ArrayList<>(); if (EmptyUtil.isNotEmpty(orderGoodsList)) { for (OrderGoodsDto orderGoods : orderGoodsList) { if (null == orderGoods.getDeliveredNum() || orderGoods.getDeliveredNum() <= 0) { validDeliveryCount = false; } else { // 根據商品發貨數量重新計算訂單總金額...... Double price = (null == orderGoods.getDiscountPrice() ? orderGoods.getOriginalPrice() : orderGoods.getDiscountPrice()); Integer goodsNum = (null == orderGoods.getDeliveredNum() ? 0 : orderGoods.getDeliveredNum()); orderTotalAmount += price * goodsNum; // 更新orderGoods的收貨數量 orderGoods.setReceivedNum(goodsNum); OrderGoods orderGoodsForUpdate = new OrderGoods(); BeanUtils.copyProperties(orderGoods, orderGoodsForUpdate); orderGoodsListForUpdate.add(orderGoodsForUpdate); } } } if (!validDeliveryCount) { result.setIsOperationSuccess(false); result.setOperationMassage("訂單號:" + order.getOrderNo() + ",訂單下所有商品都已發貨才可進行簽收操作,請確認。"); return result; } order.setOrderStatus(Integer.valueOf(StatusEnum.ORDER_STATUS.ORDER_SIGN.getCode())); order.setOrderTotalAmount(orderTotalAmount); order.setPaymentAmount(orderTotalAmount); order.setUnpaidAmount(orderTotalAmount); update(order); orderGoodsBiz.batchUpdate(orderGoodsListForUpdate); List<Order> orders = new ArrayList<Order>(); orders.add(order); saveRouteMessage(orders); return result; }
類似這樣的程式碼非常常見,通過閱讀這段業務邏輯程式碼,可以發現它處理了以下的任務:
(1)返回結果的處理。
(2)資料庫訪問。
(3)關聯物件的資料庫訪問。
(4)業務規則。
業務規則程式碼混雜在各種任務的程式碼中,通過程式碼還原業務規則會越來越複雜且隨著時間推移,程式碼邏輯會越來越偏離設計。作為軟體系統最核心的部分——業務規則,如果我們僅僅將其從其它任務中剝離,我們的程式碼將演化如下。
public void signOrder(Order order) { Assert.notNull(order, "OrderDto can not be null."); if (order.getOrderStatus() != Integer.valueOf(StatusEnum.ORDER_STATUS.ORDER_WAIT_RECEIVE.getCode())) { throw new BusinessException(“訂單號:{" + order.getOrderNo() + "}不是待收貨狀態,不能進行簽收。"); } // 該訂單下的所有商品的實收數(發貨數量)必須都大於0 boolean validDeliveryCount = true; Double orderTotalAmount = 0d; List<OrderGoods> orderGoodsList = order.getOrderGoods(); if (EmptyUtil.isNotEmpty(orderGoodsList)) { for (OrderGoods orderGoods : orderGoodsList) { if (null == orderGoods.getDeliveredNum() || orderGoods.getDeliveredNum() <= 0) { validDeliveryCount = false; } else { // 根據商品發貨數量重新計算訂單總金額...... Double price = (null == orderGoods.getDiscountPrice() ? orderGoods.getOriginalPrice() : orderGoods.getDiscountPrice()); Integer goodsNum = (null == orderGoods.getDeliveredNum() ? 0 : orderGoods.getDeliveredNum()); orderTotalAmount += price * goodsNum; // 更新orderGoods的收貨數量 orderGoods.setReceivedNum(goodsNum); } } } if (!validDeliveryCount) { throw new BusinessException(“訂單號:" + order.getOrderNo() + ",訂單下所有商品都已發貨才可進行簽收操作,請確認。"); } order.setOrderStatus(Integer.valueOf(StatusEnum.ORDER_STATUS.ORDER_SIGN.getCode())); order.setOrderTotalAmount(orderTotalAmount); order.setPaymentAmount(orderTotalAmount); order.setUnpaidAmount(orderTotalAmount); }
你可以發現這段單純實現業務規則的程式碼,會更加的簡單、清晰,也會使軟體更加的容易維護。在DDD的方法論裡面,業務規則是在領域層來實現的,領域層的程式碼僅僅是業務規則,這時候,其分層架構的分層邏輯和基於貧血模型的分層邏輯也會不一樣了。
通過以上程式碼的對比我們發現:
- 剝離業務規則無關的程式碼,將更加清晰簡單,容易和業務規則保持一致。
- 貧血模型會導致業務邏輯層混雜了太多程式碼和邏輯,難以還原業務規則,保證程式碼與設計一致性,是複雜性根源。
3 DDD如何解決軟體複雜性
DDD解決軟體複雜性的方法核心為兩點:
- 通過領域模型為業務知識建模,領域模型作為業務、技術團隊溝通的統一語言。
- 確保軟體實現與領域模型保持一致。
軟體實現與領域模型保持一致是本書的核心思想,DDD構建了一套完整的方法論來支援領域模型驅動程式設計。這套方法論簡述如下。
- 分層架構:業務規則的程式碼只佔軟體很少的程式碼卻是最核心的部分程式碼,將其分離出來作為獨立的領域層,使領域層的實現與領域模型保持一致,領域層的業務物件不再是貧血模型。
- 領域驅動設計:領域驅動設計,即領域模型驅動程式設計。這裡給出瞭如何通過程式碼表達領域模型的編碼模式。這些模式包括:關聯、實體、值物件、服務、聚合根、Repository、Factory。它們構建了將領域模型表達成程式碼的方法論,保證了程式碼和設計一致。
- 戰略設計:複雜領域模型的實現方法論。
我將在下一篇文章中詳細解釋DDD的核心思想,讓你明白它是如何解決複雜性的。