解構領域驅動設計(一):為什麽領域驅動設計能夠解決軟件復雜性
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的核心思想,讓你明白它是如何解決復雜性的。
解構領域驅動設計(一):為什麽領域驅動設計能夠解決軟件復雜性