面向对象编程(OOP)和完全避免实现继承是可能的吗?

22
我将选择Java作为示例,大多数人都知道它,尽管其他每个面向对象语言也可以工作。
像许多其他语言一样,Java具有接口继承和实现继承。例如,Java类可以从另一个类继承,并且在那里具有实现的每个方法(假设父类不是抽象的)也会被继承。这意味着接口被继承以及此方法的实现。我可以覆盖它,但我不必这样做。如果我不覆盖它,则已继承该实现。
但是,我的类还可以“继承”(不是Java术语),只是一个没有实现的接口。实际上,在Java中,接口确实以这种方式命名,它们提供接口继承,但不继承任何实现,因为接口的所有方法都没有实现。
现在有这篇文章,说继承接口比实现更好,您可能想阅读它(至少是第一页的前半部分),它非常有趣。它避免了脆弱的基类问题等问题。到目前为止,所有这些都很有道理,文章中说的许多其他事情对我来说也很有道理。
让我感到困扰的是,实现继承意味着代码重用,这是面向对象语言最重要的属性之一。现在,如果Java没有类(就像Java教父James Gosling根据这篇文章的说法所希望的那样),它解决了实现继承的所有问题,但是您如何使代码重用成为可能?
例如,如果我有一个名为Car的类,它有一个方法move(),使得车辆移动。现在我可以为不同类型的车辆子类化Car,它们都是车辆,但都是Car的专业版本。一些可能以不同的方式移动,这些需要覆盖move(),但大多数只需保留继承的move(),因为它们像抽象父级Car一样移动。现在假设Java中只有接口,只有接口可以从彼此继承,类可以实现接口,但所有类都是final的,因此没有类可以从任何其他类继承。
当您拥有一个名为Interface Car和100个Car类时,如何避免需要为每个类实现相同的move()方法?除了实现继承之外,还存在哪些代码重用概念?
一些语言有Mixins。 Mixins是我的问题的答案吗?我读过它们,但我无法想象Mixins在Java世界中如何工作,以及它们是否真的能解决这个问题。
另一个想法是有一个仅实现Car接口的类,让我们称其为AbstractCar,并实现move()方法。现在其他车辆也实现了Car接口,内部它们创建了AbstractCar的实例,并通过调用其内部抽象Car上的move()方法来实现自己的move()方法。但这不会浪费资源(一个方法只调用另一个方法-好吧,JIT可以内联代码,但仍然)并使用额外的内存来保留内部对象吗? (毕竟,每个对象都需要比封装数据的总和更多的内存)此外,对于程序员来说,编写像这样的虚拟方法是否很尴尬?
public void move() {
    abstractCarObject.move();
}

?

有没有更好的想法可以避免实现继承,但仍然能够轻松地重用代码?


我个人不喜欢称之为 "接口继承",而更喜欢称之为 "接口实现"。这是 Java 语言中一个我真正喜欢的地方,两个不同的概念分别有相应的名称。 - Trap
8个回答

11

简短回答:是的,这是可能的。但您必须有意识地去做,而不是碰巧(例如使用final、abstract和设计时考虑继承等方法)。

详细回答:

实际上,继承并不是为了“代码重用”,而是为了类“特殊化”,我认为这是一种误解。

例如,仅仅因为它们相似,就从Vector创建Stack是非常糟糕的想法。或者从HashTable中创建属性,仅仅因为它们存储值。请参见[Effective]。

“代码重用”更多地是从OO特性的“商业视角”来看待的,意味着您的对象可以轻松地在节点之间分配,并且具有移植性,不会出现以前编程语言生成的问题。这已经被证明是半正确的。我们现在拥有可以轻松分发的库;例如,在Java中,JAR文件可以在任何项目中使用,节省数千小时的开发时间。 OO仍然存在一些与可移植性等问题相关的问题,这就是为什么现在WebServices如此流行(以前是CORBA),但这是另一个主题。

这是“代码重用”的一个方面。另一个有效的方面是与编程有关的方面。但是,在这种情况下,不仅仅是为了“节省”代码行并创建脆弱的怪物,而是要考虑继承的设计。这是先前提到的书中的第17项: Item 17:为继承设计并记录文档,否则禁止使用。请参见[Effective]

当然,您可能会有一个Car类以及大量的子类。是的,您所提到的关于Car接口、AbstractCar和CarImplementation的方法是正确的方式。

您定义了Car应遵守的“协议”,并说这些是我在谈论汽车时期望拥有的方法。抽象汽车具有每个汽车基本功能的基本功能,但是留下并记录了子类负责处理的方法。在Java中,您可以通过将方法标记为抽象来实现此目的。

当您按照这种方式进行操作时,就没有“脆弱”的类的问题(或者至少设计师意识到了威胁),而子类只完成设计者允许的部分。

继承更多地是“专业化”类,在同样的方式中,卡车是汽车的专业化版本,怪兽卡车是卡车的专业化版本。

如果只是因为它像汽车一样有一个轮子(滚轮),移动并且下方有一个轮子,就从汽车创建“计算机鼠标”子类是没有意义的。它属于不同的领域,并将用于其他用途。

防止“实现”继承的方法在编程语言中开始时使用final关键字在类声明上,并且通过这种方式,你正在禁止子类。

如果目的明确,则子类化不是邪恶的。如果随意进行,它可能会变成噩梦。我认为你应该尽可能以私有和“final”的方式开始,如果需要的话,可以将事情变得更加公共和可扩展。这也在演示文稿“How to design good API's and why it matters”中广泛解释。请参见[Good API]

继续阅读文章,并随着时间和实践(以及大量耐心),这个问题会变得更清晰。虽然有时你只需要做一些工作并复制/粘贴一些代码:P。只要你首先尝试做好,这就可以了。

这里是来自Joshua Bloch(曾在Sun核心Java工作,现在在Google工作)的两个参考文献


[Effective] 《Effective Java》。非初学者应该学习、理解和实践的最好的Java书籍。必备品。

Effective Java


[Good API]演示文稿,讨论API设计、可重用性和相关主题。它有点冗长,但每一分钟都值得。

How To Design A Good API and Why it Matters

问候。


更新:看看我给你发送的视频链接的第42分钟。它谈到了这个主题:

当你在公共API中有两个类,并且想让其中一个成为另一个的子类,比如Foo是Bar的子类,你要问自己,每个Foo都是Bar吗?...

同时,在上一分钟里它谈到了"代码重用",并提到了TimeTask。


说真的,我不明白为什么卡车应该继承自汽车 :) - Trap
2
也许卡车和汽车应该继承自汽车。 :-) - StriplingWarrior

8
大多数反对继承的例子都是因为人们使用继承不当,而不是继承本身存在问题。在您发布的文章中,作者使用 Stack 和 ArrayList 展示了继承的“破坏性”。这个例子有缺陷,因为 Stack 不是 ArrayList,因此不应该使用继承。这个例子就像 String 扩展 Character,或 PointXY 扩展 Number 一样有缺陷。在您扩展类之前,应始终进行“is_a”测试。由于不能说 Every Stack is an ArrayList 而不会出现错误,因此不应该继承。

Stack 的契约与 ArrayList(或 List)的契约不同,Stack 不应该继承它不关心的方法(例如 get(int i) 和 add())。事实上,Stack 应该是一个包含以下方法的接口:
interface Stack<T> {
   public void push(T object);
   public T pop();
   public void clear();
   public int size();
}

像ArrayListStack这样的类可以实现Stack接口,在这种情况下,应使用组合(具有内部ArrayList),而不是继承。

继承本身不是坏事,坏的继承才是坏的。


4
你还可以使用组合和策略模式。链接文本
public class Car
{
  private ICar _car;

  public void Move() {
     _car.Move();
  }
}

这比使用基于继承的行为更加灵活,因为它允许您在运行时进行更改,通过根据需要替换新的汽车类型。

2
您可以使用 组合。 在您的示例中,Car对象可能包含另一个名为Drivetrain的对象。汽车的move()方法可以简单地调用其驱动器的drive()方法。 Drivetrain类可以反过来包含像Engine,Transmission,Wheels等对象。如果您以这种方式构建类层次结构,则可以通过组合不同的简单部件(即重用代码)轻松创建以不同方式移动的汽车。

2

这是一些非常有趣的东西,你有。我会仔细研究它 :) - Mecki

2
很有趣地回答自己的问题,但我发现了一些非常有趣的东西:Sather
它是一种没有实现继承的编程语言!它知道接口(称为没有实现或封装数据的抽象类),接口可以相互继承(实际上它们甚至支持多重继承!),但类只能实现接口(抽象类,可以实现任意数量),不能从另一个类继承。但是,它可以“包含”另一个类。这是一个委托概念。被包含的类必须在您的类的构造函数中实例化,并在您的类被销毁时被销毁。除非覆盖它们的方法,否则您的类也会继承它们的接口,但不会继承它们的代码。相反,创建方法只是将调用转发到包含对象的同名方法的您的方法。包含对象和封装对象之间的区别在于您不必自己创建委托转发,它们不存在作为独立对象的情况,您可以传递它们,它们是您对象的一部分,并且与您的对象一起生存和死亡(或更严谨地说:您对象和所有包含对象的内存是通过单个alloc调用创建的,相同的内存块,您只需要在构造函数调用中初始化它们,而使用真正的委托时,每个这些对象都会导致自己的alloc调用,具有自己的内存块,并且完全独立于您的对象)。
该语言不太美观,但我喜欢它背后的理念 :-)。

1
继承对于面向对象的语言并非必需。
以JavaScript为例,它甚至比Java更加面向对象。JavaScript没有类,只有对象。通过将现有方法添加到对象中来重用代码。JavaScript对象本质上是一个将名称映射到函数(和数据)的集合,其中映射的初始内容由原型确定,并且可以在给定实例上动态添加新条目。

0

你应该阅读《设计模式》。你会发现接口对于许多有用的设计模式至关重要。例如,抽象不同类型的网络协议将具有相同的接口(对调用它的软件而言),但由于每种协议的不同行为,代码重用很少。

对于一些算法来说,它们可以让你眼前一亮,展示如何将编程中的无数元素组合在一起以执行某些有用的任务。设计模式也是这样,它向你展示了如何以一种方式组合对象以执行有用的任务。

《设计模式》(四人帮)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接