掌握 DRY 原則:在 C# 中應用設計模式以撰寫更乾淨的程式碼
[[academy-video-youtube({"vid": "dhnsegiPXoo", "start_time": "0", "title": "掌握DRY原則:在C#中應用設計模式以獲得更清晰的程式碼", "creator": "Tim Corey", "length": "53m 20s"})]]
C#的設計模式是撰寫高效、可重用及可維護程式碼的基本工具。 這些模式為常見的軟體設計問題提供標準解決方案,推廣最佳實踐,幫助開發者避免重複程式碼。 應用設計模式的一個核心原則是DRY(Don't Repeat Yourself)原則,強調通過最小化程式碼重複來提升可讀性和維護性。
本文受到Tim Corey啟發性影片的啟發:"設計模式:在C#中避免重複自己",深入探討了DRY原則及其在創造更整潔有序程式碼中的實際應用。 透過探索Tim影片中討論的關鍵概念和策略,本文旨在為您提供一份全面指南,以有效地在C#專案中實施DRY設計模式原則。
Introduction to the DRY Principle in C
在介紹中,Tim Corey解釋了DRY原則,這代表"不重複自己"。這一原則是編程中的基本概念,強調通過確保每個知識或邏輯片段在程式碼中僅表示一次來避免冗餘。 Tim用一個簡單的WinForms應用程式範例來說明這原則。 該表單包括用於輸入名字和姓氏的字段,和一個基於這些字段生成員工ID的按鈕。
識別和預計程式碼重複
在(0:53),Tim繼續識別和預計程式碼中的重複。 他用WinForms應用程式的例子展示了即使方法只被調用一次也可能出現重複的情況。 在應用程式中,生成員工ID的邏輯涉及從名字和姓氏的文字字段中提取子字串,並在末尾附加一個三位數程式碼。

在(1:31)的截圖中,Tim演示了應用程式的功能,展示如何通過結合名字和姓氏的前四個字母和三位數程式碼生成員工ID。 他指出,雖然程式碼看似遵循DRY原則,因為它沒有明確地重複相同的邏輯,但在重複模式中存在需要解決的潛在問題。
在(1:51),他指出,儘管程式碼看似簡單,但它並未完全遵循DRY原則,因為生成員工ID的邏輯與按鈕的單擊事件緊密耦合。 這意味著如果在客戶端程式碼中的其他地方需要此邏輯,例如在處理新員工列表時(3:58),需要重複或適應程式碼,從而導致冗餘。
建立獨立可重用的方法
在這一部分中,Tim Corey展示了如何建立獨立可重用的方法,以遵循DRY原則。 他首先將生成員工ID的邏輯從事件處理程式中提取到一個單獨的方法中。 此重構涉及建立一個名為GenerateEmployeeID的私有方法,並將現有程式碼移動到此方法中 (5:15)。 隨後,事件處理程式中的修訂程式碼簡單地調用此方法。
步驟和範例:
初始程式碼:生成員工ID的邏輯直接在按鈕的單擊事件處理程式中。

重構程式碼:Tim通過使其更靈活改進了該方法。 此方法現在不再依賴特定的UI元素,而是接受
lastName作為參數並返回生成的ID。 此更改允許方法在各種上下文和UI元素中使用:private string GenerateEmployeeID(string firstName, string lastName) { string employeeID = firstName.Substring(0, 4) + lastName.Substring(0, 4) + DateTime.Now.Millisecond.ToString(); return employeeID; }private string GenerateEmployeeID(string firstName, string lastName) { string employeeID = firstName.Substring(0, 4) + lastName.Substring(0, 4) + DateTime.Now.Millisecond.ToString(); return employeeID; }隨後,Tim演示了如何從單擊事件中調用此方法:
employeeIdText.Text = GenerateEmployeeID(firstNameText.Text, lastNameText.Text);employeeIdText.Text = GenerateEmployeeID(firstNameText.Text, lastNameText.Text);他還指出,此方法現在可在應用程式的其他部分使用,例如在處理具有多個員工記錄的CSV文件時,而不用重複程式碼。
構建和使用類庫
隨後,Tim Corey探討了類庫的概念,以進一步提升程式碼的可重用性和可維護性。 他說明了如何將GenerateEmployeeID方法封裝到一個類庫物件中,這可以在多個專案中使用。
在(8:00),Tim解釋說設計會根據使用者要求或公司政策不斷變化,使其更具互動性,具有圖形和動畫。 因此,他在解決方案中引入一個具有精確字段和生成員工ID按鈕的WPF專案。
Tim在(9:15)中強調使用類庫的重要性,說如果我們要避免重複自己,程式碼就必須在新的WPF專案中進行複製粘貼。 因此,要保持DRY,我們需要在類庫中建立類。
步驟和範例:
建立類庫:
Tim在(9:47)建立了一個新的.NET Framework類庫專案,命名為DRYDemoLibrary。
在這個庫中,他定義了一個公共類
GenerateEmployeeID方法移動到此類中:public class EmployeeProcessor { public string GenerateEmployeeID(string firstName, string lastName) { string employeeID = firstName.Substring(0, 4) + lastName.Substring(0, 4) + DateTime.Now.Millisecond.ToString(); return employeeID; } }public class EmployeeProcessor { public string GenerateEmployeeID(string firstName, string lastName) { string employeeID = firstName.Substring(0, 4) + lastName.Substring(0, 4) + DateTime.Now.Millisecond.ToString(); return employeeID; } }
在專案中使用類庫:
在他的WinForms (13:18)和WPF專案(14:00)中,Tim新增了一個對DRYDemoLibrary類庫的引用。
然後,他用來自類庫的
GenerateEmployeeID方法的調用替換了舊程式碼:EmployeeProcessor processor = new EmployeeProcessor(); employeeIDText.Text = processor.GenerateEmployeeID(firstNameText.Text, lastNameText.Text);EmployeeProcessor processor = new EmployeeProcessor(); employeeIDText.Text = processor.GenerateEmployeeID(firstNameText.Text, lastNameText.Text);- 這種方法消除冗餘,因為方法現在被維護在一個地方。 Tim演示了同一類庫可以在不同的UI框架(WinForms和WPF)中使用而不需重複程式碼。
優點:
一致性:通過在類庫中集中邏輯,Tim確保對邏輯的更改(如錯誤修復)將被一致地應用於所有專案。
- 減少維護:方法中的更改只需在類庫中進行,避免不一致並減少維護開銷。
將類庫整合到多個專案中
Tim Corey繼續探索如何在不同型別的專案中使用DRYDemoLibrary類庫,特別是專注於將該庫整合到新的主控台應用程式中。 這顯示了如何在各種應用程式中(不僅僅是單一實例或同一解決方案中的應用程式)重用該庫的功能。
步驟和範例:
建立新解決方案和專案:
Tim 在(17:29)中以建立主控台應用程式的新解決方案開始,模擬在不同型別專案(如Windows服務或主控台應用程式)中需要使用DRYDemoLibrary的場景。
他將新專案命名為ConsoleUI,並展示了如何設置基本的主控台應用程式。
class Program { static void Main(string[] args) { Console.ReadLine(); } }class Program { static void Main(string[] args) { Console.ReadLine(); } }
新增類庫的引用:
Tim解釋了如何在新專案中新增對DRYDemoLibrary DLL的引用。 這涉及瀏覽類庫專案bin文件夾中的DLL文件,並將其新增到主控台應用程式中。
using DRYDemoLibrary;using DRYDemoLibrary;新增引用後,Tim (19:24) 使用庫中的EmployeeProcessor類根據使用者輸入生成員工ID。
Console.WriteLine("What is your first name?"); string firstName = Console.ReadLine(); Console.WriteLine("What is your last name?"); string lastName = Console.ReadLine(); EmployeeProcessor processor = new EmployeeProcessor(); string employeeID = processor.GenerateEmployeeID(firstName, lastName); Console.WriteLine($"Your employee ID is {employeeID}");Console.WriteLine("What is your first name?"); string firstName = Console.ReadLine(); Console.WriteLine("What is your last name?"); string lastName = Console.ReadLine(); EmployeeProcessor processor = new EmployeeProcessor(); string employeeID = processor.GenerateEmployeeID(firstName, lastName); Console.WriteLine($"Your employee ID is {employeeID}");
運行主控台應用程式:
Tim演示了運行主控台應用程式,顯示它使用該庫成功生成了員工ID。 這確認了來自類庫的同一程式碼可以在不同的專案中重用。

更新DLL:
- Tim簡要提到如果DLL更改,可以在引用它的專案中進行更新。 他指出,儘管此影片未詳細介紹,但使用NuGet包是管理和更新多個專案中DLL的推薦方法。
更新DLL和管理NuGet包
Tim Corey簡要介紹了使用NuGet包管理和更新類庫的概念。 這種方法為處理依賴和更新提供了一個更具可擴展性的解決方案,特別是在大規模專案或組織中。
重點:
建立NuGet包:
- Tim建議將類庫打包為NuGet包,而不是手動管理DLL文件。 這涉及將DLL打包到NuGet包並將其上傳到NuGet伺服器(私有或公共)。
更新包:
- 使用NuGet包可以簡單地通過更新包版本來更新引用類庫的所有專案。 這確保了一致性並降低了版本不匹配或更新丟失的風險。
效益:
集中管理:NuGet包提供了一種集中管理庫版本和依賴的方法。
易於更新:跨多個專案更新庫變得更容易且更可靠。
- 整合:NuGet與各種開發工具和環境整合,簡化管理庫依賴的過程。
在單元測試中實施DRY:速成課
在這一部分中,Tim Corey演示了如何應用DRY(Don't Repeat Yourself)原則來增強單元測試。 他展示了如何在開發工作中實施DRY原則,特別是專注於單元測試。
初始測試設置
Tim首先運行一個因DLL中的錯誤而目前失敗的單元測試。 他強調了單元測試在發現問題中的重要性,儘管程式碼在主解決方案之外。 程式碼預期從四個字母的輸入中,但是Tim傳遞了一個三字母的名字,導致DLL文件出現崩潰,即使它未直接包含在解決方案中。

重構程式碼以解決錯誤
為了解決名字處理的問題,Tim重構了程式碼。 他解釋了如何通過建立一個新的類庫專案(23:50)來應用DRY於開發。 此方法確保對多個物件的更改可以一次完成,並在無需重複修復的情況下有效地測試。

新增單元測試
Tim在類庫專案中引入了一個名為EmployeeProcessorTest的新測試類,並使用XUnit設置單元測試。 他演示了如何建立生成員工ID的測試方法,並討論了模擬依賴而非依賴實際值的重要性。

寫測試方法
Tim編寫了一個名為GenerateEmployeeID_ShouldCalculate的單元測試方法。 他設置了一個帶內聯資料的理論以測試不同場景,確保方法返回預期結果。 他還解釋了如何使用Assert.Equal來驗證輸出。
public class EmployeeProcessorTest
{
[Theory]
[InlineData("Timothy", "Corey", "TimoCore")]
public void GenerateEmployeeID_ShouldCalculate(string firstName, string lastName, string expectedStart)
{
// Arrange
var processor = new EmployeeProcessor();
// Act
var actualStart = processor.GenerateEmployeeID(firstName, lastName).Substring(0, 8);
// Assert
Assert.Equal(expectedStart, actualStart);
}
}public class EmployeeProcessorTest
{
[Theory]
[InlineData("Timothy", "Corey", "TimoCore")]
public void GenerateEmployeeID_ShouldCalculate(string firstName, string lastName, string expectedStart)
{
// Arrange
var processor = new EmployeeProcessor();
// Act
var actualStart = processor.GenerateEmployeeID(firstName, lastName).Substring(0, 8);
// Assert
Assert.Equal(expectedStart, actualStart);
}
}運行單元測試
Tim強調模擬動態資料(如日期和時間值)以控制測試條件和結果的重要性。 他討論了處理動態字串的挑戰,以及如何使用受控值測試不同情況。 隨後,他運行單元測試,但在此之前,他新增了兩個運行測試所需的NuGet包:xunit.runner.visualstudio。

在成功運行對一個內聯資料的所有測試後,輸出如下:

現在,在(31:30),Tim新增了另一個內聯資料並將子字串的第二個參數更改為expectedStart.Length:
public class EmployeeProcessorTest
{
[Theory]
[InlineData("Timothy", "Corey", "TimoCore")]
[InlineData("Tim", "Corey", "TimCore")]
public void GenerateEmployeeID_ShouldCalculate(string firstName, string lastName, string expectedStart)
{
var processor = new EmployeeProcessor();
var actualStart = processor.GenerateEmployeeID(firstName, lastName).Substring(0, expectedStart.Length);
Assert.Equal(expectedStart, actualStart);
}
}public class EmployeeProcessorTest
{
[Theory]
[InlineData("Timothy", "Corey", "TimoCore")]
[InlineData("Tim", "Corey", "TimCore")]
public void GenerateEmployeeID_ShouldCalculate(string firstName, string lastName, string expectedStart)
{
var processor = new EmployeeProcessor();
var actualStart = processor.GenerateEmployeeID(firstName, lastName).Substring(0, expectedStart.Length);
Assert.Equal(expectedStart, actualStart);
}
}在(32:05)再次運行單元測試,第二個理論的測試失敗:

通過私有方法提高程式碼
為了遵循DRY,Tim進一步重構程式碼,通過在DRYDemoLibrary下的實際GetPartOfName。 此方法處理部分名字的提取,提高了程式碼可重用性和可讀性。 Tim進行了以下更改:
public string GenerateEmployeeID(string firstName, string lastName)
{
string employeeID = $@"{GetPartOfName(firstName, 4)}{GetPartOfName(lastName, 4)}{DateTime.Now.Millisecond.ToString()}";
return employeeID;
}
private string GetPartOfName(string name, int numberOfCharacters)
{
string output = name;
if (name.Length > numberOfCharacters)
{
output = name.Substring(0, numberOfCharacters);
}
return output;
}public string GenerateEmployeeID(string firstName, string lastName)
{
string employeeID = $@"{GetPartOfName(firstName, 4)}{GetPartOfName(lastName, 4)}{DateTime.Now.Millisecond.ToString()}";
return employeeID;
}
private string GetPartOfName(string name, int numberOfCharacters)
{
string output = name;
if (name.Length > numberOfCharacters)
{
output = name.Substring(0, numberOfCharacters);
}
return output;
}更新單元測試
Tim更新了單元測試以反映程式碼中的更變,例如修改子字串的預期長度。 他解釋了運行這些測試如何快速識別問題並確認程式碼符合新要求。 Tim新增了新理論,然後運行單元測試以驗證輸出是否符合預期:

使用.NET Standard庫擴展多功能性
建立.NET Standard庫
為了提高您類庫的多功能性,Tim Corey推薦由.NET Framework類庫轉變為.NET Standard類庫。 這一變更使類庫能在各種平台上相容,包括:
- Windows平台:WinForms、WPF和主控台應用程式
- 跨平台:.NET Core、Xamarin(適用於iOS和Android)、Linux和macOS
建立.NET Standard庫的步驟:
- 新增新專案:右鍵點擊您的解決方案並選擇新增新專案。
選擇.NET Standard:選擇.NET Standard而不是.NET Framework類庫。 此類庫型別支持廣泛的平台。

- 程式碼遷移:將現有程式碼(例如EmployeeProcessor類)複製並粘貼到新的.NET Standard庫中。 該過程可能需要一些小調整,但核心邏輯保持一致。
通過轉換為.NET Standard,您讓您的庫可從各種平台存取,減少了在不同應用程式型別中的程式碼重複並節省開發 effort。
避免程式碼和測試中的重複
減少開發中的重複
Tim Corey強調,通過採用.NET Standard類庫,您不僅在程式碼庫中最小化程式碼重複,也在開發過程中最小化。 而不是在不同平台專案中重複程式碼,您可以在一個通用的庫中集中它,適用於多個環境。
效益:
- 統一程式碼庫:對於各種平台只有一個程式碼庫,減少了維護和更新程式碼所需的 effort。
- 簡化的測試:通過.NET Standard庫,您可以一次性編寫單元測試,確保它們適用於所有支持的平台。
測試和除錯:Tim介紹了自動化測試作為進一步減少 effort 和重複的一種方法。 自動化測試證明您的程式碼正確無誤而不需要手動測試每個應용程式的迭代版本。
運用DRY原則的提示:知道何時該停止
Tim Corey強調,雖然遵循DRY(Don't Repeat Yourself)原則對編寫可維護程式碼至關重要,但知道何時何地應用它也很重要。 並非所有情況都需要相同的方法,因此這裡有一些取材自Tim的深刻見解的實用提示:
避免在程式碼後面和UI中的程式碼:Tim建議不要將邏輯直接放在程式碼後面文件或使用者介面中。 例如,業務邏輯不應嵌入在表單或按鈕單擊事件中。 而是將這些邏輯保留在單獨的類或庫中。 這種分離有助於保持一個清晰的架構,使您的程式碼更能在不同的使用者介面中重用。
利用.NET Standard Library:建立庫時,如果可能,Tim建議使用.NET Standard庫而非.NET Framework庫。 .NET Standard庫更具多功能性,使您的程式碼可用於不同平台,包括.NET Core、Xamarin等。 這種方法減少了程式碼重複,並增強了程式碼的便攜性。
分離平台特定程式碼:由於平台的特定需求(如文件處理或配置管理),某些程式碼可能不適合放入.NET Standard庫。 Tim建議在此情況下建立兩個庫:一個用於.NET Standard程式碼,另一個用於平台特定程式碼。 這樣,您可以在滿足平台特定需求的同時重用核心邏輯。
強調單元測試:Tim強烈建議為您的程式碼編寫單元測試。 單元測試幫助及早發現錯誤,確保您的程式碼如預期運行。 它們可以顯著加快除錯過程,因為您可以快速驗證更改而不需手動測試整個應用程式。
- 考量專案規模:對於非常小型或實驗專案,Tim承認建立單獨庫和大範圍單元測試的開銷可能不必要。 然而,對於生產應用程式,建議從清晰架構和單元測試開始,因為小型專案常常隨著時間推移而成長並演變。
通過遵循這些提示,您能夠有效地運用DRY原則,同時在程式碼重用和可維護性需求與實際考量之間取得平衡。
結論
通過設計模式掌握DRY原則對於撰寫乾淨且易於維護的C#程式碼至關重要。 正如Tim Corey所展示的,實施DRY涉及建立可重用方法、利用類庫以及採用.NET Standard以達到更廣泛的相容性。 通過瞭解何時,以及如何應用這些實踐,您可顯著提升程式碼質量和靈活性。
欲獲得更深入的見解,請點擊此連結查看Tim Corey的相關影片。 若要隨時掌握Tim的最新內容,請存取他的YouTube頻道。



