1. 程式人生 > >解構領域驅動設計(一):為什麼領域驅動設計能夠解決軟體複雜性

解構領域驅動設計(一):為什麼領域驅動設計能夠解決軟體複雜性

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的核心思想,讓你明白它是如何解決複雜性的。