天天看點

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

目錄

介紹

類寫得那麼糟糕......

1、命名

2、神奇數字      ​

3、不明顯的錯誤

4、不可讀

5、神奇數字——又一次

6、DRY——不要重複自己

7、每類多重責任

重構...

I STEP——命名,命名,命名

II STEP——神奇數字

III STEP——更具可讀性

IV STEP——不明顯的錯誤

V STEP——讓我們分析計算結果

VI STEP——擺脫神奇數字——另一種技巧

第七步——不要重複自己!

第八步——删除一些不必要的行......

STEP IX——進階——最後得到幹淨的代碼

結論

介紹

本文的目的是通過展示寫得不好的類的示例來展示如何編寫幹淨,可擴充和可維護的代碼。我将解釋它可能帶來的麻煩,并提出一種如何通過更好的解決方案取代它的方法——使用良好的實踐和設計模式。

第一部分是每個熟悉C#語言基礎知識的開發人員——它将展示一些基本的錯誤和技巧,如何讓代碼像書一樣可讀。進階部分适用于至少對設計模式有基本了解的開發人員——它将顯示完全幹淨,可單元測試的代碼。

要了解本文,您至少需要具備以下基本知識:

  • C#語言
  • 依賴注入,工廠方法和政策設計模式

本文中描述的示例是一個具體的,真實世界的功能——我不會顯示使用裝飾模式建構披薩或使用政策模式實作電腦的示例:)

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論
C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

由于這些理論示例非常适合解釋,我發現在實際的生産應用程式中使用它非常困難。

我們聽到很多次不要使用this ,并使用that 代替。但為什麼?我将嘗試解釋它并證明所有良好實踐和設計模式真正拯救了我們的生命!

注意:

  • 我不會解釋C#語言的特性和設計模式(它會使這篇文章太長),網絡中有很多很好的理論例子。我将集中精力展示如何在日常工作中使用它
  • 示例非常簡化,僅突出顯示所描述的問題——當我從包含代碼色調的示例中學習時,我發現難以了解文章的一般概念。
  • 我并不是說我所展示的解決方案對于下面描述的問題是唯一的解決方案,但肯定是可以使用您的代碼高品質的解決方案。
  • 我不在乎下面的代碼中關于錯誤處理,日志記錄等。編寫代碼隻是為了顯示常見程式設計問題的解決方案。 

我們具體的看看......

類寫得那麼糟糕......

我們的真實世界的例子如下所示:

public class Class1
{
  public decimal Calculate(decimal amount, int type, int years)
  {
    decimal result = 0;
    decimal disc = (years > 5) ? (decimal)5/100 : (decimal)years/100; 
    if (type == 1)
    {
      result = amount;
    }
    else if (type == 2)
    {
      result = (amount - (0.1m * amount)) - disc * (amount - (0.1m * amount));
    }
    else if (type == 3)
    {
      result = (0.7m * amount) - disc * (0.7m * amount);
    }
    else if (type == 4)
    {
      result = (amount - (0.5m * amount)) - disc * (amount - (0.5m * amount));
    }
    return result;
  }
}
           

這真是個壞人。我們能想象上述類的作用是什麼?它正在做一些有線計算?這就是我們現在所能說的一切......

現在想象一下,它是一個DiscountManager類,負責計算客戶在網上商店購買某些産品時的折扣。

——不是吧?真的嗎?

——不幸的是真的!

它是完全不可讀的,不可維護的,不可擴充的,它使用了許多不良做法和反模式。

我們在這裡有什麼确切的問題?

1、命名

我們隻能猜測這種方法的計算方法以及這些計算的輸入究竟是什麼。從這個類中提取計算算法真的很難。

風險:

在這種情況下最重要的是——浪費時間

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

,如果我們将從業務部門獲得詢問以呈現算法細節,或者我們需要修改這段代碼,我們需要花費很多時間來了解我們的Calculate方法的邏輯。如果我們不注釋它或重構代碼,下次我們/其他開發人員将花費同樣的時間來弄清楚那裡到底發生了什麼。我們也可以在修改它時輕易搞錯。

2、神奇數字
C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

在我們的示例中,類型變量表示——客戶帳戶的狀态。你能猜到嗎?If-else if語句可以選擇折扣後如何計算産品價格。

現在我們不知道什麼樣的帳戶是1,2,3或4。我們現在想象你必須改變為ValuableCustomer帳戶提供折扣的算法你可以嘗試從其餘的代碼中找出它——什麼會花費你很長時間,但即使我們很容易犯錯并修改BasicCustomer帳戶的算法——像2或3這樣的數字不是很具描述性。在我們的錯誤之後,客戶将非常高興,因為他們将獲得有價值客戶的折扣:)

3、不明顯的錯誤

因為我們的代碼非常髒且不可讀,我們很容易錯過非常重要的事情。想象一下,我們的系統中添加了一個新的客戶帳戶狀态——GoldenCustomer。現在,我們的方法将傳回0作為每種産品的最終價格,這些産品将從新類型的帳戶中購買。為什麼?因為如果我們的if-else if條件都不滿足(将有未處理的帳戶狀态)方法總是會傳回0.我們的老闆不高興——在有人意識到出了問題之前他賣了很多産品。

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

4、不可讀

我們都必須同意我們的代碼非常難以了解。

難以了解=更多時間了解代碼+增加錯誤風險。

5、神奇數字——又一次

我們知道像0.1,0.7,0.5那樣的數字是什麼意思?不,我們不知道,但如果我們是代碼的所有者,我們就應該知道。

讓我們假設您必須更改此行:

result =(amount - (0.5m * amount)) - disc *(amount - (0.5m * amount)); 

由于該方法完全不可讀,是以您隻需先将0.5更改為0.4,然後将第二個0.5保持原樣。它可能是一個錯誤,但它也可以是一個完全正确的修改。這是因為0.5并沒有告訴我們什麼。

将years 變量轉換為disc 變量時我們遇到的相同故事  :

decimal disc = (years > 5) ? (decimal)5/100 : (decimal)years/100;

在我們的系統的百分比計算中擁有帳戶的時間的折扣。好的,但5到底是什麼?這是客戶可以獲得忠誠度的最大折扣百分比。你能猜到嗎?

6、DRY——不要重複自己

第一次看不見它,但我們的方法中有許多地方的重複代碼。

例如:

disc * (amount - (0.1m * amount)); 

與以下邏輯相同:

disc * (amount - (0.5m * amount))

隻有靜态變量才有所不同——我們可以輕松地對這個變量進行參數化。

如果我們不會删除重複的代碼,我們将遇到隻執行任務的一部分的情況,因為我們不會看到我們必須以相同的方式進行更改,例如代碼中的5個地方(重複)。以上邏輯正在計算我們系統中作為客戶多年的折扣。是以,如果我們将在3個位置中的2個位置更改此邏輯,則我們的系統将變得不一緻。

7、每個類都是多重責任

我們的方法至少有3個職責:

1.選擇計算算法,

2.計算賬戶狀态的折扣,

3.計算作為客戶的年份的折扣。

它違反了單一責任原則。它帶來了什麼風險?如果我們需要修改這3個功能中的一個,它也會影響其他2個功能。這意味着它可能會破壞我們不想觸及的功能。是以我們必須再次測試所有類——浪費時間。 

重構...

在下面的9個步驟中,我将向您展示如何避免上述所有風險和不良做法,以實作幹淨、可維護和可單元測試的代碼,這些代碼可以像書一樣閱讀。 

I STEP——命名,命名,命名

這是一個好代碼最重要的方面之一。我們隻更改了方法、參數和變量的名稱,現在我們确切知道下面的類負責了什麼。

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, int accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100; 
    if (accountStatus == 1)
    {
      priceAfterDiscount = price;
    }
    else if (accountStatus == 2)
    {
      priceAfterDiscount = (price - (0.1m * price)) - (discountForLoyaltyInPercentage * (price - (0.1m * price)));
    }
    else if (accountStatus == 3)
    {
      priceAfterDiscount = (0.7m * price) - (discountForLoyaltyInPercentage * (0.7m * price));
    }
    else if (accountStatus == 4)
    {
      priceAfterDiscount = (price - (0.5m * price)) - (discountForLoyaltyInPercentage * (price - (0.5m * price)));
    }
 
    return priceAfterDiscount;
  }
}
           

但是我們仍然不知道1,2,3,4是什麼意思,讓我們用它做點什麼吧!

II STEP——神奇數字

在C#中避免神奇數字的技巧之一是用枚舉替換它。我準備了AccountStatus  枚舉來替換if-else if語句中的神奇數字:

public enum AccountStatus
{
  NotRegistered = 1,
  SimpleCustomer = 2,
  ValuableCustomer = 3,
  MostValuableCustomer = 4
}
           

現在看看我們的重構類,我們可以很容易地說出哪種算法計算折扣用于哪個帳戶狀态。混淆帳戶狀态的風險迅速下降。

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;
 
    if (accountStatus == AccountStatus.NotRegistered)
    {
      priceAfterDiscount = price;
    }
    else if (accountStatus == AccountStatus.SimpleCustomer)
    {
      priceAfterDiscount = (price - (0.1m * price)) - (discountForLoyaltyInPercentage * (price - (0.1m * price)));
    }
    else if (accountStatus == AccountStatus.ValuableCustomer)
    {
      priceAfterDiscount = (0.7m * price) - (discountForLoyaltyInPercentage * (0.7m * price));
    }
    else if (accountStatus == AccountStatus.MostValuableCustomer)
    {
      priceAfterDiscount = (price - (0.5m * price)) - (discountForLoyaltyInPercentage * (price - (0.5m * price)));
    }
    return priceAfterDiscount;
  }
}
           

III STEP——更具可讀性

在這一步中,我們将通過用switch-case語句替換if-else if語句來提高類的可讀性。

我還将長單行算法劃分為兩個單獨的行。現在把“帳戶狀态的折扣的計算”從“有顧客帳戶的年度的折扣的計算”分開了。

例如,行:

priceAfterDiscount = (price - (0.5m * price)) - (discountForLoyaltyInPercentage * (price - (0.5m * price)));

被替換為:

priceAfterDiscount = (price - (0.5m * price));

priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);

下面的代碼顯示了所描述的更改

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = (price - (0.1m * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = (0.7m * price);
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = (price - (0.5m * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
    }
    return priceAfterDiscount;
  }
}
           

IV STEP——不明顯的錯誤

我們終于得到了隐藏的bug!

正如我之前提到的,我們的方法ApplyDiscount将傳回0作為将從新類型帳戶購買的每個産品的最終價格。傷心但真實......

我們該如何解決?通過抛出NotImplementedException!

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

你會想——這不是驅動開發的異常嗎?不,不是!

當我們的方法将作為我們不支援的AccountStatus的參數值擷取時,我們希望立即注意到這個事實,并停止程式流程,不要在我們的系統中進行任何不可預測的操作。

這種情況絕不應該發生,是以如果發生這種情況,我們必須抛出異常。

如果沒有條件滿足,則下面的代碼被修改為抛出NotImplementedException——switch-case語句的預設部分:

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = (price - (0.1m * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = (0.7m * price);
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = (price - (0.5m * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      default:
        throw new NotImplementedException();
    }
    return priceAfterDiscount;
  }
}
           

V STEP——讓我們分析計算結果

在我們的示例中,我們有兩個為客戶提供折扣的标準:

  1. 帳戶狀态
  2. 在我們的系統中擁有帳戶的時間。

在作為客戶的時間折扣的情況下,所有算法看起來相似:

(discountForLoyaltyInPercentage * priceAfterDiscount),但在計算賬戶狀态的恒定折扣時有一個例外:0.7m * price

是以讓我們改變它看起來和其他情況一樣:

price - (0.3m * price)

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = (price - (0.1m * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = (price - (0.3m * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = (price - (0.5m * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      default:
        throw new NotImplementedException();
    }
    return priceAfterDiscount;
  }
}
           

現在我們有了所有的規則,這些規則都是以一種格式根據帳戶狀态計算折扣的:

price - ((static_discount_in_percentages/100) * price)

VI STEP——擺脫神奇數字——另一種技巧

讓我們看一下靜态變量,它是帳戶狀态折扣算法的一部分:(static_discount_in_percentages/100)

具體執行個體:

0.1m 

0.3m 

0.5m

這些數字也很神奇——他們沒有告訴我們任何關于他們自己的事情。

我們也有同樣的情況,如果将“擁有賬戶的時間”轉換為“忠誠的折扣”:

decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > 5) ? (decimal)5/100 : (decimal)timeOfHavingAccountInYears/100;

數字5讓我們的代碼變得非常神秘。

我們必須對它做些什麼來使它更具描述性!

我将使用另一種避免神奇字元串的技術——就是常量(C#中的const關鍵字)。我強烈建議為常量建立一個靜态類,以便将它放在我們應用程式的一個位置。

對于我們的示例,我建立了以下類:

public static class Constants
{
  public const int MAXIMUM_DISCOUNT_FOR_LOYALTY = 5;
  public const decimal DISCOUNT_FOR_SIMPLE_CUSTOMERS = 0.1m;
  public const decimal DISCOUNT_FOR_VALUABLE_CUSTOMERS = 0.3m;
  public const decimal DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS = 0.5m;
}
           

修改後,我們的DiscountManager類将如下所示:

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY) ? (decimal)Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY/100 : (decimal)timeOfHavingAccountInYears/100;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = (price - (Constants.DISCOUNT_FOR_SIMPLE_CUSTOMERS * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = (price - (Constants.DISCOUNT_FOR_VALUABLE_CUSTOMERS * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = (price - (Constants.DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS * price));
        priceAfterDiscount = priceAfterDiscount - (discountForLoyaltyInPercentage * priceAfterDiscount);
        break;
      default:
        throw new NotImplementedException();
    }
    return priceAfterDiscount;
  }
}
           

我希望你會同意我們的方法現在更加自解釋:)

第七步——不要重複自己!

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

為了不重複我們的代碼,我們将部分算法移到單獨的方法中。

我們将使用擴充方法來做到這一點。

首先,我們必須建立2個擴充方法:

public static class PriceExtensions
{
  public static decimal ApplyDiscountForAccountStatus(this decimal price, decimal discountSize)
  {
    return price - (discountSize * price);
  }
 
  public static decimal ApplyDiscountForTimeOfHavingAccount(this decimal price, int timeOfHavingAccountInYears)
  {
     decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY) ? (decimal)Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY/100 : (decimal)timeOfHavingAccountInYears/100;
    return price - (discountForLoyaltyInPercentage * price);
  }
}
           

由于我們的方法名稱非常具有描述性,是以我不必解釋它們的責任,現在讓我們在我們的示例中使用新代碼:

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus(Constants.DISCOUNT_FOR_SIMPLE_CUSTOMERS)
          .ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus(Constants.DISCOUNT_FOR_VALUABLE_CUSTOMERS)
          .ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus(Constants.DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS)
          .ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);
        break;
      default:
        throw new NotImplementedException();
    }
    return priceAfterDiscount;
  }
}
           

擴充方法非常好,可以使您的代碼更簡單,但在一天結束時仍然是靜态類,可以使您的單元測試非常困難甚至不可能。是以,我們将在最後一步中擺脫它。我用它隻是為了告訴你如何讓我們的生活更輕松,但我不是他們的忠實粉絲。

無論如何,您是否同意我們的代碼現在看起來好多了?

那麼讓我們跳到下一步吧!

第八步——删除一些不必要的行......

我們應該盡可能地編寫簡短的代碼。更短的代碼=更少的可能錯誤,更短的了解業務邏輯的時間。

讓我們簡化一下我們的例子吧。

我們可以很容易地注意到,我們對3種客戶帳戶有相同的方法調用:

.ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);

我們不能一次這樣做嗎?不,我們對未注冊使用者有例外,因為多年來作為注冊客戶的折扣對未注冊的客戶沒有任何意義。是的,但是帳戶有未注冊使用者的時間是什麼時候?

——0年

在這種情況下折扣将始終為0,是以我們可以安全地為未注冊使用者添加此折扣,讓我們這樣做!

public class DiscountManager
{
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        priceAfterDiscount = price;
        break;
      case AccountStatus.SimpleCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus(Constants.DISCOUNT_FOR_SIMPLE_CUSTOMERS);
        break;
      case AccountStatus.ValuableCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus(Constants.DISCOUNT_FOR_VALUABLE_CUSTOMERS);
        break;
      case AccountStatus.MostValuableCustomer:
        priceAfterDiscount = price.ApplyDiscountForAccountStatus(Constants.DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS);
        break;
      default:
        throw new NotImplementedException();
    }
    priceAfterDiscount = priceAfterDiscount.ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);
    return priceAfterDiscount;
  }
}
           

我們能夠在switch-case語句之外移動這一行。好處——更少的代碼!

STEP IX——進階——最後得到幹淨的代碼

行!現在我們可以像書一樣閱讀我們的類,但這對我們來說還不夠!我們現在想要超級幹淨的代碼!

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

好吧,讓我們做一些改變,最終實作這個目标。我們将使用依賴注入和政策和工廠方法設計模式! 

這就是我們的代碼在一天結束時的樣子:

public class DiscountManager
{
  private readonly IAccountDiscountCalculatorFactory _factory;
  private readonly ILoyaltyDiscountCalculator _loyaltyDiscountCalculator;
 
  public DiscountManager(IAccountDiscountCalculatorFactory factory, ILoyaltyDiscountCalculator loyaltyDiscountCalculator)
  {
    _factory = factory;
    _loyaltyDiscountCalculator = loyaltyDiscountCalculator;
  }
 
  public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
  {
    decimal priceAfterDiscount = 0;
    priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);
    priceAfterDiscount = _loyaltyDiscountCalculator.ApplyDiscount(priceAfterDiscount, timeOfHavingAccountInYears);
    return priceAfterDiscount;
  }
}
           
public interface ILoyaltyDiscountCalculator
{
  decimal ApplyDiscount(decimal price, int timeOfHavingAccountInYears);
}
 
public class DefaultLoyaltyDiscountCalculator : ILoyaltyDiscountCalculator
{
  public decimal ApplyDiscount(decimal price, int timeOfHavingAccountInYears)
  {
    decimal discountForLoyaltyInPercentage = (timeOfHavingAccountInYears > Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY) ? (decimal)Constants.MAXIMUM_DISCOUNT_FOR_LOYALTY/100 : (decimal)timeOfHavingAccountInYears/100;
    return price - (discountForLoyaltyInPercentage * price);
  }
}
           
public interface IAccountDiscountCalculatorFactory
{
  IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus);
}
 
public class DefaultAccountDiscountCalculatorFactory : IAccountDiscountCalculatorFactory
{
  public IAccountDiscountCalculator GetAccountDiscountCalculator(AccountStatus accountStatus)
  {
    IAccountDiscountCalculator calculator;
    switch (accountStatus)
    {
      case AccountStatus.NotRegistered:
        calculator = new NotRegisteredDiscountCalculator();
        break;
      case AccountStatus.SimpleCustomer:
        calculator = new SimpleCustomerDiscountCalculator();
        break;
      case AccountStatus.ValuableCustomer:
        calculator = new ValuableCustomerDiscountCalculator();
        break;
      case AccountStatus.MostValuableCustomer:
        calculator = new MostValuableCustomerDiscountCalculator();
        break;
      default:
        throw new NotImplementedException();
    }
 
    return calculator;
  }
}
           
public interface IAccountDiscountCalculator
{
  decimal ApplyDiscount(decimal price);
}
 
public class NotRegisteredDiscountCalculator : IAccountDiscountCalculator
{
  public decimal ApplyDiscount(decimal price)
  {
    return price;
  }
}
 
public class SimpleCustomerDiscountCalculator : IAccountDiscountCalculator
{
  public decimal ApplyDiscount(decimal price)
  {
    return price - (Constants.DISCOUNT_FOR_SIMPLE_CUSTOMERS * price);
  }
}
 
public class ValuableCustomerDiscountCalculator : IAccountDiscountCalculator
{
  public decimal ApplyDiscount(decimal price)
  {
    return price - (Constants.DISCOUNT_FOR_VALUABLE_CUSTOMERS * price);
  }
}
 
public class MostValuableCustomerDiscountCalculator : IAccountDiscountCalculator
{
  public decimal ApplyDiscount(decimal price)
  {
    return price - (Constants.DISCOUNT_FOR_MOST_VALUABLE_CUSTOMERS * price);
  }
}
           

 首先我們擺脫了擴充方法(read:靜态類),因為使用它們使得調用者類(DiscountManager)與擴充方法中的折扣算法緊密結合。如果我們想要對我們的ApplyDiscount方法進行單元測試,那将是不可能的,因為我們也會測試PriceExtensions類。 

為了避免它,我建立了DefaultLoyaltyDiscountCalculator類,其中包含ApplyDiscountForTimeOfHavingAccount擴充方法的邏輯,并隐藏它在抽象(read:接口)ILoyaltyDiscountCalculator背後的實作。現在,當我們想要測試我們的DiscountManager類時,我們将能夠通過構造函數将實作ILoyaltyDiscountCalculator的 mock / fake對象注入到我們的DiscountManager類中,僅僅用于測試DiscountManager實作。我們在這裡使用依賴注入設計模式。

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

通過這樣做,我們還将計算忠誠折扣的責任轉移到了另一個類,是以如果我們需要修改這個邏輯,我們将隻需要更改DefaultLoyaltyDiscountCalculator類,所有其他代碼将保持不變——降低破壞風險的風險,更少的測試時間。

下面在我們的DiscountManager類中使用劃分為單獨的類邏輯:

priceAfterDiscount = _loyaltyDiscountCalculator.ApplyDiscount(priceAfterDiscount, timeOfHavingAccountInYears);

為了計算帳戶狀态邏輯的折扣,我必須建立更複雜的内容。我們有兩個想從DiscountManager移出的責任:

  1. 根據帳戶狀态使用哪種算法
  2. 特定算法計算的細節

為了移出第一個責任,我建立了一個工廠類(DefaultAccountDiscountCalculatorFactory),它是工廠方法設計模式的一個實作,并将其隐藏在抽象——IAccountDiscountCalculatorFactory之後。

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

我們的工廠将決定選擇哪種折扣算法。最後,我們使用依賴注入設計模式通過構造函數将工廠注入到DiscountManager類中。

下面在我們的DiscountManager類中使用工廠:

priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);

上面的行将傳回特定帳戶狀态的正确政策,并将在其上調用ApplyDiscount方法。

第一個責任分開,讓我們談談第二個。

我們來談談政策.....

C#壞習慣:通過不好的例子學習如何制作好的代碼——第1部分介紹類寫得那麼糟糕......重構...結論

由于每個帳戶狀态的折扣算法可能不同,我們将不得不使用不同的政策來實作它。這是使用戰略設計模式的絕佳機會!

在我們的示例中,我們現在有3個政策:

NotRegisteredDiscountCalculator 

SimpleCustomerDiscountCalculator 

MostValuableCustomerDiscountCalculator

它們包含特定折扣算法的實作,并隐藏在抽象背後:

IAccountDiscountCalculator。

它将允許我們的DiscountManager類在不知道其實作的情況下使用适當的政策。DiscountManager隻知道傳回的對象實作了包含方法ApplyDiscount的IAccountDiscountCalculator接口。

NotRegisteredDiscountCalculator,SimpleCustomerDiscountCalculator,MostValuableCustomerDiscountCalculator類根據帳戶狀态包含正确算法的實作。由于我們的3個政策看起來相似,我們唯一可以做的就是為所有3個算法建立一個方法,并使用不同的參數從每個政策類調用它。因為它會使我們的示例變得更大,是以我沒有決定那樣做。 

好吧,總結一下,現在我們有一個幹淨可讀的代碼和我們所有的類都隻有一個責任——隻有一個理由去改變:

1。DiscountManager——管理代碼流

2. DefaultLoyaltyDiscountCalculator ——忠誠折扣的計算

3. DefaultAccountDiscountCalculatorFactory ——決定計算帳戶狀态折扣的政策選擇

4. NotRegisteredDiscountCalculator, SimpleCustomerDiscountCalculator,MostValuableCustomerDiscountCalculator——計算帳戶狀态的折扣 

現在比較開始的方法:

public class Class1
{
    public decimal Calculate(decimal amount, int type, int years)
    {
        decimal result = 0;
        decimal disc = (years > 5) ? (decimal)5 / 100 : (decimal)years / 100;
        if (type == 1)
        {
            result = amount;
        }
        else if (type == 2)
        {
            result = (amount - (0.1m * amount)) - disc * (amount - (0.1m * amount));
        }
        else if (type == 3)
        {
            result = (0.7m * amount) - disc * (0.7m * amount);
        }
        else if (type == 4)
        {
            result = (amount - (0.5m * amount)) - disc * (amount - (0.5m * amount));
        }
        return result;
    }
}
           

到我們新的重構代碼:

public decimal ApplyDiscount(decimal price, AccountStatus accountStatus, int timeOfHavingAccountInYears)
{
  decimal priceAfterDiscount = 0;
  priceAfterDiscount = _factory.GetAccountDiscountCalculator(accountStatus).ApplyDiscount(price);
  priceAfterDiscount = _loyaltyDiscountCalculator.ApplyDiscount(priceAfterDiscount, timeOfHavingAccountInYears);
  return priceAfterDiscount;
}
           

結論

本文提供的代碼非常簡化,可以更輕松地解釋使用過的技術和模式。它顯示了如何以肮髒的方式解決常見的程式設計問題,以及使用良好實踐和設計模式以适當、幹淨的方式解決它的好處。

在我的工作經曆中,我在這篇文章中看到過很多次被強調的壞習慣。它們顯然存在于許多應用程式中,而不是像我的例子那樣存在于一個類中,這使得查找它更加困難,因為它們隐藏在正确的代碼之間。編寫這種代碼的人總是認為他們遵循保持簡單和直接的規則。不幸的是,幾乎總是系統正在成長并變得非常複雜。然後,這個簡單的、不可擴充的代碼中的每一個修改都是非常重要的,并且帶來了巨大的風險。

請記住,您的代碼将在生産環境中存在很長時間,并且會在每次業務需求更改時進行修改。是以,編寫過于簡單,無法使用的代碼将很快産生嚴重後果。最後對開發人員很好,他們會自己維護你的代碼:)

下一篇:C#壞習慣:通過不好的例子學習如何制作好的代碼——第2部分 

原文位址:https://www.codeproject.com/Articles/1083348/Csharp-BAD-PRACTICES-Learn-how-to-make-a-good-code

繼續閱讀