iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
Software Development

一個好的系統之好維護基本篇 ( 馬克版 )系列 第 13

Day-13: 契約式設計 ( DBC Design By Contract ) vs 防禦式程式設計( Defensive Programming )

  • 分享至 

  • xImage
  •  

同步至 medium

https://ithelp.ithome.com.tw/upload/images/20240929/20089358BiKHcMWNH3.png


接下來第 13 天開始後,會比較往我在探索軟體工程整個過程中,有看到一些我想提出來的一些思考,並且整理一些我自已的想法,有些我已經有在用了,有些沒有 ~ 就自已當參考吧。

然後這篇文章我們將要來談談以下來個東西 :

  • 防禦式程式設計( Defensive Programming )
  • 契約式設計 ( DBC Design By Contract )

防禦式程式設計 ( Defensive Programming )

它是一種的主要核心概念為 :

保護自已,不相信任何人,也就是從外部進來的所有東西,都要檢查

在任何情況下,都有保證 code 的可靠性,先來給個簡單的範例如下,它是一個課程平台,然後如果完全要走這個概念來開發,那大概就會寫的如下一樣,一堆的檢查,事實上如果真的要寫還可以繼續寫下去。

class Product {
  constructor(public id: number, public name: string, public price: number) {}
}

class ShoppingCart {
  private items: { product: Product, quantity: number }[] = [];

  // 防禦性檢查,確保產品和數量的有效性
  addItem(product: Product, quantity: number): void {
    // 檢查產品是否有效
    if (!product) {
      throw new Error("Invalid product.");
    }

    // 檢查產品 ID 是否有效
    if (!product.id || product.id <= 0) {
      throw new Error("Invalid product ID. It must be a positive number.");
    }
    
    const productIdExist = await ProductMode.isExistAsync(product.id)
    if(!productIdExist){
      throw new Error("The product is nonexistent")
    }

    // 檢查產品名稱是否有效
    if (!product.name || product.name.trim() === '') {
      throw new Error("Invalid product name. It cannot be empty.");
    }

    // 檢查產品價格是否有效
    if (!product.price || product.price <= 0) {
      throw new Error("Invalid product price. It must be a positive number.");
    }

    // 檢查數量是否有效
    if (!quantity || quantity <= 0) {
      throw new Error("Invalid quantity. It must be a positive number.");
    }

    // 檢查產品是否已經在購物車中
    const existingItem = this.items.find(item => item.product.id === product.id);
    if (existingItem) {
      throw new Error(`Product with ID ${product.id} is already in the cart.`);
    }

    // 添加產品到購物車
    this.items.push({ product, quantity });
  }
}

客觀來說,不能說有錯,但是有個東西要想一下 :

如果 product 早就已經被驗證過了呢 ?

然後它還有一些潛在的問題 :

  • 過度複雜化,增加維護、開發成本 : 每個程式碼都會變很長,而且每個 unit test 也是。
  • 性能影響 : 因為每個方法都要檢查,這也代表有可能會有重複檢查的情況。

然後在 DBC 的作者寫的一篇論文中《Applying “Design by Contract》也有提到這些問題,所以他提出了 Design By Contract ( DBC ) 來當替代方案。

然後我這裡先說一下我的想法,之前我年輕時的確也一直都有不要相信任何傳進來的東西,所以寫了一大堆的檢查,一開始寫到只是單純的簡單驗證那還好,但後來有些驗證可能還會去 db 要資料來判斷在不在,然後就有在開始思考,這個事情是對的嗎 ?

如果是 API 我覺得還有理由,因為可能會被用戶玩壞,但是拉回到自已系統內,有時後就在想,真的有需要這樣嗎 ? 所以後來往這個方向來查,就查到這兩個名詞,原來不是只有我有這個煩惱……

所以 code review 時,通常如果是 API 那層我會傾向用防禦式來看,而在內部時就會選擇偏相信帶入的


契約式設計 ( DBC: Design By Contract )

這個是《Applying “Design by Contract》中的核心想法 :

software elements should be considered as implementations meant to satisfy well-understood specifications, not as arbitrary executable texts. This is where the contract theory comes in.

然後我自已的理解為 :

軟體應該被視為為了滿足明確規範(Contract)而設計的實現,而不是任意的可執行代碼

而下面就是 contract 內的三種規範條件:

  • 前置條件(Precondition)=> 客戶(呼叫方)的義務
  • 後置條件(Postcondition)=> 供應商(實作的方法)的責任
  • 不變條件(Invariant)=> 在整個生命周期(執行這方法的期間),有那些狀態一定是不變的。

然後加上這幾個以後,我覺得比較好理解話為 :

一個方法呼叫方有義務帶入符合Precondition的資料進來,並且實作方有責任達成後置條件與回傳結果,如果有 error 那就一定是呼叫方的問題

然後下面比較算是參考其它網路資料後,將上面的防禦式程式設計的範例,拿下來改,整體來說這個方法讓我們明確的知道前置條件那些是呼叫方的義務,那時是這個方法的責任,但我自已寫有個點很 confuse 那就是 :

我好像還是要檢查 Precondition 啊……

奇怪是我理解有問題嗎… 就算看目前最完整支援 DbC 的語言 Eiffel 的範例看起來的確還是會需要檢查 Precondition ……

https://www.eiffel.org/doc/solutions/Design_by_Contract_and_Assertions

    make (a_nm: STRING; a_offset: INTEGER)
            -- Initalize with name `a_nm' and utcoffset `a_offset'.
        require
            name_not_empty: a_nm /= Void and then not a_nm.is_empty
            offset_valid: a_offset >= -12 and a_offset <= 12
        do
            name := a_nm.twin
            utcoffset := a_offset
        ensure
            name_initialized: name.is_equal (a_nm)
            utcoffset_initialized: utcoffset = a_offset
        end

但我在猜,它的核心還在於明確規範(Contract),而不是什麼東西都假設別人會亂來 ~

class ShoppingCart {
  private items: { product: Product, quantity: number }[] = [];

  /**
   * 添加產品到購物車
   * 
   * 它期望的是什麼?(前置條件)
   * - 產品 ID、名稱和價格必須是有效的(由 Product 類內的檢查保證)
   * - 數量必須是正數
   * - 產品不能已經存在於購物車中
   * 
   * 它要保證的是什麼?(後置條件)
   * - 購物車中的產品數量會增加,且不能有重複的產品
   */
  async addItem(product: Product, quantity: number): Promise<void> {
    // 前置條件檢查
    this.requireProductExists(product); // 產品存在
    await this.requireValidProductId(product.id); // 產品 ID 合法
    this.requirePositiveQuantity(quantity); // 數量必須為正數
    this.requireProductNotInCart(product.id); // 產品不能已存在於購物車

    // 添加產品到購物車
    this.items.push({ product, quantity });

    // 後置條件檢查
    this.ensureProductAdded(product.id); // 確保產品已成功加入購物車
    this.ensureNoDuplicateProducts(); // 確保購物車中沒有重複產品
  }
  
  private requireProductExists(product: Product): void {
    if (!product) {
      throw new Error("Product is required.");
    }
  }

  private requirePositiveQuantity(quantity: number): void {
    if (quantity <= 0) {
      throw new Error("Quantity must be a positive number.");
    }
  }

  private requireProductNotInCart(productId: number): void {
    const existingItem = this.items.find(item => item.product.id === productId);
    if (existingItem) {
      throw new Error(`Product with ID ${productId} is already in the cart.`);
    }
  }

  private ensureProductAdded(productId: number): void {
    const existingItem = this.items.find(item => item.product.id === productId);
    if (!existingItem) {
      throw new Error(`Postcondition failed: Product with ID ${productId} was not added to the cart.`);
    }
  }

  private ensureNoDuplicateProducts(): void {
    const productIds = this.items.map(item => item.product.id);
    const uniqueProductIds = new Set(productIds);
    if (productIds.length !== uniqueProductIds.size) {
      throw new Error("Postcondition failed: Duplicate products found in the cart.");
    }
  }
}

小結

整體來說這兩個設計思想的核心分別如下 :

  • 防禦式程式設計( Defensive Programming ) : 不相信任何人
  • 契約式設計 ( DBC Design By Contract ) : 相信合約

我自已本身年輕時比較算是走防禦式程式設計這一塊的思想,就是每個方法都假設帶入的參數不可以相信,所以全部都要檢查,這個思想簡單,就是 check 到爆,但後來到了一定的經驗後,就在想這樣是合理的嗎 ?

然後接下來查到 DBC 它所提的 :

軟體應該被視為為了滿足明確規範(Contract)而設計的實現,而不是任意的可執行代碼

程式應該是我們說好這個方法要如何使用,而不是不相信一切,如果真的不相信一切,那我覺得只有在有錢有閒的公司才做的到。

不過我也承認在這篇文章中的 DBC 範例我自已覺得只是個樣子,是不是對的我也有點不確定,但是我自已覺得至少在開發時,每一次的檢查都可以試這分看看PreconditionPostconditionInvariant,雖然可能還不能減少太多 check,但至少可以知道那些職責是呼叫方,那些時職責是實作方的。


上一篇
Day-12: 實務時 Code Review 時會看的地方 ( 錯誤處理 )
下一篇
Day-14: 提升維護性與降低複雜度的好方法之 Domain Model
系列文
一個好的系統之好維護基本篇 ( 馬克版 )30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
Sunny.Cat
iT邦新手 3 級 ‧ 2024-09-28 10:53:46

很有共鳴(點頭點頭

馬克 iT邦研究生 2 級 ‧ 2024-09-29 21:31:39 檢舉

哈哈哈 ~ 看來也是碰過坑的人 ~

然後你寫的文章也很棒呢 ~

我要留言

立即登入留言