抽象方法和开闭原则

发布于 2024-09-26 12:28:17 字数 1983 浏览 0 评论 0原文

假设我有以下人为的代码:

abstract class Root
{
  public abstract void PrintHierarchy();
}

class Level1 : Root
{
  override public void PrintHierarchy()
  {
    Console.WriteLine("Level1 is a child of Root");
  }
}

class Level2 : Level1
{
  override public void PrintHierarchy()
  {
    Console.WriteLine("Level2 is a child of Level1");
    base.PrintHierarchy();
  }
}

如果我只查看 Level2 类,我可以立即看到 Level2.PrintHierarchy 遵循 开放/封闭原则,因为它自己做一些事情,并调用它正在重写的基本方法。

但是,如果我只查看 Level1 类,它似乎违反了 OCP,因为它没有调用 base.PrintHierarchy ——事实上,在 C# 中,编译器禁止它并显示错误“无法调用抽象基成员”。

Level1 看起来遵循 OCP 的唯一方法是将 Root.PrintHierarchy 更改为空虚拟方法,但这样我就不能再依赖编译器强制派生类了实现PrintHierarchy

我在维护代码时遇到的真正问题是看到许多不调用 base.Whatever()override 方法。如果 base.Whatever 是抽象的,那么很好,但如果不是,那么 Whatever 方法可能是被拉入接口的候选方法,而不是具体的可重写方法-- 或者类或方法需要以其他方式重构,但无论哪种方式,它都清楚地表明设计很差。

如果不记住 Root.PrintHierarchy 是抽象的,或者在 Level1.PrintHierarchy 中添加注释,我是否还有其他选项来快速识别一个类是否像 Level1 那样形成 是否违反 OCP?


评论中有很多很好的讨论,也有一些很好的答案。我很难弄清楚到底要问什么。我认为令我沮丧的是,作为@Jon Hanna 的观点出,有时虚拟方法只是表示“你必须实现我”,而其他时候则意味着“你必须扩展我——如果你无法调用基本版本,你就破坏了我的设计!”但 C# 没有提供任何方法来指示您指的是哪一个,除了抽象或接口显然是“必须实现”的情况。 (除非代码合同中有一些内容,我认为这有点超出了范围)。

但是,如果一种语言确实具有必须实现和必须扩展的装饰器,并且无法禁用它,则可能会给单元测试带来巨大的问题。有这样的语言吗?这听起来很像 design-by-contract,所以如果是的话我也不会感到惊讶以埃菲尔铁塔为例。

最终结果可能是正如@Jordão所说,而且它完全是上下文相关的;但在我仍然接受任何答案之前,我将让讨论继续进行一段时间。

Suppose I have the following contrived code:

abstract class Root
{
  public abstract void PrintHierarchy();
}

class Level1 : Root
{
  override public void PrintHierarchy()
  {
    Console.WriteLine("Level1 is a child of Root");
  }
}

class Level2 : Level1
{
  override public void PrintHierarchy()
  {
    Console.WriteLine("Level2 is a child of Level1");
    base.PrintHierarchy();
  }
}

If I am only looking at the Level2 class, I can immediately see that Level2.PrintHierarchy follows the open/closed principle because it does something on its own and it calls the base method that it is overriding.

However, if I only look at the Level1 class, it appears to be in violation of OCP because it does not call base.PrintHierarchy -- in fact, in C#, the compiler forbids it with the error "Cannot call an abstract base member".

The only way to make Level1 appear to follow OCP is to change Root.PrintHierarchy to an empty virtual method, but then I can no longer rely on the compiler to enforce deriving classes to implement PrintHierarchy.

The real issue I'm having while maintaining code here is seeing dozens of override methods that do not call base.Whatever(). If base.Whatever is abstract, then fine, but if not, then the Whatever method might be a candidate to be pulled into an interface rather than a concrete override-able method -- or the class or method need to be refactored in some other fashion, but either way, it clearly indicates poor design.

Short of memorizing that Root.PrintHierarchy is abstract or putting a comment inside Level1.PrintHierarchy, do I have any other options to quickly identify whether a class formed like Level1 is violating OCP?


There's been a lot of good discussion in the comments, and some good answers too. I was having trouble figuring out exactly what to ask here. I think what is frustrating me is that, as @Jon Hanna points out, sometimes a virtual method simply indicates "You must implement me" whereas other times it means "you must extend me -- if you fail to call the base version, you break my design!" But C# doesn't offer any way to indicate which of those you mean, other than that abstract or interface clearly is a "must implement" situation. (Unless there's something in Code Contracts, which is a little out of scope here, I think).

But if a language did have a must-implement vs. must-extend decorator, it would probably create huge problems for unit-testing if it couldn't be disabled. Are there any languages like that? This sounds rather like design-by-contract, so I wouldn't be surprised if it were in Eiffel, for instance.

The end result is probably as @Jordão says, and it's completely contextual; but I'm going to leave the discussion open for a while before I accept any answers still.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(3

您的好友蓝忘机已上羡 2024-10-03 12:28:17

Root 定义如下: Root 对象有一个 PrintHierarchy 方法。它仅定义了有关 PrintHierarchy 方法的内容。

Level1 有一个 PrintHierarchy 方法。它不会停止使用 PrintHierarchy 方法,因此它绝不会违反开放/封闭原则。

现在,更重要的是:将“PrintHierarchy”重命名为“Foo”。 Level2 遵循还是违反开放/封闭原则?

答案是我们没有任何线索,因为我们不知道“Foo”的语义是什么。因此我们不知道是否应该在方法体的其余部分之后、在其余部分之前、在方法体的中间调用base.Foo,或者根本不调用。

1.ToString() 应该返回“System.ObjectSystem.ValueType1”或“1System.ValueTypeSystem.Object”以在打开/关闭时维持此伪装,还是应该将调用分配给 base。在返回“1”之前将 ToString() 转换为未使用的变量?

显然这些都不是。它应该返回尽可能有意义的字符串。它的基类型尽可能返回有意义的字符串,并且扩展它不会从对其基类型的调用中受益。

开放/封闭原则意味着,当我调用 Foo() 时,我期望发生一些 Fooing,并且当我在 Level1 上调用它时,我期望一些 Level1 适当的 Fooing,当我在 Level2 上调用它时,我期望一些 Level2 适当的 Fooing。 Level2 Fooing 是否应该包含一些 Level1 Fooing 也发生取决于 Fooing 是什么。

base 是一个帮助我们扩展类的工具,而不是一个要求。

Root defines the following: Root objects have a PrintHierarchy method. It defines nothing more about the PrintHierarchy method than that.

Level1 has a PrintHierarchy method. It does not stop having a PrintHierarchy method so it has in no way violated the open/closed principle.

Now, more to the point: Rename your "PrintHierarchy" as "Foo". Does Level2 follow or violate the open/closed principle?

The answer is that we haven't a clue, because we don't know what the semantics of "Foo" are. Therefore we don't know whether base.Foo should be called after the rest of the method body, before the rest, in the middle of it, or not at all.

Should 1.ToString() return "System.ObjectSystem.ValueType1" or "1System.ValueTypeSystem.Object" to maintain this pretence at open/closed, or should it assign the call to base.ToString() to an unused variable before returning "1"?

Obviously none of these. It should return as meaningful a string as it can. Its base types return as meaningful a string as they can, and extending that does not benefit from a call to its base.

The open/closed principle means that when I call Foo() I expect some Fooing to happen, and I expect some Level1 appropriate Fooing when I call it on Level1 and some Level2 appropriate Fooing when I call it on Level2. Whether Level2 Fooing should involve some Level1 Fooing also happening depends on what Fooing is.

base is a tool that helps us in extending classes, not a requirement.

笑红尘 2024-10-03 12:28:17

在没有上下文信息的情况下,您无法仅通过静态查看系统(或类)来确定它是否遵循 OCP。

只有当您知道设计可能有哪些变化时,您才能判断它是否遵循针对这些特定类型的变化的OCP。

没有任何语言结构可以帮助你做到这一点。

一个好的启发式方法是使用某种度量标准,例如 Robert Martin 的不稳定性和抽象性 (pdf) ,或缺乏凝聚力的指标,无法更好地为系统变化做好准备,并有更好的机会遵循 OCP 和 OOD 的所有其他重要原则。

You cannot decide if a system (or class) follows the OCP just by looking at it statically, with no contextual information.

Only if you know what are the likely changes to a design, can you judge whether it follows the OCP for those specific kinds of changes.

There's no language construct that can help you with that.

A good heuristic would be to use some kind of metric, like Robert Martin's instability and abstractness (pdf), or metrics for lack of cohesion to better prepare your systems for change, and to have a better chance of following the OCP and all the other important principles of OOD.

南渊 2024-10-03 12:28:17

OCP 是关于前置条件和后置条件的:“当你只能用较弱的前置条件替换其前置条件,并用更强的后置条件替换其后置条件时”。
我不认为在重写方法中调用 base 会违反它。

OCP is all about preconditions and postconditions: "when you may only replace its precondition by a weaker one, and its postcondition by a stronger one".
I don't think that calling base in overridden methods violates it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文