【问题标题】:Is OOP & completely avoiding implementation inheritance possible?OOP & 完全避免实现继承是可能的吗?
【发布时间】:2008-09-23 20:48:36
【问题描述】:

我将选择 Java 作为示例,大多数人都知道,尽管其他所有 OO 语言都可以正常工作。

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

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

现在有这个article, saying it's better to inherit interfaces than implementations,你可能喜欢阅读它(至少是第一页的前半部分),它非常有趣。它避免了像fragile base class problem 这样的问题。到目前为止,这一切都很有意义,文章中所说的许多其他内容对我来说也很有意义。

让我感到困惑的是,实现继承意味着代码重用,这是面向对象语言最重要的属性之一。现在如果 Java 没有类(就像本文所希望的 Java 教父 James Gosling),它解决了实现继承的所有问题,但是你如何让代码重用成为可能呢?

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

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

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

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

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

?

谁能想出一个更好的主意,即如何避免实现继承并仍然能够以简单的方式重用代码?

【问题讨论】:

  • 我不喜欢称它为“接口继承”而是“接口实现”。这是我真正喜欢 Java 语言的一件事,两个不同的概念因此而得名。

标签: java oop


【解决方案1】:

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

长答案:

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

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

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

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

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

您定义了 Car 应该遵守的“合同”,并说这些是我在谈论汽车时所期望的方法。抽象汽车,它具有每辆汽车的基本功能,但留下并记录子类负责处理的方法。在 java 中,您可以通过将方法标记为抽象来做到这一点。

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

继承更多地是为了“专门化”类,就像 Truck 是 Car 的专门版本,MosterTruck 是 Truck 的专门版本一样。

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

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

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

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

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


[有效的] 有效的Java。绝对是非初学者应该学习、理解和实践的最佳 Java 书籍。必须有。

Effective Java


[Good API]介绍 API 的设计、可重用性和相关主题的演讲。 它有点长,但每一分钟都值得。

How To Design A Good API and Why it Matters

问候。


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

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

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

【讨论】:

  • 说真的,我看不出卡车应该如何从汽车继承 :)
  • 也许卡车和汽车应该继承自汽车。 :-)
【解决方案2】:

大多数反对继承的例子的问题是人们错误地使用继承的例子,而不是继承未能正确抽象的例子。

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

在扩展类之前,您应该始终执行“is_a”测试。既然你不能说 Every Stack 是一个 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)而不是继承。

继承不是坏事,继承不好就是坏事。

【讨论】:

    【解决方案3】:

    你也可以使用组合和策略模式。link text

    public class Car
    {
      private ICar _car;
    
      public void Move() {
         _car.Move();
      }
    }
    

    这比使用基于继承的行为灵活得多,因为它允许您在运行时根据需要替换新的 Car 类型来进行更改。

    【讨论】:

      【解决方案4】:

      您可以使用composition。在您的示例中, Car 对象可能包含另一个名为 Drivetrain 的对象。汽车的 move() 方法可以简单地调用它的传动系统的 drive() 方法。反过来,Drivetrain 类可以包含 Engine、Transmission、Wheels 等对象。如果您以这种方式构建类层次结构,您可以通过将更简单的部分(即重用代码)。

      【讨论】:

        【解决方案5】:

        为了让 mixins/composition 更容易,看看我的 Annotations 和 Annotation Processor:

        http://code.google.com/p/javadude/wiki/Annotations

        特别是 mixins 示例:

        http://code.google.com/p/javadude/wiki/AnnotationsMixinExample

        请注意,如果被委派的接口/类型具有参数化方法(或方法上的参数化类型),则它目前不起作用。我正在努力……

        【讨论】:

        • 这是一些非常有趣的东西,你有。我会详细看一下:)
        【解决方案6】:

        回答我自己的问题很有趣,但我发现这里很有趣:Sather

        它是一种完全没有实现继承的编程语言!它知道接口(称为没有实现或封装数据的抽象类),并且接口可以相互继承(实际上它们甚至支持多重继承!),但是一个类只能实现接口(抽象类,尽可能多),它不能从另一个类继承。然而,它可以“包含”另一个类。这是一个委托概念。包含的类必须在类的构造函数中实例化,并在类被销毁时被销毁。除非您覆盖它们拥有的方法,否则您的类也会继承它们的接口,但不会继承它们的代码。相反,创建的方法只是将对您的方法的调用转发到包含对象的同名方法。包含对象和仅封装对象之间的区别在于,您不必自己创建委托转发,它们也不作为可以传递的独立对象存在,它们是对象的一部分,与您的对象一起生死存亡。对象(或更专业地说:对象和所有包含的对象的内存是通过单个分配调用创建的,相同的内存块,您只需要在构造函数调用中初始化它们,而在使用真正的委托时,这些对象中的每一个都会导致自己的 alloc 调用,拥有自己的内存块,并且完全独立于您的对象)。

        语言不是那么优美,但我喜欢它背后的想法:-)

        【讨论】:

          【解决方案7】:

          面向对象的语言不需要继承。

          考虑一下 Javascript,可以说它比 Java 更加面向对象。没有类,只有对象。通过将现有方法添加到对象来重用代码。 Javascript 对象本质上是名称到函数(和数据)的映射,其中映射的初始内容由原型建立,新条目可以动态添加到给定实例。

          【讨论】:

            【解决方案8】:

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

            因为一些算法在展示如何将编程的无数元素组合在一起来完成一些有用的任务时令人大开眼界。设计模式对对象做同样的事情。向您展示如何组合对象以执行有用的任务。

            Design Patterns by the Gang of Four

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 2021-04-26
              • 1970-01-01
              • 2018-03-25
              • 2015-02-12
              • 2019-02-04
              • 1970-01-01
              • 2011-01-15
              相关资源
              最近更新 更多