C#:如何將壞的代碼重新編譯為好的代碼

尋夢新聞LINE@每日推播熱門推薦文章,趣聞不漏接❤️

加入LINE好友

自己的前言說明:

本文原作者:Radoslaw Sadowski,原文鏈接為:C# BAD PRACTICES: Learn how to make a good code by bad example。

本系列還有其他文章,後續將慢慢翻譯。

引言:

我的名字叫Radoslaw Sadowski,我現在是一個微軟技術開發人員。我從開始工作時就一直接觸的微軟技術.

在工作一年後,我看到的質量很差的代碼的數量基本上都可以寫成一整本書了。

這些經歷讓我變成了一個想要清潔代碼的強迫症患者。

寫這篇文章的目的是為了通過展示質量很差的類的例子來說明如何書寫出乾淨的、可延伸的和可維護的代碼。我會通過好的書寫方式和設計模式來解釋壞的代碼帶來的問題,以及替換他的好的解決方法。

第一部分是針對那些擁有C#基礎知識的開發人員——我會展示一些常見的錯誤,然後再展示一些讓代碼變得可讀性的方法與技巧。高級部分主要針對那些至少擁有設計模式概念的開發人員——我將會展示完全乾淨的、單元可測試的代碼。

為了能夠理解這篇文章你需要至少掌握以下兩個部分的基本知識:

  • C#語言
  • 依賴注入、工廠設計模式、策略設計模式

本文中所涉及的例子都將會是現實中實實在在的具體的特性,而不是用裝飾模式來做披薩或者用策略模式來做計算器這樣的示例。

(ps解釋:看過設計模式相關的書籍的人應該會知道很多這方面的書籍都是用這種例子,只是為了幫助讀者理解設計模式)

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

因為我發現這種類型的產品不好用來解釋,相反這些理論性的例子卻是非常適合用來在本文中做解釋的。

我們常常會聽到說不要用這個,要用那個,但是卻不知道這種替換的理由。今天我將會努力解釋和證明那些好的書寫習慣以及設計模式是真的是在拯救我們的開發生活!

提示:

  • 在本文中我不會花時間來講解C#的特性和涉及模式之類(我也解釋不完),網上有很多關於這方面的好的理論的例子。我將集中講述如何在我們日常工作中使用這些東西。
  • 例子是一種比較容易的突出我們要說明的問題的方法,但是僅限於描述的問題——因為我發現當我在學習哪些包含著主要代碼的例子時,我發現在理解文章的總體思想方面會有困難。
  • 我不是說我文中說的方法是惟一的解決方式,我只是能保證這些方法將會是讓你的代碼變得更高質量的途徑。
  • 我並不關心下面這些代碼的什麼錯誤處理,日志記錄等等。我要表述的只是用來解決日常編碼一些問題的方法。

那就開始吧….

那些糟糕透了的類…

下面的例子是我們現實中的類:

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

上面這個例子真的是一種非常差的書寫方式。你能知道這個類是用來幹嘛的麼?這個東西是用來做一些奇怪的運算的麼?我們文章就從他開始入手來講解吧

現在我來告訴你,剛剛那個類是用來當顧客在網上買東西的時候為他們計算對應折扣的折扣計算和管理的類。

-難以置信吧!

-可是這是真的!

這種寫法真的是將難以閱讀、難以維護和難以擴展這三種集合在一起了,而且擁有著太差的書寫習慣和錯誤的模式。

除此之外還有其他什麼問題麼?

1.命名方式-從源代碼中我們可以連蒙帶猜可能出來這個計算方法和輸出結果是什麼。而且我們想要從這個類中提取計算算法將會是一件非常困難的事情。

這樣帶來的危害是:

最嚴重的問題是:浪費時間,

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

如果我們需要滿足客戶的商業咨詢,要像他們展示算法細節,或者我們需要修改這段代碼,這將花費我們很長的時間去理解我們的計算方法的邏輯。即使我們不記錄他或重構代碼,下次我們/其他開發人員再看這段代碼的時候,還是需要花費同等的時間來研究這些代碼是幹嘛的。而且在修改的同時還容易出錯,導致原來的計算全部出錯。

2.魔法數字

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

在這個例子中type是變量,你能猜到它代表著客戶帳戶的等級麼?If-else if語句是用來做到如何選擇計算出產品價格折扣的方法。

現在我們不知道什麼樣的帳戶是1,2,3或4。現在想像一下,當你不得不為了那些有價值的VIP客戶改變他們的折扣計算方式的時候,你試著從那些代碼中找出修改的方法—這個過程可能會花費你很長的時間不說,還很有可能犯錯以至於修改那些基礎的一般的客戶的帳戶,畢竟像2或者3這些詞語毫無描述性的。但是在我們犯錯以後,那些一般的客戶卻很高興,因為他們得到了VIP客戶的折扣。:)

3.沒有明顯的bug

因為我們的代碼質量很差,而且可讀性非常差,所以我們可能輕易就忽略掉很多非常重要的事情。想像一下,現在突然在系統中增加一種新的客戶類型-金卡用戶,而在我們的系統中任何一種新的帳戶類型最後獲得的價格將是0元。為什麼呢?因為在我們的if-else if語句中沒有任何狀態是滿足新的狀態的,所以只要是未處理過的帳戶類型,最後返回值都將變成0。一旦我們的老板發現這件事,他將會大發雷霆-畢竟他已經免費賣給這樣用戶很多很多東西了!

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

4.沒有可讀性

我們必須承認上面這段代碼的可讀性是真的糟糕。

她讓我們花費了太多的時間去理解這段代碼,同時代碼隱藏錯誤的幾率太大了,而這就是沒有可讀性的最重要的定義。

5.魔法數字(再次)

你從代碼中能知道類似0.1,0.7,0.5這些數字的意思麼?好的,我承認我不知道。只有我們自己編寫這些代碼我們才知道這是什麼意思,別人是無法理解的。

你試試想想如果讓你修改下面這句代碼,你會怎麼樣:

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

因為這個方法完全不可讀,所以你修改的過程中只能嘗試著把第一個0.5改成0.4而保持第二個0.5不懂。這可能會是一個bug,但是卻是最好的最合適的修改方式。因為這個0.5什麼都沒有告訴我們。

同樣的事也存在將years變量轉換到disc變量的轉換過程中

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

這是用來計算折扣率的,會通過帳戶在我們系統的時間的百分比來獲取。好的,那麼現在問題來了,如果時間剛剛好就是5呢?

6.簡潔-不要反復做無用功

雖然第一眼看的時候不容易看出來,但是仔細研究一下就會發現:我們的代碼里有很多重復的地方。例如:disc* (amount – (0.1m * amount));

而與之有同樣效果的還有(只是變了一個參數而已):disc* (amount – (0.5m * amount))

在這兩個算術中,唯一的區別就只是一個靜態參數,而我們完全可以用一個可變的參數來替代。

如果我們不試著在寫代碼的時候從一直ctri+c,ctrl+v中擺脫出來,那我們將遇到的問題就是我們只能修改代碼中的部分功能,因為我們不知道有多少地方需要修改。上面的邏輯是計算出在我們系統中每個客戶對應年限獲得的折扣,所以如果我們只是貿然修改兩到三處,很容易造成其他地方的前後不一致。

7.每個類有著太多的複雜的責任區域

我們寫的類至少背負了三個責任:

  1. 選擇計算的運算法則
  2. 為每個不同狀態的帳戶計算折扣率
  3. 根據每個客人的年限計算出對應的折扣率

這個違背了單一責任原則。那麼這會帶來什麼危害呢?如果我們想要改變上訴3個特性中的兩個,那就意味著可能會碰觸到一些其他的我們並不想修改的特性。所以在修改的時候我們不得不重新測試所有的類,那麼這就造成了很重的時間的浪費。

那就開始重構吧…

在接下來的9個步驟中我將向你展示我們如何避免上訴問題來構建一個乾淨的易維護,同時又方便單元測試的看起來一目了然的代碼。

I:命名,命名,命名

恕我直言,這是代碼中最重要的一步。我們只是修改方法/參數/變量這些的名字,而現在我們可以直觀的了解到下面這個類代表什麼意思。

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

雖然如此,我們還是不理解1,2,3,4代表著什麼,那就繼續往下吧!

II:魔法數

C#中避免出現不理解的魔法數的方法是通過枚舉來替代。我通過枚舉方法來替代在if-else if語句中出現的代替帳戶狀態的魔法數。

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

現在在看我們重構了的類,我們可以很容易的說出那個計算法則是用來根據不用狀態來計算折扣率的。將帳戶狀態弄混的幾率就大幅度減少了。

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

III:更多的可讀性

在這一步中我們將通過將if-else if語句改為switch-case語句,來增加文章的可讀性。

同時,我也將一個很長的計算方法拆分為兩句話來寫。現在我們將「 通過帳戶狀態來計算折扣率」與「通過帳戶年限來計算折扣率」這兩者分開來計算。

例如:priceAfterDiscount = (price – (0.5m * price)) – (discountForLoyaltyInPercentage * (price – (0.5m * price)));

我們將它重構為:priceAfterDiscount = (price – (0.5m * price));

priceAfterDiscount = priceAfterDiscount – (discountForLoyaltyInPercentage * priceAfterDiscount);

這就是修改後的代碼:

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

IV:沒有明顯的 bug

我們終於找到我們隱藏的bug了!

因為我剛剛提到的我們的方法中對於不適合的帳戶狀態會在造成對於所有商品最後都返回0。雖然很不幸,但卻是真的。

那我們該如何修復這個問題呢?那就只有通過沒有錯誤提示了。

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

你是不是會想,這個會不會是開發的例外,應該不會被提交到錯誤提示中去?不,他會的!

當我們的方法通過獲取帳戶狀態作為參數的時候,我們並不想程序讓我們不可預知的方向發展,造成不可預計的失誤。

這種情況是絕對不允許出現的,所以我們必須通過拋出異常來防止這種情況。

下面的代碼就是通過拋出異常後修改的以防止出現不滿足條件的情況-修改方式是將拋出異常防止 switch-case語句中的default句中。

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

V:分析計算方法

在我們的例子中我們有兩個定義給客戶的折扣率的標準:

  1. 帳戶狀態;
  2. 帳戶在我們系統中存在的年限

對於年限的計算折扣率的方法,所有的計算方法都有點類似:

(discountForLoyaltyInPercentage * priceAfterDiscount)

當然,也還是存在例外的:0.7m * price

所以我們把這個改成這樣:price – (0.3m * price)

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

現在我們將整理所有通過帳戶狀態的計算方法改為同一種格式:price – ((static_discount_in_percentages/100) * price)

VI:通過其他方式再擺脫魔法數

接下來讓我們的目光放在通過帳戶狀態計算折扣率的計算方法中的靜態變量:(static_discount_in_percentages/100)

然後帶入下面數字距離試試:0.1m,0.3m,0.5m

這些數字其實也是一種類型的魔法數-他們也沒有直接告訴我們他們代表著什麼。

我們也有同樣的情況,比如將「有帳戶的時間」折價為「忠誠折扣」。

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

數字5讓我們的代碼變得神秘了起來。

我們必須做些什麼讓這個變得更具表現性。

我會用另外一種方法來避免魔法數的表述的出現-也就是C#中的常量(關鍵詞是const),我強烈建議在我們的應用程序中專門定義一個靜態類來存儲這些常量。

在我們的例子中,我是創建了下面的類:

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

經過一定的修改,我們的DiscountManager類就變成了這樣了:

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

我希望你也認同我這個方法會更加使代碼自身變得更具有說明性:)

VII:不要再重復啦!

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

我們可以通過分拆算法的方式來移動我們的計算方法,而不是僅僅簡單的復制代碼。

我們會通過擴展方法。

首先我們會創建兩個擴展方法。

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

正如方法的名字一般,我不再需要單獨解釋一次他們的功能是什麼。現在就開始在我們的例子中使用這些代碼吧:

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

擴展方法讓代碼看起來更加友善了,但是這個代碼還是靜態的類,所以會讓你單元測試的時候遇到困難,甚至不可能。那麼出於擺脫這個問題的打算我們在最後一步來解決這個問題。我將展示這些是如何簡化我們的工作生活的。但是對於我個人而言,我喜歡,但是並不算是熱衷粉。

不管怎樣,你現在同意我們的代碼看起來友善多了這一點麼?

那我們就繼續下去吧!

VIII:移除那些多餘的代碼

在寫代碼的時候原則上是我們的代碼越是精簡越好。精簡的代碼的意味著,越少的錯誤的可能性,在閱讀理解代碼邏輯的時候花費的時間越少。

所以現在開始精簡我們的代碼吧。

我們可以輕易發現我們三種客戶帳戶下有著相同的方法:

.ApplyDiscountForTimeOfHavingAccount(timeOfHavingAccountInYears);

我們可不可以只寫一次呢?我們之前將未註冊的用戶放在了拋出異常中,因為我們的折扣率只會計算註冊用戶的年限,並沒有給未註冊用戶留有功能設定。所以,我們應該給未註冊用戶設定的時間為多少呢? -0年

那麼對應的折扣率也將變成0了,這樣我們就可以安全的將折扣率交付給未註冊用戶使用了,那就開始吧!

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

我們還可以將這一行移除到switch-case語句外面。好處就是:更少的代碼量!

IX:提高-最後的得到乾淨整潔的代碼

好了,現在我們可以像閱讀一本書一樣方便來審視我們的代碼了,但是這就夠了麼?我們可以將代碼變得超級精簡的!

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

好的,那就開始做一些改變來做到這個目標吧。我們可以使用依賴注入和使用策略模式這兩種方式。

這就是我們今天最後整理出來的代碼了:

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

首先我們擺脫了擴展方法(也就是靜態類),之所以要擺脫這種是因為擴展方法與折扣計算方法之間存在了緊耦合的關係。如果我們想要單元測試我們的方法ApplyDiscount的時候將變得不太容易,因為我們必須統一測試與之緊密關聯的類PriceExtensions

為了避免這個,我創建了DefaultLoyaltyDiscountCalculator類,這里麵包含了ApplyDiscountForTimeOfHavingAccount擴展方法,同事我通過抽象接口ILoyaltyDiscountCalculator隱藏了她的具體做到。現在,當我想測試我們的類DiscountManager的時候,我就可以通過 ILoyaltyDiscountCalculator模擬注入虛構對象到DiscountManager類中通過構造函數顯示測試功能。這里我們運用的就叫依賴注入模式。

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

在做這個的同時,我們也將計算折扣率這個功能安全的移交到另一個不同的類中,如果我們想要修改這一段的邏輯,那我們就只需要修改DefaultLoyaltyDiscountCalculator類就好了,而不需要改動其他的地方,這樣減少了在改動他的時候產生破壞其他地方的風險,同時也不需要再增加單獨測試的時間了。

下面是我們在DiscountManager類中使用分開的邏輯類:

priceAfterDiscount = _loyaltyDiscountCalculator.ApplyDiscount(priceAfterDiscount, timeOfHavingAccountInYears);

為了針對帳戶狀態的邏輯來計算折扣率,我創建了一些比較複雜的東西。我們在DiscountManager類中有兩個責任需要分解出去。

  1. 根據帳戶狀態如何選擇對應的計算方法。
  2. 特殊計算方法的細節

為了將第一個責任移交出去,我創建了工廠類(DefaultAccountDiscountCalculatorFactory),為了做到工廠模式,然後再把這個隱藏到抽象IAccountDiscountCalculatorFactory里面去。

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

我們的工廠會決定選擇哪種計算方法。最後我們通過依賴註冊模式構造函數將工廠模式注射到DiscountManager類中

下面就是運用了工廠的DiscountManager類:

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

以上會針對不同的帳戶狀態返回何時的策略,然後調用ApplyDiscount方法。

第一個責任已經被交接出去了,接下來就是第二個了。

接下來我們就開始討論策略了…..

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

因為不同的帳戶狀態會有不用的折扣計算方法,所以我們需要不同的做到策略。座椅非常適用於策略模式。

在我們的例子中,我們有三種策略:

NotRegisteredDiscountCalculatorSimpleCustomerDiscountCalculator MostValuableCustomerDiscountCalculator

他們包含了具體的折扣計算方法的做到並被藏在了抽象IAccountDiscountCalculator里。

這就允許我們的類DiscountManager使用合適的策略,而不需要知道具體的做到。我們的類只需要知道與ApplyDiscount方法相關的IAccountDiscountCalculator接口返回的對象的類型。

NotRegisteredDiscountCalculator, SimpleCustomerDiscountCalculator, MostValuableCustomerDiscountCalculator這些類包含了具體的通過帳戶狀態選擇適合計算的計算方法的做到。因為我們的這三個策略看起來相似,我們唯一能做的基本上就只有針對這三種計算策略創建一個方法然後每個策略類通過一個不用的參數來調用她。因為這會讓我們的代碼變得越來越多,所以我現在決定不這麼做了。

好了,到目前為止我們的代碼變得可讀了,而且每個類都只有一個責任了-這樣修改他的時候會單獨一一對應了:

  1. DiscountManager-管理代碼流
  2. DefaultLoyaltyDiscountCalculator-可靠的計算折扣率的方法
  3. DefaultAccountDiscountCalculatorFactory-決定根據帳戶狀態選擇哪個策略來計算。
  4. NotRegisteredDiscountCalculator, SimpleCustomerDiscountCalculator, MostValuableCustomerDiscountCalculator– 根據帳戶狀態計算折扣率

現在開始比較現在與之前的方法:

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

這是我們的新的重構的代碼:

C#:如何將壞的代碼重新編譯為好的代碼-雪花新聞

總結

在本文中,代碼被極其簡化了,使得所有的技術和模式的解釋更容易了。它展示了如何解決常見的編程問題,以及使用良好的實踐和設計模式以適當、乾淨的方式解決這些問題的好處。

在我的工作經歷中,我多次在這篇文章中強調了不良的做法。它們顯然存在於許多應用場合,而不是在一個類中,如在我的例子中那樣,這使得發現它們更加困難,因為它們隱藏在適當的代碼之間。寫這種代碼的人總是爭辯說,他們遵循的是簡單愚蠢的規則。不幸的是,幾乎所有的系統都在成長,變得非常複雜。然後,這個簡單的、不可擴展的代碼中的每一個修改都是非常重要的,並且帶來了巨大的風險。

請記住,您的代碼將長期存在於生產環境中,並將在每個業務需求更改上進行修改。因此編寫過於簡單、不可擴展的代碼很快就會產生嚴重的後果。最後一點是對開發人員有利,尤其是那些在你自己之後維護你的代碼。

如果你有一些問題根據文章不要猶豫聯繫我!

原文地址https://www.cnblogs.com/Aries-rong/p/9289725.html