Linux中國

ZOMBIES:在軟體開發中定義邊界和介面(三)

喪屍是沒有邊界感的,需要為你的軟體設定限制和期望。

喪屍沒有邊界感。它們踩倒柵欄,推倒圍牆,進入不屬於它們的地盤。在前面的文章中,我已經解釋了為什麼把所有編程問題當作一群喪屍一次性處理是錯誤的。

ZOMBIES 代表首字母縮寫:

  • Z – 最簡場景(Zero)
  • O – 單個元素場景(One)
  • M – 多個元素場景(Many or more complex)
  • B – 邊界行為(Boundary behaviors)
  • I – 介面定義(Interface definition)
  • E – 處理特殊行為(Exercise exceptional behavior)
  • S – 簡單場景用簡單的解決方案(Simple scenarios, simple solutions)

在本系列的前面兩篇文章中,我演示了 ZOMBIES 方法的前三部分:最簡場景、單元素場景和多元素場景。第一篇文章 實現了最簡場景,它提供了代碼中的最簡可行路徑。第二篇文章中針對單元素場景和多元素場景 運行測試。在這篇文章中,我將帶你了解邊界和介面。

回到單元素場景

要想處理邊界,你需要繞回來(迭代)。

首先思考下面的問題:電子商務的邊界是什麼?我需要限制購物框的大小嗎?(事實上,我不認為這有任何意義。)

目前唯一合理的邊界條件是確保購物框里的商品數量不能為負數。將這個限制表示成可運行的期望:

[Fact]
public void Add1ItemRemoveItemRemoveAgainHas0Items() {
        var expectedNoOfItems = 0;
        var actualNoOfItems = -1;
        Assert.Equal(expectedNoOfItems, actualNoOfItems);
}

這就是說,如果你向購物框里添加一件商品,然後將這個商品移除兩次,shoppingAPI 的實例應該告訴你購物框里有零個商品。

當然這個可運行期望(微測試)不出意料地會失敗。想要這個微測試能夠通過,最小改動是什麼呢?

[Fact]
public void Add1ItemRemoveItemRemoveAgainHas0Items() {
        var expectedNoOfItems = 0;
        Hashtable item = new Hashtable();
        shoppingAPI.AddItem(item);
        shoppingAPI.RemoveItem(item);
        var actualNoOfItems = shoppingAPI.RemoveItem(item);
        Assert.Equal(expectedNoOfItems, actualNoOfItems);
}

這個期望測試依賴於 RemoveItem(item) 功能。目前的 shippingAPI 還不具備該功能,你需要增加該功能。

回到 app 文件夾,打開 IShippingAPI.cs 文件,新增以下聲明:

int RemoveItem(Hashtable item);

ShippingAPI.cs 中實現該功能:

public int RemoveItem(Hashtable item) {
        basket.RemoveAt(basket.IndexOf(item));
        return basket.Count;
}

運行,然後你會得到如下錯誤:

![Error](/data/attachment/album/202305/30/092254w5456rl7z4071rr5.png "Error")

系統在移除一個不在購物框的商品,這導致了系統崩潰。加一點點 防禦式編程defensive programming

public int RemoveItem(Hashtable item) {
        if(basket.IndexOf(item) >= 0) {
                basket.RemoveAt(basket.IndexOf(item));
        }
        return basket.Count;
}

在移除商品之前先檢查它是否在購物框中。(你可能試過用捕獲異常的方式來處理,但是我認為上面的處理方式更具可讀性。)

更多具體的期望

在講更多具體的期望之前,讓我們先探討一下什麼是介面。在軟體工程中,介面表示一種規範,或者對能力的描述。從某種程度上來說,介面類似於菜譜。它羅列出了製作蛋糕的原材料,但它本身並不能吃。我們只是按照菜譜上的說明來烤蛋糕。

與此類似,我們首先通過說明這個服務能做什麼的方式來定義我們的服務。這個描述說明就是所謂的介面。但是介面本身並不能向我們提供任何功能。它只是指導我們實現指定功能的藍圖而已。

到目前為止,我們已經實現了介面(只是某部分實現了,稍後還會增加新功能)和業務處理邊界(也就是購物框里的商品不能是負數)。你指導了 shoppingAPI 怎麼向購物框添加商品,並通過 Add2ItemsBasketHas2Items 測試驗證了該功能的有效性。

然而僅僅具備向購物框添加商品的功能還不足以使其成為一個網購應用程序。它還需要能夠計算購物框里的商品的總價。現在需要增加另一個期望。

按照慣例,從最直接明了的期望開始。當你向購物框里加入一件價值 ¥10 的商品時,你希望這個購物 API 能正確地計算出總價為 ¥10。

第五個測試(偽造版)如下:

[Fact]
public void Add1ItemPrice10GrandTotal10() {
        var expectedTotal = 10.00;
        var actualTotal = 0.00;
        Assert.Equal(expectedTotal, actualTotal);
}

還是一樣的老把戲,通過硬編碼一個錯誤的值讓 Add1ItemPrice10GrandTotal10 測試失敗。當然前三個測試成功通過,但第四個新增的測試失敗了:

A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.57] tests.UnitTest1.Add1ItemPrice10GrandTotal10 [FAIL]
  X tests.UnitTest1.Add1ItemPrice10GrandTotal10 [4ms]
  Error Message:
   Assert.Equal() Failure
Expected: 10
Actual: 0

Test Run Failed.
Total tests: 4
     Passed: 3
         Failed: 1
 Total time: 1.0320 Seconds

將硬編碼值換成實際的處理代碼。首先,檢查介面是否具備計算訂單總價的功能。根本沒有這種東西。目前為止介面中只聲明了三個功能:

  1. int NoOfItems();
  2. int AddItem(Hashtable item);
  3. int RemoveItem(Hashtable item);

它們都不具備計算總價的能力。所以需要聲明一個新功能:

double CalculateGrandTotal();

這個新功能應該讓 shoppingAPI 具備計算總價的能力。這是通過遍歷購物框中的商品並把它們的價格累加起來實現的。

修改第五個測試:

[Fact]
public void Add1ItemPrice10GrandTotal10() {
        var expectedGrandTotal = 10.00;
        Hashtable item = new Hashtable();
        item.Add("00000001", 10.00);
        shoppingAPI.AddItem(item);
        var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

這個測試表明了這樣的期望:如果向購物框里加入一件價格 ¥10 的商品,然後調用 CalculateGrandTotal() 方法,它會返回商品總價 ¥10。這是一個完全合理的期望,它完全符合商品總價計算的邏輯。

那麼怎麼實現這個功能呢?就像以前一樣,先寫一個假的實現。回到 ShippingAPI 類中,實現在介面中聲明的 CalculateGrandTotal() 方法:

public double CalculateGrandTotal() {
                return 0.00;
}

現在先將返回值硬編碼為 0.00,只是為了檢驗這個測試能否正常運行,並確認它是能夠失敗的。事實上,它能夠運行,並且如預期一樣失敗。接下來的工作就是正確實現計算商品總價的處理邏輯:

public double CalculateGrandTotal() {
        double grandTotal = 0.00;
        foreach(var product in basket) {
                Hashtable item = product as Hashtable;
                foreach(var value in item.Values) {
                        grandTotal += Double.Parse(value.ToString());
                }
        }
        return grandTotal;
}

運行,五個測試全部通過!

從單元素場景到多元素場景

現在是時候進入下一輪迭代了。你已經通過處理最簡場景、單元素場景和邊界場景迭代地構建了系統,現在需要處理稍複雜的多元素場景了。

快捷提示:由於我們一直在針對單個元素場景、多元素場景和邊界行為這三點上對軟體進行迭代改進,一些讀者可能會認為我們同樣應該對介面進行改進。我們稍後就會發現,介面已經完全滿足需要了,目前沒有新增功能的必要。請記住,應該保持介面的簡潔。(盲目地)擴增介面不會帶來任何好處,只會引入噪音。我們要遵循 奧卡姆剃刀 Occam's Razor 原則:如無必要,勿增實體。 現在我們已經基本完成了介面功能描述的工作,是時候改進實現了。

通過上一輪的迭代,系統已經能夠處理購物框里有超過一件商品的情況了。現在我么來讓系統具備購物框里有超過一件商品時計算總價的能力。首先寫可執行期望:

[Fact]
public void Add2ItemsGrandTotal30() {
        var expectedGrandTotal = 30.00;
        var actualGrandTotal = 0.00;
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

硬編碼所有值,盡量讓期望測試失敗。

測試確實失敗了,現在得想辦法讓它通過。向購物框添加兩件商品,然後調用 CalculateGrandTotal() 方法:

[Fact]
public void Add2ItemsGrandTotal30() {
          var expectedGrandTotal = 30.00;
        Hashtable item = new Hashtable();
        item.Add("00000001", 10.00);
        shoppingAPI.AddItem(item);
        Hashtable item2 = new Hashtable();
        item2.Add("00000002", 20.00);
        shoppingAPI.AddItem(item2);
        var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

測試通過。現在共有六個可以通過的微測試,系統回到了穩態。

設定期望

作為一個認真負責的工程師,你希望確保當用戶向購物框添加一些商品然後又移除一些商品後系統仍然能夠計算出正確出總價。下面是這個新的期望:

[Fact]
public void Add2ItemsRemoveFirstItemGrandTotal200() {
        var expectedGrandTotal = 200.00;
        var actualGrandTotal = 0.00;
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

這個期望表示將兩件商品加入到購物框,然後移除第一件後期望的總價是 ¥200。硬編碼行為失敗了。現在設計更具體的正面測試樣例,然後運行代碼:

[Fact]
public void Add2ItemsRemoveFirstItemGrandTotal200() {
        var expectedGrandTotal = 200.00;
        Hashtable item = new Hashtable();
        item.Add("00000001", 100.00);
        shoppingAPI.AddItem(item);
        Hashtable item2 = new Hashtable();
        item2.Add("00000002", 200.00);
        shoppingAPI.AddItem(item2);
        shoppingAPI.RemoveItem(item);
        var actualGrandTotal = shoppingAPI.CalculateGrandTotal();
        Assert.Equal(expectedGrandTotal, actualGrandTotal);
}

在這個正面測試樣例中,先向購物框加入第一件商品(編號為 00000001,價格為 ¥100),再加入第二件商品(編號為 00000002,價格為 ¥200)。然後將第一件商品移除,計算總價,比較計算值與期望值是否相等。

運行期望測試,系統正確地計算出了總價,滿足這個期望測試。現在有七個能順利通過的測試了。系統運行良好,無異常!

Test Run Successful.
Total tests: 7
     Passed: 7
 Total time: 0.9544 Seconds

敬請期待

現在你已經學習了 ZOMBIES 方法中的 ZOMBI 部分,下一篇文章將介紹處理特殊行為。到那個時候,你可以試試自己的測試!

(題圖:MJ/c4eb23b5-84aa-4477-a6b9-7d2a6d1aeee4)

via: https://opensource.com/article/21/2/boundaries-interfaces

作者:Alex Bunardzic 選題:lujun9972 譯者:toknow-gh 校對:wxy

本文由 LCTT 原創編譯,Linux中國 榮譽推出


本文轉載來自 Linux 中國: https://github.com/Linux-CN/archive

對這篇文章感覺如何?

太棒了
0
不錯
0
愛死了
0
不太好
0
感覺很糟
0
雨落清風。心向陽

    You may also like

    Leave a reply

    您的郵箱地址不會被公開。 必填項已用 * 標註

    此站點使用Akismet來減少垃圾評論。了解我們如何處理您的評論數據

    More in:Linux中國