OOP和完全避免实现继承是可能的吗?

2022-09-02 03:52:06

我将选择Java作为一个例子,大多数人都知道它,尽管其他所有OO语言也在工作。

与许多其他语言一样,Java具有接口继承和实现继承。例如,一个Java类可以从另一个类继承,并且每个具有实现的方法(假设父级不是抽象的)也是继承的。这意味着接口是继承的,也是此方法的实现。我可以覆盖它,但我不必这样做。如果我不覆盖它,我就继承了实现。

但是,我的类也可以“继承”(不是Java术语)只是一个接口,而无需实现。实际上,接口在Java中实际上是以这种方式命名的,它们提供接口继承,但不继承任何实现,因为接口的所有方法都没有实现。

现在有这篇文章,说继承接口比实现更好,你可能喜欢阅读它(至少是第一页的前半部分),这很有趣。它避免了像脆弱的基类问题这样的问题。到目前为止,这一切都很有意义,文章中说的许多其他事情对我来说也很有意义。

让我烦恼的是,实现继承意味着代码重用,这是OO语言最重要的属性之一。现在,如果Java没有类(就像Java教父James Gosling根据本文所希望的那样),它可以解决实现继承的所有问题,但是您将如何使代码重用成为可能呢?

例如,如果我有一个类 Car,而 Car 有一个 move() 方法,这使得 Car 移动。现在我可以将Car归类为不同类型的汽车,这些都是汽车,但都是汽车的专用版本。有些可能以不同的方式移动,无论如何,这些都需要覆盖move(),但大多数会简单地保留继承的移动,因为它们的移动方式与抽象的父Car相同。现在假设Java中只有接口,只有接口可以相互继承,类可以实现接口,但所有类始终是最终的,因此任何类都不能从任何其他类继承。

当你有一个 Interface Car 和一百个 Car 类时,你如何避免需要为每个类实现一个相同的 move() 方法?除了实现继承之外,OO世界中还存在哪些代码重用概念?

有些语言有Mixins。Mixins是我问题的答案吗?我读过关于它们的文章,但我无法想象Mixins在Java世界中将如何工作,以及它们是否真的能在这里解决问题。

另一个想法是,有一个类只实现Car接口,我们称之为抽象Car,并实现move()方法。现在其他汽车也实现了Car接口,它们在内部创建了一个抽象Car的实例,并通过在其内部抽象Car上调用move()来实现自己的move()方法。但是,这难道不是在浪费资源(一个方法只调用另一个方法 - 好吧,JIT可以内联代码,但仍然)并使用额外的内存来保留内部对象,你甚至不需要实现继承?(毕竟每个对象需要更多的内存,而不仅仅是封装数据的总和)对于程序员来说,编写像这样虚拟的方法也不是很尴尬吗?

public void move() {
    abstractCarObject.move();
}

?

任何人都可以想象一个更好的想法,如何避免实现继承,并且仍然能够以简单的方式重用代码?


答案 1

简短的回答:是的,这是可能的。但你必须故意这样做,而不是偶然的(使用最终的,抽象的和设计,并考虑到继承等。

长答案:

好吧,继承实际上不是为了“代码重用”,而是为了类“专业化”,我认为这是一种误解。

例如,从Vector创建堆栈是一个非常糟糕的主意,仅仅因为它们是相似的。或者来自 HashTable 的属性,因为它们存储值。请参阅[有效]。

“代码重用”更像是OO特征的“业务视图”,这意味着您的对象很容易在节点之间分发;并且是可移植的,并且没有以前编程语言一代的问题。这已被证明是半个严谨的。我们现在有可以轻松分发的库;例如,在java中,jar文件可以在任何项目中使用,从而节省数千小时的开发时间。OO在可移植性等方面仍然存在一些问题,这就是现在WebServices如此流行的原因(就像在CORBA之前一样),但这是另一个线程。

这是“代码重用”的一个方面。另一个是有效的,与编程有关的那个。但在这种情况下,不仅仅是为了“保存”代码行和创建脆弱的怪物,而是在设计时考虑到继承。这是前面提到的书中的第17项;第17项:设计和继承文件,否则禁止继承。请参阅 [有效]

当然,你可能有一个汽车类和大量的子类。是的,你提到的关于Car接口,AbstractCar和CarImplementation的方法是正确的方法。

你定义了汽车应该遵守的“合同”,并说这些是我在谈论汽车时期望拥有的方法。抽象的 car,它具有每个 car 的基本功能,但离开并记录子类负责处理的方法。在java中,您可以通过将方法标记为抽象来执行此操作。

当你以这种方式进行时,“脆弱”类没有问题(或者至少设计师是有意识的或威胁的),子类只完成了设计师允许的那些部分。

继承更多的是“专业化”类,同样,卡车是汽车的专用版本,而MosterTruck是卡车的专用版本。

从汽车创建“ComputerMouse”子循环并不神圣,仅仅因为它有一个像汽车一样的车轮(滚轮),它会移动,并且下面有一个轮子只是为了节省代码行。它属于不同的域,它将用于其他目的。

防止“实现”继承的方法是从一开始就在编程语言中,您应该在类声明上使用final关键字,这样就可以禁止子类。

如果子类化是故意的,那么它就不是邪恶的。如果做得不小心,它可能会成为一场噩梦。我想说的是,你应该尽可能从私人和“最终”开始,如果需要,让事情变得更加公开和可扩展。这在“如何设计好的API以及为什么它很重要”的演示文稿中也得到了广泛的解释,请参阅[好的API]

继续阅读文章,随着时间的推移和练习(以及很多耐心),这件事会变得更加清晰。虽然有时您只需要完成工作并复制/粘贴一些代码:P。这没关系,只要你先试着把它做好。

以下是Joshua Bloch(以前在Sun工作,现在是Java的核心,现在为Google工作)的参考资料


[有效]有效的Java。绝对是非初学者应该学习,理解和练习的最好的java书。必须有。

有效的Java


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

如何设计一个好的API以及为什么它很重要

问候。


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

“当你在一个公共API中有两个类,你想让一个类成为另一个的子类,就像Foo是Bar的子类一样,问问你自己,Every Foo是Bar吗?..."

在前一分钟,它谈到了“代码重用”,同时谈到了TimeTask。


答案 2

大多数反对继承的例子的问题是,这个人错误地使用继承,而不是继承失败,无法正确抽象。

在您发布链接的文章中,作者使用 Stack 和 ArrayList 展示了继承的“破碎性”。该示例存在缺陷,因为堆栈不是 ArrayList,因此不应使用继承。该示例与字符串扩展字符或 PointXY 扩展编号一样有缺陷。

在扩展类之前,应始终执行“is_a”测试。既然你不能说每个堆栈都是一个ArrayList而不以某种方式出错,那么你不应该被淹没。

Stack 的协定不同于 ArrayList(或 List)的协定,堆栈不应该继承不关心的方法(如 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)而不是继承。

继承不是坏的,不好的继承是坏的。