开放封闭原则 (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
    }
}

 哈哈...一个更好的构建日志功能的方法。

现在,您的软件园又有了一个种子。通过遵循开闭原则,你的代码会更好,更不容易出错。您正在使您的软件变得丰富、绿色和充满活力的道路上。

posted @ 2020-12-19 22:46  超难微猫  阅读(547)  评论(0编辑  收藏  举报