C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分

目录

介绍

定义和历史

我如何理解OCP?

我如何理解OCP?

3个级别

当代码关闭时

预测未来和YAGNI

让我们编码

不好的例子

更好的方法

SOLID恰当的结合在一起

更多例子

修改或扩展

什么时候我会扩展

例1

什么时候我会修改

例2

例3

结论


介绍

本文是关于SOLID原则中的开放封闭原则OCP),您不必阅读我以前的文章来理解它,但我鼓励您这样做。:)

我写这篇文章的动机是这个原则存在很大的混乱,并且对它有许多不同的解释。

你可以在这里找到我之前的四篇文章:

  1. C# 通过不好的例子学习如何制作好的代码——第1部分
  2. C# 通过不好的例子学习如何制作好的代码——第2部分
  3. C# 通过不好的例子学习如何制作好的代码——第3部分
  4. C# 通过不好的例子学习如何制作好的代码——第4部分

C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分_第1张图片

这个原则也让我自己感到困惑,这就是为什么我深入了解这个话题,并在此提出我的发现和想法。

在我看来,除了里氏替换原则之外,它是SOLID中最难解释的(完全理解)。

根据我的经验,我可以说这很令人困惑的,即使是高级工程师和大多数开发人员也只知道它的定义,而不会深入了解它为何以及何时是有用的。

这可能导致盲目地应用此规则,这可能使代码库变得奇怪。

我还在许多互联网论坛上看到很多人都在提出具体情况,并问它是否违反了OCP等待。 这些问题有时会得到解答,但是:

  • 有很多不同的意见
  • 没有事实来源
  • 并且视情况而定

在我看来,OCP仍然是个谜。

在本文中,我将尝试:

  • 介绍我如何理解OCP
  • 目前违反OCP和突出问题的解决方案
  • 当我在应用需求更改时扩展我的代码时提供示例
  • 当我在应用需求更改时不修改我的代码时,提供示例

与往常一样,我将使用一个真实世界的例子,首先显示一个坏的代码(在我看来),然后提出一个更好的解决方案。

请记住,提供代码示例,我没有提供端到端功能实现,我只提出了一种方法。

要理解本文,您需要:

  • C#语言的基础知识
  • 基本了解依赖注入设计模式和IOC容器
  • NuGet包的基本知识

定义和历史

Bertrand Meyer面向对象的软件构建一书中介绍了开放封闭原则。

它说:

软件实体(类,模块,函数等)应该是对扩展开放的,但是对修改关闭

解决这个问题的建议方法是继承。

这意味着您应该通过创建子类来扩展组件,而不是修改现有代码。 

Robert C. Martin介绍了一种解决这个问题的新方法——抽象。

因此,如果您将编程用于抽象,您的代码将更灵活地适应未来的更改,因为您可以替换或添加新实现而无需修改类的使用者,因为使用者只会看到抽象(abstract类或interface)而不是具体实现。

另外一件事是OCP在主要规则中添加了一个例外,即bug修复。因此,如果组件中存在错误,则允许修改代码以修复此错误。

我如何理解OCP

我对这个原理的理解是它完全是关于设计函数,类和模块。

这是什么意思?

那么,IMO作为一名经验丰富的开发人员,在创建新功能的同时,您应该尝试预测未来。

您应该确定组件中将来可能需要进行一些修改的位置(需求更改)而这些修改将会出现问题(重新设计模块,很有可能在现有功能中引入错误)。

一旦确定了这些地方,就应该找到一个解决方案,使您的代码能够灵活应对这些变化。

所以...

IMO,这并不意味着:

你永远不应该修改你现有的类

但:

当您想要扩展功能时,您应该以一种您不必这样做的方式设计您的模块

但是,我们正在设计符合OCP的代码,以便能够扩展代码而不是修改代码。因此修改设计为可扩展的代码没有多大意义。这就是为什么在设计阶段遵循OCP与未来应用需求变更的方式严格相关,理想情况下应该通过扩展而不是通过修改来完成。

所以IMO,你可以说,违反OCP将是不可扩展代码的实现以及对现有代码的修改。我稍后会解释我认为第二个什么时候可以。

您可以使用许多技术编写符合OCP的代码,例如:

  • 抽象化
  • 依赖注入
  • 多态性/继承
  • 组成
  • 策略模式
  • 将委托传递给函数
  • 等等

完整的解决方案通常包含它们的组合。你应该使用哪种,取决于具体情况。在我的例子中,我将使用抽象和依赖注入。

我如何理解OCP

为什么不编写有效的代码并在需要时修改它而没有这种灵活性?

因为,现有代码中的每个修改:

  • 是非常危险的,理想情况下,你应该避免它
  • 可能会在功能中引入一个错误,而这个错误并不是要修改的
  • 可能会导致您的单元/集成测试的修改
  • 可能导致破坏向后兼容性(即,修改NuGet包中存在的类,该类在使用该包的外部系统中用作基类)

如果您的代码已经对修改关闭并对扩展开放,则无需在新需求出现时修改现有代码,您只需添加新组件并使用配置开始使用它们。

3个级别

C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分_第2张图片

在该原则的原始定义中,据说该规则应在3个层面得到尊重:

  • 函数级别
  • 类级别
  • 模块级别

虽然函数和类级别的名称很清楚,但我对模块级别的理解是,在这种情况下,模块是一组类和它们之间的依赖关系,它创建了一些功能。

所以它比程序集更抽象。我看到了对模块级别的许多不同解释,所以我想清楚地说,在我看来,它并没有说你不应该在你现有的程序集中添加新的代码。

我将在我的示例中显示在这三个级别中的每个级别上违反OCP

当代码关闭时

在我看来,组件在第一次部署到生产时就会关闭。我假设在发布之前,它仍处于开发阶段。

C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分_第3张图片

预测未来和YAGNI

显然,上面写的所有内容都假设我们生活在一个理想的世界中,开发人员是能够预测将来会发生的一切事情的透视者。

C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分_第4张图片

在现实世界中,我们永远不会预测可能来自业务的所有需求变化。

我们甚至不应该尝试,因为它可能导致过度工程,这也是不好的。因此,我们最终可以得到极其复杂的系统,我们永远不需要使用它的灵活性/复杂性。

但是,如果在开发某些功能时,我们发现该功能的本质是它需要一些扩展或更改,而对此的需求只是时间问题,我们应该以一种我们将要设计的方式来设计我们的代码。能够通过添加新组件来实现此扩展,而不是修改现有组件。

这是最困难的部分。我们需要在开放封闭原则和YAGNI(你不需要它)原则之间找到平衡点。

让我们编码

不好的例子

C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分_第5张图片

作为代码示例,我选择了一个验证密码的模块。

最初的要求是密码:

  • 必须超过8个字符
  • 不能与用户名相同
  • 不能与用户过去设置的任何密码相同

下面我提出一个实现,它违反了所有3个级别的OCP

public class PasswordChangeModel
{
    public string NewPassword { get; set; }
    public List PasswordHistoryItems { get; set; }
    public string Username { get; set; }
}

public class PasswordValidator
{
    private const int _minLength = 8;

    bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        if (passwordChangeModel.NewPassword.Length < _minLength ||
            passwordChangeModel.NewPassword == passwordChangeModel.Username ||
            (passwordChangeModel.PasswordHistoryItems != null &&
            passwordChangeModel.PasswordHistoryItems.Contains(passwordChangeModel.NewPassword)))
        {
            return false;
        }

        return true;
    }
}

public class PasswordManager
{
    public bool ChangePassword()
    {
        var passwordValidator = new PasswordValidator();
        var isPasswordValid = passwordValidator.IsValid();

        // the rest of the logic
    }
}

上面的代码提供了3个类:

  • PasswordChangeModel——包含所有密码更改详细信息的对象
  • PasswordValidator——我们的验证类
  • PasswordManager——PasswordValidator 的类使用者

由于密码验证规则很可能会随着时间的推移而发生变化以提高安全性,因此我们的模块应该可以进行扩展。由于我们不想在扩展它时破坏现有功能,因此也应该对修改关闭。

以上示例明显违反了所有3个级别的OCP

  • 函数级别 ——新规则的任何要求或现有规则的更改都需要修改IsValid 方法。很容易在这个函数中引入一些愚蠢的错误,即使只有3个规则,代码也是不可读的。你可以想象10条规则会有多乱。
  • 类级别 ——如果我们想要将最小密码长度更改为10,我们需要在类级别上进行修改并修改如下private字段:
private const int _minLength = 10;

有什么问题?

如果只有一个PasswordValidator类的使用者 (或者所有使用者需要使用最小长度为10)并且没有PasswordValidator类的子类,则没有问题。

但是.. 

如果PasswordValidator类被许多使用者使用,您将改变所有使用者的行为。也许在2个应用程序中,它应该仍然是8,在第三个应用程序中,它应该是10. 

如果PasswordValidator 类是其他类的基类,您也将改变它们的行为。这可能不是预期的行为。

此外,如果您将考虑需求不能与用户过去设置的任何密码相同,您可以假设它可能会更改为:
不能与用户设置的任何X最后密码相同,其中x可以是任何东西:D

PasswordChangeModel类不允许实现此功能,因为我们没有关于密码更改顺序的信息。

  • 模块级别 ——假设这3个类创建了一个模块,如果我们希望我们的使用者开始使用我们的密码验证器的新实现,我们必须修改使用者,因为PasswordValidator实现不会在接口后隐藏,我们将无法只是注入一个不同的实现。

如您所见,我们的代码明显违反了OCP

如果我们想创建一个可重用的库来验证我们公司或许多公司的许多应用程序中的密码,并使用NuGet进行分发,上述实现很可能会在将来造成麻烦。

为什么?

因为,你不能说:

  • 什么是PasswordValidator使用者 
  • 什么类继承自PasswordValidator(除非你把它标记为密封的(MSDN),但它是一种限制)

这就是为什么我们必须保持向后兼容性以避免改变使用我们库的应用程序的行为。

但是,如果我们的密码验证模块是一个简单的库,我们可以拥有PasswordValidator类的多个使用者和子类,在扩展功能时我们不希望改变它们的行为。

好吧,为了证明违反OCP,我们试着处理一些可能会发生的需求变化。

现在,作为现有验证的补充,我们必须添加一条新规则:

  • 密码不能包含以下单词:“password”“qwerty”“abc123”

由于我们不关心OCP,我们只是向我们的PasswordValidator类添加另一条规则:

public class PasswordValidator
{
    private const int _minLength = 8;
    static List BlackList = new List { "password", "qwerty", "abc123" };

    bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        if (passwordChangeModel.NewPassword.Length < _minLength ||
            passwordChangeModel.NewPassword == passwordChangeModel.Username ||
            (passwordChangeModel.PasswordHistoryItems != null &&
            passwordChangeModel.PasswordHistoryItems.Contains(passwordChangeModel.NewPassword)) ||
            (BlackList != null && BlackList.Contains(passwordChangeModel.NewPassword)))
        {
            return false;
        }

        return true;
    }
}

现在,一些包含新引入的禁用词(并且正在通过)的IsValid方法的单元测试将开始失败。

但更大的问题是,这种变化将影响所有PasswordValidator类的使用者和子类。因此,如果我们引入了一个错误,在这样脆弱的代码中非常容易,它可能会破坏现有的功能。

而且,每次我们想要在黑名单中添加/删除/更改单词时,您将不得不修改PasswordValidator 类,这将影响所有使用者和派生类。

您可以考虑许多不同的规则,我们需要在几个需求变更之后实现这些规则:

  • 最大密码长度
  • 必须包含特殊字符
  • 不能有以下数字
  • 必须包括大写
  • 必须包括小写
  • 必须包含2位数字
  • 不能与用户设置的任何X个最后密码相同(如上所述)
  • 还有更多

我们的代码不适合处理任何扩展或修改。
为什么?
因为开发人员只是直接实现了需求中所说的内容,而没有考虑太多。保持简单,他创建了不可扩展的代码。

让我们尝试更好的方法!

更好的方法

C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分_第6张图片

让我们从一开始就采用更好的方法开始:

public class PasswordChangeModel
{
    public string NewPassword { get; set; }
    public List PasswordHistoryItems { get; set; }
    public string Username { get; set; }
}

public class PasswordHistoryItem
{
    public string PasswordText { get; set; }
    public DateTime CreationDate { get; set; }
}

public interface IPasswordValidator
{
    bool IsValid(PasswordChangeModel passwordChangeModel);
}

public interface IPasswordValidationRule
{
    bool IsValid(PasswordChangeModel passwordChangeModel);
}

public class PasswordMinLengthRule : IPasswordValidationRule
{
    private readonly int _minLength;

    public PasswordMinLengthRule(int minLength)
    {
        _minLength = minLength;
    }

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword.Length >= _minLength;
    }
}

public class PasswordUsernameRule : IPasswordValidationRule
{
    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword != passwordChangeModel.Username;
    }
}

public class PasswordHistoryRule : IPasswordValidationRule
{
    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.PasswordHistoryItems == null || 
            !passwordChangeModel.PasswordHistoryItems.Any
            (x => x.PasswordText == passwordChangeModel.NewPassword);
    }
}

public class PasswordValidator : IPasswordValidator
{
    private List _validationRules;

    public PasswordValidator(List validationRules)
    {
        _validationRules = validationRules;
    }

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        foreach (var validationRule in _validationRules)
        {
            if (!validationRule.IsValid(passwordChangeModel))
            {
                return false;
            }
        }
        return true;
    }
}

好的,所以我们以稍微不同的方式实现PasswordChangeModel类。现在我们有关于旧密码的更多信息。现在,我们在PasswordHistoryItem对象中拥有过去每个密码的创建日期。很可能需要改变需求的情况下:

  • 不能与用户过去设置的任何密码相同

至:

  • 不能与用户设置的任何X个最后密码相同

我们已准备好所有信息,我们只需要添加新规则。

接下来是2个接口的介绍:

  • IPasswordValidator
  • IPasswordValidationRule

第一个是使用者和PasswordValidation类之间的抽象层。由于引入它,我们在模块级别上删除了OCP违规(同时紧密耦合)。
我将在稍后的使用者代码中显示它。

第二个接口是PasswordValidatorPasswordValidationRules之间的抽象:

  • PasswordMinLengthRule
  • PasswordUsernameRule
  • PasswordHistoryRule

如您所见,每个规则逻辑都被移动到一个单独的类中,并且这些类正在实现IPasswordValidationRule interface

最后是“ heart” PasswordValidator类,它现在实现了IPasswordValidator接口。

public class PasswordValidator : IPasswordValidator
{
    private List _validationRules;

    public PasswordValidator(List validationRules)
    {
        _validationRules = validationRules;
    }

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        foreach (var validationRule in _validationRules)
        {
            if (!validationRule.IsValid(passwordChangeModel))
            {
                return false;
            }
        }
        return true;
    }
}

你可以看到,现在我们正在向PasswordValidator类注入所有必需的规则。这应该在每个应用程序(主机)配置中完成。我在这里谈论应用程序,它将使用验证模块。因此,每个应用程序甚至同一应用程序中的每个使用者都可以配置不同的规则集(List)。

现在让我们假设我们有2个密码验证功能的使用者:

public class Consumer1
{
    private readonly IPasswordValidator _passwordValidator;

    public Consumer1(IPasswordValidator passwordValidator)
    {
        _passwordValidator = passwordValidator;
    }

    void SomeMethod(PasswordChangeModel passwordChangeModel)
    {
        // some code

        _passwordValidator.IsValid(passwordChangeModel);

        // some code
    }
}

public class Consumer2
{
    private readonly IPasswordValidator _passwordValidator;

    public Consumer2(IPasswordValidator passwordValidator)
    {
        _passwordValidator = passwordValidator;
    }

    void SomeMethod(PasswordChangeModel passwordChangeModel)
    {
        // some code

        _passwordValidator.IsValid(passwordChangeModel);

        // some code
    }
}

您可以看到我们现在正在使用依赖注入原则注入IPasswordValidator实现。

多亏了这一点,我们可以通过注入与PasswordValidator类不同的实现来自由地更改使用者密码验证功能所使用的功能。我们的模块可以进行扩展,并在密码验证时对修改关闭。

在我解释模块的配置之前,让我们尝试实现一个新的需求(新密码验证规则):

  • 密码不能包含以下单词:“password”“qwerty”“abc123”

再次,但在一个新的代码库中。

我们真正需要做的是添加一个新的IPasswordValidationRule实现:

public class PasswordBlackListRule : IPasswordValidationRule
{
    private readonly List _blackList;

    public PasswordBlackListRule(List blackList)
    {
        _blackList = blackList;
    }
    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return _blackList == null || !_blackList.Contains(passwordChangeModel.NewPassword);
    }
}

你可以看到PasswordBlackListRule实现本身也是对扩展开发和对修改关闭的,因为我们正在注入一个黑名单,如果我们必须修改它,我们将在配置中而不是在规则实现中。

我们现在一起设置所有内容!

我建议使用现有的IoC容器中的一个来设置密码验证模块的配置。下面是使用Ninjectninject模块配置)的示例:

public class Bindings : NinjectModule
{
    public override void Load()
    {
        Bind().ToConstant(new PasswordValidator
             (GetPasswordRulesForConsumer1())).WhenInjectedInto();
        Bind().ToConstant(new PasswordValidator
             (GetPasswordRulesForConsumer2())).WhenInjectedInto();
    }

    private List GetPasswordRulesForConsumer1()
    {
        return new List()
        {
            { new PasswordMinLengthRule(8) },
            { new PasswordUsernameRule() },
            { new PasswordHistoryRule() }
        };
    }

    private List GetPasswordRulesForConsumer2()
    {
        return new List()
        {
            { new PasswordMinLengthRule(8) },
            { new PasswordUsernameRule() },
            { new PasswordHistoryRule() },
            { new PasswordBlackListRule(new List() { "password", "qwerty", "abc123" }) }
        };
    }

}

正如您在上面所看到的,多亏了来自Ninject APIWhenInjectedInto方法,即使在同一个应用程序中,我们也可以拥有不同的配置(每个使用者的一套规则)。对于不熟悉Ninject的人,这里有一个说明文档的链接:Ninject文档

第一个使用者将使用密码验证器和3条规则:

  • PasswordMinLengthRule
  • PasswordUsernameRule
  • PasswordHistoryRule

而第二个使用者将使用4个验证规则:

  • PasswordMinLengthRule
  • PasswordUsernameRule
  • PasswordHistoryRule
  • PasswordBlackListRule

显然,您可以在应用程序中手动实现依赖注入,这取决于您的需求。

所以当我们想要:

  • 向验证过程添加新规则——我们需要创建新的IPasswordValidationRule实现并将其添加到配置中——现有组件不会更改
  • 将现有规则添加到验证过程——更改配置——现有组件不会更改
  • 替换验证过程中的现有规则——更改配置——现有组件不会更改
  • 从验证过程中删除规则——更改配置——现有组件不会更改

C#坏习惯:通过不好的例子学习如何制作好的代码——第5部分_第7张图片

即使是这样灵活的代码也无法满足各种要求,但在我的意见中进一步提高灵活性将是过度工程,并将违反YAGNI。在这里,我想说我们达到了平衡。

SOLID恰当的结合在一起

你可以注意到,在这个例子中,我也在使用:

  • 依赖性反转——依赖注入实现
  • 单一责任——
    PasswordValidator ——使用规则验证密码(1责任)
    特殊规则——仅检查一件事

来自SOLID原则。

只有SOLID规则的组合才能让您编写更易理解,更灵活且可维护的软件。

更多例子

如果你想看到另一个应用OCP的例子,请阅读我的文章,其中我描述了为什么switchcase语句破坏OCP以及如何将其重新编写为符合OCP的代码:

  • C#坏习惯:通过不好的例子学习如何制作好的代码——第2部分

修改或扩展

作为符合OCP标准的模块设计的自然延续是应用需求变更,在本段中,我将展示具体的例子,当我这样做时我会/不会修改原始代码。

什么时候我会扩展

让我们假设我们已经实现了前一段中提到的密码验证库(更好,更灵活的版本)。我们决定将其作为NuGet包分享。

1

我们假设在nuget包的版本1中,我们实现了PasswordMinLengthRule规则,如下:

public class PasswordMinLengthRule : IPasswordValidationRule
{
    private const int _minLength = 8;

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword.Length >= _minLength;
    }
}

然后,我们意识到不同的应用程序想要使用不同的最小长度。这个版本显然不具备这种灵活性。

我们希望用更灵活的选项替换此实现:

public class PasswordMinLengthRule : IPasswordValidationRule
{
    private readonly int _minLength;

    public PasswordMinLengthRule(int minLength)
    {
        _minLength = minLength;
    }

    public bool IsValid(Password password)
    {
        return password.NewPassword.Length >= _minLength;
    }
}

现在,每个应用程序都可以在创建规则对象时设置自己的密码最小长度。

但是如何用更好的版本替换代码......

有什么选择?

选项1:我们可以修改现有的类代码——这会破坏OCP

选项2:我们可以添加一个新规则,除了原始规则之外还有更灵活的实现——遵循OCP

public class PasswordConfigurableMinLengthRule : IPasswordValidationRule
{
    private readonly int _minLength;

    public PasswordConfigurableMinLengthRule(int minLength)
    {
        _minLength = minLength;
    }

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword.Length >= _minLength;
    }
}

在这种情况下,我会选择选项2并遵循OCP

为什么?

因为这段代码是作为包分发的,所以我不知道PasswordMinLengthRule类原始版本的所有调用者和子类。因此,遵循Option1并修改软件包版本2中的现有代码,我可能会破坏现有代码并强制类的使用者对其进行修改——请阅读缺乏向后兼容性。例如,它们可能需要升级NuGet包版本,因为它还包含一个非常重要的错误修复。

但我会在这里再做一件事,我会标记现有的类——PasswordMinLengthRule——已经过时了:

[Obsolete("You should not use this class anymore, 
there is more flexible version of this class - PasswordConfigurableMinLengthRule")]
public class PasswordMinLengthRule : IPasswordValidationRule
{
    private const int _minLength = 8;

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword.Length >= _minLength;
    }
}

多亏了这一点,我们库的使用者将被告知他们应该开始转移到新版本,但是它们不会出现既成事实。

如果您不熟悉Obsolete属性,请阅读MSDN

什么时候我会修改

2

现在请忘记原始示例,并想象这是您PasswordMinLengthRule的第一个版本:

public class PasswordMinLengthRule : IPasswordValidationRule
{
    private readonly int _minLength;

    public PasswordMinLengthRule(int minLength)
    {
        _minLength = minLength;
    }

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword.Length <= _minLength;
    }
}

你有一个明显的错误:

return password.NewPassword.Length <= _minLength;

比较符号写错了:

<=

代替:

>=

不管它是否是一个与100个消费者共享的nuget包,不管是否有许多类继承自PasswordMinLengthRule,或者如果只有一个调用者而没有一个类继承自PasswordMinLengthRule,你必须修复明显的bug尽快地。使用上面的代码没有意义。

所以我只想修改这个类并更正比较符号。由于OCP允许在修复错误时修改代码,因此该解决方案仍然符合OCP标准。

3

让我们从头开始吧。现在假设我们有3PasswordMinLengthRule类的调用者,这是最初的实现:

public class PasswordMinLengthRule : IPasswordValidationRule
{
    private const int _minLength = 8;

    public bool IsValid(Password password)
    {
        return password.NewPassword.Length >= _minLength;
    }
}

它是一个类,仅在一个解决方案中使用(它不是NuGet包),此类的子类不存在,并且您拥有3个正在使用它的应用程序。

有一个新要求说:所有3个调用者现在应该使用10作为最小密码长度

如果您想要关注OCP(而不是修改原始PasswordMinLengthRule类),您可以做什么:

选项1:创建一个新的PasswordMinLengthRule实现:

public class PasswordMin10LengthRule : IPasswordValidationRule
{
    private const int _minLength = 10;

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword.Length >= _minLength;
    }
}

或更多可配置的:

public class PasswordConfigurableMinLengthRule : IPasswordValidationRule
{
    private readonly int _minLength;

    public PasswordConfigurableMinLengthRule(int minLength)
    {
        _minLength = minLength;
    }

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword.Length >= _minLength;
    }
}

并开始在三个调用者中使用此实现。

假设我们正在使用依赖注入并且我们正在注入IPasswordValidationRule实现,我们只需要替换IOC容器中的实现/手动配置。

请注意,如果我们这样做,旧的PasswordMinLengthRule将成为您必须维护的孤儿类,并且每个将阅读代码的人都会想知道为什么这个类存在而不使用它。

添加新类和停止使用旧类并修改旧类有什么区别?没有区别。:)

在这种情况下,我不会遵循这两个选项中的任何一个,也不会遵循OCP原则。我会使用选项2中的代码,只需用以下代码替换原始代码:

public class PasswordMinLengthRule : IPasswordValidationRule
{
    private readonly int _minLength;

    public PasswordMinLengthRule(int minLength)
    {
        _minLength = minLength;
    }

    public bool IsValid(PasswordChangeModel passwordChangeModel)
    {
        return passwordChangeModel.NewPassword.Length >= _minLength;
    }
}

为什么?

因为如果在更改之后我没有看到保留第一个实现的任何好处,它将不会被使用。此外,我们显然不希望任何人在将来错误地使用这个不灵活的代码版本,因为新版本可以执行旧版本所做的一切,但是以更灵活的方式。

总而言之,我将对代码进行重构(打破OCP)并开始使用新版本。

结论

总而言之,我就一个非常广泛的主题提出了我的观点,即开放封闭原则。你必须记住,它只是实现更易理解、更灵活、可维护和更少错误软件的指导原则之一,这不是你必须盲目应用的规则。

它需要一些时间来获得体验,这样可以正确地设计组件。我希望我的例子清楚地说明了OCP的概念,并有助于判断在类似情况下采取何种方法。但是,当您决定在特定情况下哪种解决方案最好并在OCPYAGNI之间找到平衡时,您必须始终使用常识。

在决定扩展或修改现有类时,请始终考虑所有潜在的使用者和子类,这样您就可以针对特定情况采取最佳方法。

 

原文地址:https://www.codeproject.com/Articles/1213327/Csharp-BAD-PRACTICES-Learn-How-to-Make-a-Good-Co

你可能感兴趣的:(CSharp.NET,开放封闭原则)