65.9K
CodeProject 正在变化。 阅读更多。
Home

不受欢迎的继承

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2013年7月28日

CPOL

4分钟阅读

viewsIcon

12802

继承与代码复用

引言

代码复用是谈到继承时经常出现的话题。论坛、博客和维基百科充斥着许多关于继承的观点和伪定义,听起来或多或少像这样:“继承是一种软件可复用性”、“继承的本质是可复用性”、“继承是面向对象编程中复用代码的方式”等等。然而,继承也与古老的“是一种”关系有关,一个合理的问题是:基类真的是复用代码的正确方式吗?

背景

在审查代码时,我经常能发现开发者误用继承的情况,他们被网络上充斥的这些自制定义所误导。让我们从头开始:通常有两种应用继承的方式

  1. 向前:存在一个抽象类或具体类,我们想要扩展或覆盖其行为。
  2. 向后:存在两个或多个具体类,我们意识到可以从它们中提取一个公共基类。

根据我的经验,向后的过程是棘手的,而且最容易被滥用。
当着眼于一些设计不那么好的类——通常具有许多职责——而它们恰好具有相似的方法,即使不是完全相同的方法时,问题就开始了。

例如,假设某个类 A 和类 B 都有一个通用的方法,名为 ReadTextFile ,该方法将文本文件读入一个 string (令人惊讶的是)。现在,没有人喜欢看到或维护同一功能的多个副本,尤其是在逻辑上可重用的情况下。我们希望我们心爱的 ReadTextFile 在一个且仅有的一个地方,任何需要它的类都可以调用它。
一个地方,好的,但在哪里呢?
一种方法是将其放入某种文本文件辅助类中,可能是在一个实用程序库中,然后将其注入到任何需要使用它的类中。不幸的是,许多开发人员使用继承来处理这些情况,并为 A 和 B 创建一个基类来共享公共代码。当类 C 需要读取文本文件时,它们会让类 C 继承自基类。

那么,这有什么问题呢?嗯,实际上有很多问题。

一个问题是流行的面向对象语言,例如 C# 或 Java,不支持多重继承。如果类 C 已经有自己的基类,那么我们就无法复用 ReadTextFile。但即使我们很幸运,类 C 还没有基类,如果 A 和 C 还有另一个共同的方法,比如说 AuthenticateUser 怎么办?我们是否也要将这个共同的方法扫到基类的地毯下,即使 B 并不关心它?我见过很多开发人员这样做。他们的子类可能看起来干净且符合单一职责原则,但它们并非如此。你只需要查看基类下面,就能找到隐藏的坏实践堆:一堆混合的关注点。

毕竟,继承的混乱仍然是混乱。

下面是类图的样子

在这一点上,一些开发人员意识到当一个类被迫继承一个不需要的方法时是不好的,并开始构建像这样的继承层次结构

让我们看看图表,检查一下实现了什么。好的一面是,现在 B 不再看到 AuthenticateUser 了。坏的一面是,现在我们又多了一个类,并且继承深度增加了。如果类 D 只需要 AuthenticateUser 而不需要 ReadTextFile 怎么办?我的头已经像 Regan 一样开始旋转了。

我们依靠继承来复用代码,最终得到一个不太可重用且过于复杂的层次结构。

但是,如果使用支持多重继承的面向对象语言会发生什么呢?尽管从技术上讲,我们现在可以实现可重用性,但这仍然不是一个好主意。事实上,如果一个类继承了多个职责,那么它就具有多个职责,因此更难以理解、维护、调试和测试。在我们的例子中,例如,如果我们想更改系统以在类 A 中使用 AuthenticateUser 的不同实现,我们将不得不更改现有代码。如果我们依赖组合而不是继承,我们所要做的就是编写一个具有相同接口的新身份验证类,并仅更新工厂(或 IOC 容器注册)。重要的区别是,我们不必更改任何现有的业务代码。

使用继承进行代码复用的好处范围有限,应该侧重于每个类的特定职责和核心业务。当然,代码复用并不是采用继承而不是组合的主要驱动原因,而且在大多数情况下,组合是一个更明智的选择。

祝您编码愉快!

© . All rights reserved.