SOLID:开闭原则
开闭原则 (OCP) 是 SOLID 的第二个原则。Bertrand Meyer 博士在他的《面向对象软件构造》一书中提出了这个术语。
开闭原则指出:
那么,扩展和修改是什么意思呢?
这里的扩展意味着在不修改系统的情况下向系统添加新功能。插件系统是 OCP 的主要示例,其中通过新功能添加新功能,而无需修改现有功能。
OCP 表示类方法的行为应该在不修改其源代码的情况下进行更改。您不应该编辑方法的代码(修复错误是可以的),而应该使用多态性或其他技术来改变它的作用。通过编写新代码来添加新功能。
在 C# 中,可以使用以下方法应用开闭原则:
- 使用函数参数
- 使用扩展方法
- 使用类、抽象类或基于接口的继承
- 泛型
- 组合和依赖注入
为了演示 OCP,我们以下面的 `Logger` 类为例。假设您是此类的创建者,其他程序员希望重用您的类,这样他们就不必花费时间重写它(SOLID 原则提倡可重用性)。
public class Logger
{
public void Log(string message)
{
Console.WriteLine(message);
}
public void Info(string message)
{
Console.WriteLine($"Info: {message}");
}
public void Debug(string message)
{
Console.WriteLine($"Debug: {message}");
}
}
现在,一些开发人员希望更改调试消息以适应他们的需求。例如,他们希望调试消息以 `"Dev Debug ->"` 开头。因此,为了满足他们的需求,您需要编辑 `Logger` 类的代码,并为他们创建一个新方法或修改现有的 `Debug()` 方法。如果您更改现有的 `Debug()` 方法,那么其他不希望此更改的开发人员也会受到影响。
使用 OCP 解决此问题的一种方法是使用基于类的继承(多态)和重写方法。您可以将 `Logger` 类的所有方法标记为 `virtual`,这样如果有人想更改任何方法,他们就可以将 `Logger` 类继承到一个新类中并重写它。
public class Logger
{
public virtual void Log(string message)
{
Console.WriteLine(message);
}
public virtual void Info(string message)
{
Console.WriteLine($"Info: {message}");
}
public virtual void Debug(string message)
{
Console.WriteLine($"Debug: {message}");
}
}
现在,一个新类可以继承 `Logger` 类并更改一个或多个方法的行为。希望更改调试消息的开发人员将创建一个新类,继承 `Logger` 类并重写 `Debug()` 方法以显示他们想要的消息,如下所示。
public class NewLogger : Logger
{
public override void Debug(string message)
{
Console.WriteLine($"Dev Debug -> {message}");
}
}
他们现在将使用上面的类来显示他们想要的调试消息,而无需编辑原始类的源代码。
public class Program
{
public static void Main(string[] args)
{
Logger logger = new Logger();
logger.Debug("Testing debug");
Logger newlogger = new NewLogger();
newlogger.Debug("Testing debug ");
}
}
Debug: Testing debug Dev Debug -> Testing debug
因此,使用继承的 OCP 使其“对扩展开放,对修改关闭”。
我们再举一个例子。以下是我们之前在 SRP 部分中创建的 `Course` 类。
public class Course
{
public int CourseId { get; set; }
public string Title { get; set; }
public string Type { get; set; }
public void Subscribe(Student std)
{
Logger.Log("Starting Subscribe()");
//apply business rules based on the course type live, online, offline, if any
if (this.Type == "online")
{
//subscribe to online course
}
else if (this.Type == "offline")
{
//subscribe to offline course
}
// payment processing
PaymentManager.ProcessPayment();
//create CourseRepository class to save student and course into StudentCourse table
// send confirmation email
EmailManager.SendEmail();
Logger.Log("End Subscribe()");
}
}
每当需要添加新类型的课程时,我们都必须编辑上面的 `Course` 类。我们将不得不添加一个 if 条件或 switch 案例来处理课程类型。此外,上面的 `Course` 类不遵循单一职责原则,因为如果订阅课程的处理方式发生任何变化或需要添加新类型的课程,那么我们将不得不更改 `Course` 类。
要将 OCP 应用于我们的 `Course` 类,基于抽象类的继承更合适。我们可以创建一个抽象类作为基类,然后为每种课程类型创建一个新类,并在每个类中实现 `Subscribe()` 方法,该方法将执行所有必要的订阅步骤,如下所示。
public abstract class Course
{
public int CourseId { get; set; }
public string Title { get; set; }
public abstract void Subscribe(Student std);
}
public class OnlineCourse : Course
{
public override void Subscribe(Student std)
{
//write code to subscribe to an online course
}
}
public class OfflineCourse : Course
{
public override void Subscribe(Student std)
{
//write code to subscribe to a offline course
}
}
如您所见,`Course` 类现在是一个抽象类,其中 `Subscribe()` 方法是一个抽象方法,需要在继承 `Course` 类的类中实现。这样,不同课程类型有单独的 `Subscribe()` 函数(关注点分离)。您可以在将来为新类型的课程创建一个新类,该类继承 `Course` 类。这样,您就不必编辑现有类了。
您现在可以按照如下所示将学生订阅到课程:
public class Program
{
public static void Main(string[] args)
{
Student std = new Student() { FirstName = "Steve", LastName = "Jobs" };
Course onlineSoftwareEngCourse = new OnlineCourse() { Title = "Software Engneering" };
onlineSoftwareEngCourse.Subscribe(std);
}
}
OCP 的优点
- 通过不修改现有类来最大限度地减少错误的可能性。
- 通过添加新类轻松添加新功能,其中当前功能不依赖于新类。
- 促进单一职责原则
- 对每个类进行单元测试
- CleanCoder:开闭原则
- 为什么开闭原则是你需要知道但却不知道的