掌握 DRY 原则:在 C# 中应用设计模式,编写更简洁的代码
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)。 然后,事件处理程序中的修订代码只需调用该方法即可。
步骤和示例:
1.初始代码:生成员工 ID 的逻辑直接位于按钮的点击事件处理程序中。

2.重构代码: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 解释说,设计会根据用户的要求或公司的政策不断变化,使其与图形和动画的交互性更强。 因此,他在解决方案中引入了一个 WPF 项目,其中包含准确的字段和一个生成员工 ID 的按钮。
在(9:15)处,Tim 提出了使用类库的有力理由,他说,如果我们要避免重复,那么就应该在新的 WPF 项目中复制粘贴代码。 因此,为了保持 DRY,我们需要在类库中创建类。
步骤和示例:
1.创建类库:
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; } }
2.在项目中使用类库:
在 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)中使用,而无需重复代码。
3.优势:
一致性:通过将逻辑集中到一个类库中,Tim 可以确保对逻辑的更改(如错误修复)在所有项目中统一应用。
- 减少维护:方法的更改只需在类库中进行,避免了不一致,减少了维护开销。
将类库集成到多个项目中
Tim Corey 将继续探讨如何在不同类型的项目中使用 DRYDemoLibrary 类库,特别侧重于将该库集成到新的控制台应用程序中。 这将展示如何在各种应用程序中重复使用库的功能,而不仅仅是单个实例或同一解决方案中的实例。
步骤和示例:
1.创建新的解决方案和项目:
Tim 从(17:29)开始为控制台应用程序创建新的解决方案,模拟您可能需要在不同类型的项目(如 Windows 服务或控制台应用程序)中使用 DRYDemoLibrary 的场景。
他将新项目命名为 ConsoleUI,并展示了如何设置一个基本的控制台应用程序。
class Program { static void Main(string[] args) { Console.ReadLine(); } }class Program { static void Main(string[] args) { Console.ReadLine(); } }
2.在类库中添加引用:
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}");
3.运行控制台应用程序:
Tim 演示运行控制台应用程序,以显示它使用库成功生成了员工 ID。 这就确认了类库中的相同代码可以在不同的项目中重复使用。

4.更新 DLL:
- Tim 简要提到,如果 DLL 发生变化,您可以在引用它的项目中进行更新。 他指出,虽然这段视频没有详细介绍,但使用 NuGet 包是跨多个项目管理和更新 DLL 的推荐方法。
更新 DLL 和管理 NuGet 软件包
Tim Corey 简要介绍了使用 NuGet 包管理和更新类库的概念。 这种方法为处理依赖关系和更新提供了一种更具扩展性的解决方案,尤其是在大型项目或组织中。
关键点:
1.创建 NuGet 软件包:
- Tim 建议为类库创建一个 NuGet 包,而不是手动管理 DLL 文件。 这需要将 DLL 打包成一个 NuGet 包,并上传到 NuGet 服务器(私有或公共)。
2.更新软件包:
- 通过使用 NuGet 软件包,您只需更新软件包版本,即可在所有引用该库的项目中更新该库。 这样才能确保一致性,降低版本不匹配或遗漏更新的风险。
3.优点:
集中管理: NuGet 软件包提供了一种集中管理库版本和依赖关系的方法。
易于更新:在多个项目中更新库变得更容易、更可靠。
- 集成: NuGet 与各种开发工具和环境集成,简化了管理库依赖关系的过程。
在单元测试中实施 DRY:速成班
在本片段中,Tim Corey 演示了如何应用 DRY(不要重复自己)原则来增强单元测试。 他展示了如何在开发工作中实施 DRY 原则,尤其侧重于单元测试。
初始测试设置
Tim 开始运行一个单元测试,由于 DLL 中的一个错误,该测试目前失败了。 他强调了单元测试在发现问题方面的重要性,即使代码在主要解决方案之外。 代码期望输入 4 个字母,但 Tim 传递的却是 3 个字母的名字,即使没有直接包含在解决方案中,也会在 DLL 文件中崩溃。

重构代码以解决错误
为了解决名字处理问题,Tim 对代码进行了重构。 他解释了如何通过创建一个新的类库项目将 DRY 应用到开发中(23:50)。 这种方法可确保对多个对象进行一次修改,并在不重复修复的情况下进行有效测试。

添加单元测试
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库的步骤:
1.添加新项目:右键单击解决方案,选择添加新项目。 2.选择 .NET Standard:与其选择 .NET Framework 类库,不如选择 .NET Standard。 该库类型支持多种平台。

3.代码迁移:将现有代码(如 EmployeeProcessor 类)复制并粘贴到新的 .NET Standard 库中。 在此过程中可能会有细微的调整,但核心逻辑必须保持一致。
通过转换为 .NET Standard,您可以从各种平台访问您的库,减少不同应用类型中的代码重复,节省开发精力。
避免代码和测试中的重复
减少开发中的重复
Tim Corey 强调,通过采用 .NET Standard 库,不仅可以最大限度地减少代码库中的重复代码,还可以减少开发过程中的重复代码。 您不需要在不同的特定平台项目中重复代码,而是将其集中到一个可在多个环境中运行的库中。
优点:
- 统一代码库:针对不同平台的统一代码库可以减少维护和更新代码所需的工作量。
- 简化测试:使用 .NET Standard 库,您只需编写一次单元测试,即可确保它们适用于所有支持的平台。
测试和调试: Tim 介绍了单元测试,这是一种进一步减少工作量和重复的方法。 自动测试可验证代码的正确性,无需对应用程序的每次迭代进行手动测试。
应用 DRY 的技巧:了解何时停止
Tim Corey 强调,虽然遵循 DRY(不要重复自己)原则对于编写可维护代码至关重要,但知道何时何地应用该原则也很重要。 并不是每种情况都需要采用相同的方法,因此,以下是一些从 Tim 的见解中得到启发的实用技巧:
1.Avoid Code in Code-Behind and UI:Tim 建议不要将逻辑直接放在代码隐藏文件或用户界面中。 例如,业务逻辑不应嵌入表单或按钮点击事件中。 在翻译过程中,应尽量避免使用".NET"、"Java"、"Python "或 "Node js "等术语,而应将这些逻辑放在单独的类或库中。 这种分离有助于保持简洁的架构,并使您的代码在不同的用户界面上更易于重复使用。
2.充分利用 .NET Standard库:在创建库时,Tim 建议尽可能使用 .NET Standard库而不是 .NET Framework 库。 .NET Standard 库具有更强的通用性,可以让您的代码在不同的平台上使用,包括 .NET Core、Xamarin 等。 这种方法可以减少代码重复,提高代码的可移植性。
3.分离特定于平台的代码:由于特定于平台的要求(如文件处理或配置管理),有些代码可能不适合放在 .NET Standard 库中。 Tim 建议在这种情况下创建两个库:一个用于 .NET Standard 代码,另一个用于特定平台代码。 这样,在满足特定平台需求的同时,您仍然可以重复使用核心逻辑。
4.强调单元测试:Tim 强烈建议为您的代码编写单元测试。 单元测试有助于及早发现错误,并确保代码按照预期运行。 它们可以大大加快调试过程,因为您可以快速验证更改,而无需手动测试整个应用程序。
5.考虑项目规模:对于小型或试验性项目,Tim 认为可能没有必要创建单独的库和大量的单元测试。 不过,对于生产应用程序,建议从简洁的架构和单元测试开始,因为小项目往往会随着时间的推移而成长和发展。
通过遵循这些技巧,您可以有效地应用 DRY 原则,同时在代码重用和可维护性需求与实际考虑之间取得平衡。
结论
通过设计模式掌握 DRY 原则对于编写简洁、可维护的 C# 代码至关重要。 正如 Tim Corey 所演示的,有效应用 DRY 包括创建可重复使用的方法、利用类库以及采用 .NET Standard 以实现更广泛的兼容性。 通过了解何时以及如何应用这些实践,您可以大大提高代码的质量和灵活性。
有关更深入的见解,请查看 Tim Corey 有关此主题的视频此处。 要了解 Tim 的最新内容,请访问他的 YouTube 频道。



