目录
介绍
定义和历史
我如何理解OCP?
我如何理解OCP?
3个级别
当代码关闭时
预测未来和YAGNI
让我们编码
不好的例子
更好的方法
SOLID恰当的结合在一起
更多例子
修改或扩展
什么时候我会扩展
例1
什么时候我会修改
例2
例3
结论
本文是关于SOLID原则中的开放封闭原则(OCP),您不必阅读我以前的文章来理解它,但我鼓励您这样做。:)
我写这篇文章的动机是这个原则存在很大的混乱,并且对它有许多不同的解释。
你可以在这里找到我之前的四篇文章:
这个原则也让我自己感到困惑,这就是为什么我深入了解这个话题,并在此提出我的发现和想法。
在我看来,除了里氏替换原则之外,它是SOLID中最难解释的(完全理解)。
根据我的经验,我可以说这很令人困惑的,即使是高级工程师和大多数开发人员也只知道它的定义,而不会深入了解它为何以及何时是有用的。
这可能导致盲目地应用此规则,这可能使代码库变得奇怪。
我还在许多互联网论坛上看到很多人都在提出具体情况,并问“它是否违反了OCP?”等待。 这些问题有时会得到解答,但是:
在我看来,OCP仍然是个谜。
在本文中,我将尝试:
与往常一样,我将使用一个真实世界的例子,首先显示一个坏的代码(在我看来),然后提出一个更好的解决方案。
请记住,提供代码示例,我没有提供端到端功能实现,我只提出了一种方法。
要理解本文,您需要:
Bertrand Meyer在“面向对象的软件构建”一书中介绍了开放封闭原则。
它说:
“软件实体(类,模块,函数等)应该是对扩展开放的,但是对修改关闭”。
解决这个问题的建议方法是继承。
这意味着您应该通过创建子类来扩展组件,而不是修改现有代码。
Robert C. Martin介绍了一种解决这个问题的新方法——抽象。
因此,如果您将编程用于抽象,您的代码将更灵活地适应未来的更改,因为您可以替换或添加新实现而无需修改类的使用者,因为使用者只会看到抽象(abstract类或interface)而不是具体实现。
另外一件事是OCP在主要规则中添加了一个例外,即bug修复。因此,如果组件中存在错误,则允许修改代码以修复此错误。
我对这个原理的理解是它完全是关于设计函数,类和模块。
这是什么意思?
那么,IMO作为一名经验丰富的开发人员,在创建新功能的同时,您应该尝试预测未来。
您应该确定组件中将来可能需要进行一些修改的位置(需求更改)而这些修改将会出现问题(重新设计模块,很有可能在现有功能中引入错误)。
一旦确定了这些地方,就应该找到一个解决方案,使您的代码能够灵活应对这些变化。
所以...
IMO,这并不意味着:
“你永远不应该修改你现有的类”,
但:
“当您想要扩展功能时,您应该以一种您不必这样做的方式设计您的模块”。
但是,我们正在设计符合OCP的代码,以便能够扩展代码而不是修改代码。因此修改设计为可扩展的代码没有多大意义。这就是为什么在设计阶段遵循OCP与未来应用需求变更的方式严格相关,理想情况下应该通过扩展而不是通过修改来完成。
所以IMO,你可以说,违反OCP将是不可扩展代码的实现以及对现有代码的修改。我稍后会解释我认为第二个什么时候可以。
您可以使用许多技术编写符合OCP的代码,例如:
完整的解决方案通常包含它们的组合。你应该使用哪种,取决于具体情况。在我的例子中,我将使用抽象和依赖注入。
为什么不编写有效的代码并在需要时修改它而没有这种灵活性?
因为,现有代码中的每个修改:
如果您的代码已经对修改关闭并对扩展开放,则无需在新需求出现时修改现有代码,您只需添加新组件并使用配置开始使用它们。
在该原则的原始定义中,据说该规则应在3个层面得到尊重:
虽然函数和类级别的名称很清楚,但我对“模块级别”的理解是,在这种情况下,“模块”是一组类和它们之间的依赖关系,它创建了一些功能。
所以它比程序集更抽象。我看到了对“模块”级别的许多不同解释,所以我想清楚地说,在我看来,它并没有说你不应该在你现有的程序集中添加新的代码。
我将在我的示例中显示在这三个级别中的每个级别上违反OCP。
在我看来,组件在第一次部署到生产时就会关闭。我假设在发布之前,它仍处于开发阶段。
显然,上面写的所有内容都假设我们生活在一个理想的世界中,开发人员是能够预测将来会发生的一切事情的透视者。
在现实世界中,我们永远不会预测可能来自业务的所有需求变化。
我们甚至不应该尝试,因为它可能导致过度工程,这也是不好的。因此,我们最终可以得到极其复杂的系统,我们永远不需要使用它的灵活性/复杂性。
但是,如果在开发某些功能时,我们发现该功能的本质是它需要一些扩展或更改,而对此的需求只是时间问题,我们应该以一种我们将要设计的方式来设计我们的代码。能够通过添加新组件来实现此扩展,而不是修改现有组件。
这是最困难的部分。我们需要在开放封闭原则和YAGNI(你不需要它)原则之间找到平衡点。
作为代码示例,我选择了一个验证密码的模块。
最初的要求是密码:
下面我提出一个实现,它违反了所有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个类:
由于密码验证规则很可能会随着时间的推移而发生变化以提高安全性,因此我们的模块应该可以进行扩展。由于我们不想在扩展它时破坏现有功能,因此也应该对修改关闭。
以上示例明显违反了所有3个级别的OCP:
private const int _minLength = 10;
有什么问题?
如果只有一个PasswordValidator类的使用者 (或者所有使用者需要使用最小长度为10)并且没有PasswordValidator类的子类,则没有问题。
但是..
如果PasswordValidator类被许多使用者使用,您将改变所有使用者的行为。也许在2个应用程序中,它应该仍然是8,在第三个应用程序中,它应该是10.
如果PasswordValidator 类是其他类的基类,您也将改变它们的行为。这可能不是预期的行为。
此外,如果您将考虑需求“不能与用户过去设置的任何密码相同”,您可以假设它可能会更改为:
“不能与用户设置的任何X最后密码相同 “,其中x可以是任何东西:D
PasswordChangeModel类不允许实现此功能,因为我们没有关于密码更改顺序的信息。
如您所见,我们的代码明显违反了OCP。
如果我们想创建一个可重用的库来验证我们公司或许多公司的许多应用程序中的密码,并使用NuGet进行分发,上述实现很可能会在将来造成麻烦。
为什么?
因为,你不能说:
这就是为什么我们必须保持向后兼容性以避免改变使用我们库的应用程序的行为。
但是,如果我们的密码验证模块是一个简单的库,我们可以拥有PasswordValidator类的多个使用者和子类,在扩展功能时我们不希望改变它们的行为。
好吧,为了证明违反OCP,我们试着处理一些可能会发生的需求变化。
现在,作为现有验证的补充,我们必须添加一条新规则:
由于我们不关心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 类,这将影响所有使用者和派生类。
您可以考虑许多不同的规则,我们需要在几个需求变更之后实现这些规则:
我们的代码不适合处理任何扩展或修改。
为什么?
因为开发人员只是直接实现了需求中所说的内容,而没有考虑太多。保持简单,他创建了不可扩展的代码。
让我们尝试更好的方法!
让我们从一开始就采用更好的方法开始:
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对象中拥有过去每个密码的创建日期。很可能需要改变需求的情况下:
至:
我们已准备好所有信息,我们只需要添加新规则。
接下来是2个接口的介绍:
第一个是使用者和PasswordValidation类之间的抽象层。由于引入它,我们在模块级别上删除了OCP违规(同时紧密耦合)。
我将在稍后的使用者代码中显示它。
第二个接口是PasswordValidator和PasswordValidationRules之间的抽象:
如您所见,每个规则逻辑都被移动到一个单独的类中,并且这些类正在实现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类不同的实现来自由地更改使用者密码验证功能所使用的功能。我们的模块可以进行扩展,并在密码验证时对修改关闭。
在我解释模块的配置之前,让我们尝试实现一个新的需求(新密码验证规则):
再次,但在一个新的代码库中。
我们真正需要做的是添加一个新的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容器中的一个来设置密码验证模块的配置。下面是使用Ninject(ninject模块配置)的示例:
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 API的WhenInjectedInto方法,即使在同一个应用程序中,我们也可以拥有不同的配置(每个使用者的一套规则)。对于不熟悉Ninject的人,这里有一个说明文档的链接:Ninject文档
第一个使用者将使用密码验证器和3条规则:
而第二个使用者将使用4个验证规则:
显然,您可以在应用程序中手动实现依赖注入,这取决于您的需求。
所以当我们想要:
即使是这样灵活的代码也无法满足各种要求,但在我的意见中进一步提高灵活性将是过度工程,并将违反YAGNI。在这里,我想说我们达到了平衡。
你可以注意到,在这个例子中,我也在使用:
来自SOLID原则。
只有SOLID规则的组合才能让您编写更易理解,更灵活且可维护的软件。
如果你想看到另一个应用OCP的例子,请阅读我的文章,其中我描述了为什么switch- case语句破坏OCP以及如何将其重新编写为符合OCP的代码:
作为符合OCP标准的模块设计的自然延续是应用需求变更,在本段中,我将展示具体的例子,当我这样做时我会/不会修改原始代码。
让我们假设我们已经实现了前一段中提到的密码验证库(更好,更灵活的版本)。我们决定将其作为NuGet包分享。
我们假设在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。
现在请忘记原始示例,并想象这是您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个PasswordMinLengthRule类的调用者,这是最初的实现:
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的概念,并有助于判断在类似情况下采取何种方法。但是,当您决定在特定情况下哪种解决方案最好并在OCP和YAGNI之间找到平衡时,您必须始终使用常识。
在决定扩展或修改现有类时,请始终考虑所有潜在的使用者和子类,这样您就可以针对特定情况采取最佳方法。
原文地址:https://www.codeproject.com/Articles/1213327/Csharp-BAD-PRACTICES-Learn-How-to-Make-a-Good-Co