开放封闭原则 (OCP)
英文:Open-Closed Principle 参考/翻译: https://www.dotnetcurry.com/software-gardening/1176/solid-open-closed-principle |
把软件开发比作建筑,我们可以看出软件是坚固的,而且很难改变。相反,我们应该把软件开发比作园艺,因为花园总是在变化的。软件园艺包含实践和工具,可以帮助您为您的软件创建最好的可能的花园,允许它增长和改变较少的努力。了解更多关于什么是软件园艺。
今天我们来看看字母O,SOLID的第二个原理。O 是开放封闭原则。开放-关闭原则(OCP)指出“一个软件实体应该对扩展开放,但对修改关闭”。创造“开放-封闭”这一术语的功劳通常归于Bertrand Meyer在他1998年的著作《Object Oriented Software Construction》中。
当你第一次听到“开闭”这个词的时候,你可能会想,“怎么可能有东西同时开着又关着呢?”,我曾听人开玩笑地把这个叫做Shroedinger(薛定谔)的OOP原则。然而,薛定谔谈论的是猫,而不是面向对象编程。
Robert C.“Uncle Bob ”Martin在他2003年的著作《敏捷软件开发:原则、模式和实践》中进一步扩展了OCP的定义:
“对扩展开放。这意味着模块的行为可以扩展。随着应用程序需求的变化,我们能够用新的行为扩展模块来满足这些变化。换句话说,我们可以改变模块的功能。
“对修改关闭。扩展一个模块的行为不会导致模块的源代码或二进制代码的改变。模块的二进制可执行版本,无论是在可链接库、DLL或Java .jar中,都保持不变。
emmm。听起来鲍勃叔叔想让我们去做不可能的事。我们不仅不能改变现有的代码,而且我们也不能改变exe或dll文件。坚持下去。我将解释这是如何做到的。
对修改关闭
让我们深入了解这个规则,首先查看对修改的关闭,因为这个更容易讨论。简而言之,对修改关闭意味着你不应该改变现有代码的行为。这与我在上一期专栏中解释过的单一职责紧密相关。如果一个类或方法只做一件事,那么修改它的可能性就很小。
但是,有三种方法可以改变现有代码的行为。
第一个是修复一个错误。毕竟,代码不能正常运行,应该被修复。在这里您需要小心,因为该类的客户端可能知道这个错误,并已经采取了措施来解决这个问题。
举个例子,惠普的一些打印机驱动程序多年来都有一个bug。这个错误导致了Windows下的打印错误,惠普拒绝更改它。微软对Word和其他应用程序进行了修改,以解决这个问题。
修改现有代码的第二个原因是重构代码,使其遵循其他可靠的原则。例如,代码可能工作得很好,但是做的事情太多了,因此您希望重构它以遵循单一的职责。
最后,第三种方法(这有点争议)是,如果代码不能改变客户端更改的需求,则允许您更改代码。在这里,您必须小心,以免引入影响客户机的bug。良好的单元测试至关重要。
对扩展开放
在封闭了修改的道路之后,我们转向对扩展的开放。最初,Meyers从实现继承的角度对此进行了讨论。使用此解决方案,您继承一个类和它的所有行为,然后覆盖您想要更改的方法。这避免了更改原始代码,并允许类做一些不同的事情。
任何尝试过大量实现继承的人都知道它有许多缺陷。正因为如此,许多人采用了实现接口继承。使用这种形式,只定义方法签名,每次实现接口时都必须创建每个方法的代码。这是缺点。然而,好处更大,允许您轻松地用一种行为替换另一种行为。常见的例子是基于ILogger接口的日志类,然后实现该类以将日志记录到Windows事件日志或文本文件中。客户端不知道也不关心它正在使用的是哪个实现。
另一种打开方法进行扩展的方法是通过抽象方法。当继承时,抽象方法必须被重写。
与抽象方法比较像的是虚方法。区别在于你必须覆盖抽象方法,而不必须重写虚拟方法。这给作为开发人员的您带来了更多的灵活性。
还有一种提供额外功能的方法,但经常被忽视。这就是扩展方法。虽然它不改变方法的行为,但它允许您在不改变原始类代码的情况下扩展类的功能。
现在,让我们回到Uncle Bob’s对封闭定义的扩展进行修改。我们需要在不改变原始代码或二进制(exe或dll)文件的情况下扩展类的功能。好消息是,我们在这里看到的每一种扩展行为的方法都符合Uncle Bob的定义。在.net中没有规定关于类的所有内容必须在同一个源文件中,甚至在同一个程序集中。因此,通过将扩展代码放在不同的程序集中,您可以遵守Uncle Bob定义的OCP。
一个开闭原则的例子
解释很好,但是让我们看一个例子。首先,代码违反了OCP。
public class ErrorLogger { private readonly string _whereToLog; public ErrorLogger(string whereToLog) { this._whereToLog = whereToLog.ToUpper(); } public void LogError(string message) { switch (_whereToLog) { case "TEXTFILE": WriteTextFile(message); break; case "EVENTLOG": WriteEventLog(message); break; default: throw new Exception("Unable to log error"); } } private void WriteTextFile(string message) { System.IO.File.WriteAllText(@"C:\Users\Public\LogFolder\Errors.txt", message); } private void WriteEventLog(string message) { string source = "DNC Magazine"; string log = "Application"; if (!EventLog.SourceExists(source)) { EventLog.CreateEventSource(source, log); } EventLog.WriteEntry(source, message, EventLogEntryType.Error, 1); } }
如果您需要添加一个新的日志位置,比如数据库或web服务,会发生什么?您需要在几个地方修改此代码。您还需要添加新的代码来将消息写入新位置。最后,您必须修改单元测试以适应新功能。所有这些类型的更改都有可能引入bug。
您可能忽略了这段代码的另一个问题。它违反了单一责任原则。
这里有一个更好的方法。虽然功能还没有完全完成,但这只是一个起点。
public interface IErrorLogger { void LogError(string message); } public class TextFileErrorLogger : IErrorLogger { public void LogError(string message) { System.IO.File.WriteAllText(@"C:\Users\Public\LogFolder\Errors.txt", message); } } public class EventLogErrorLogger : IErrorLogger { public void LogError(string message) { string source = "DNC Magazine"; string log = "Application"; if (!EventLog.SourceExists(source)) { EventLog.CreateEventSource(source, log); } EventLog.WriteEntry(source, message, EventLogEntryType.Error, 1); } }
当您需要实现其他类型的记录器时,这很简单……只需添加新的类,而不是修改现有的类。
public class DatabaseErrorLogger : IErrorLogger { public void LogError(string message) { // Code to write error message to a database } } public class WebServiceErrorLogger : IErrorLogger { public void LogError(string message) { // Code to write error message to a web service } }
哈哈...一个更好的构建日志功能的方法。
现在,您的软件园又有了一个种子。通过遵循开闭原则,你的代码会更好,更不容易出错。您正在使您的软件变得丰富、绿色和充满活力的道路上。