代码之家  ›  专栏  ›  技术社区  ›  Mecki

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

  •  22
  • Mecki  · 技术社区  · 16 年前

    我将选择Java作为一个例子,大多数人都知道它,尽管其他所有面向对象语言都可以工作。

    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移动。现在我可以为不同类型的汽车细分汽车,那都是汽车,但都是汽车的专业版本。有些可能会以不同的方式移动,这些无论如何都需要覆盖move(),但大多数只会保留继承的移动,因为它们的移动方式就像抽象的父Car一样。现在假设Java中只有接口,只有接口可以相互继承,一个类可以实现接口,但所有类都是最终的,所以没有类可以从任何其他类继承。

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

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

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

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

    ?

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

    8 回复  |  直到 13 年前
        1
  •  11
  •   OscarRyz    16 年前

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

    长答案:

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

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

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

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

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

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

    当你这样做时,“脆弱”类没有问题(或者至少设计者有意识或有威胁),子类只完成设计者允许的部分。

    继承更多的是“专门化”类,就像卡车是汽车的专门版本一样,MosterTruck是卡车的专门版本。

    从汽车创建“计算机鼠标”子类并不是因为它有一个像汽车一样的轮子(滚轮),它会移动,下面有一个轮子只是为了节省代码行。它属于另一个域,将用于其他目的。

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

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

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

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


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

    Effective Java


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

    How To Design A Good API and Why it Matters

    当做。


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

    “当你在一个公共API中有两个类,并且你想让一个成为另一个的子类,就像Foo是Bar的子类一样,问问你自己,每个Foo都是Bar吗?…”

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

        2
  •  8
  •   Laplie Anderson    16 年前

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

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

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

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

        3
  •  4
  •   Xian    16 年前

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

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

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

        4
  •  2
  •   Ben Hoffstein    16 年前

    您可以使用 composition 。在您的示例中,Car对象可能包含另一个名为Drivetrain的对象。汽车的move()方法可以简单地调用其传动系统的drive()方法。传动系类反过来可能包含发动机、变速器、车轮等对象。如果你以这种方式构建你的类层次结构,你可以通过将更简单的部分的不同组合(即重用代码)组合起来,轻松创建以不同方式移动的汽车。

        5
  •  2
  •   Scott Stanchfield    16 年前

    为了使混合/组合更容易,请查看我的注释和注释处理器:

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

    特别是mixins示例:

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

    请注意,如果被委托的接口/类型具有参数化方法(或方法上的参数化类型),则目前不起作用。我正在努力解决这个问题。..

        6
  •  2
  •   Mecki    16 年前

    回答我自己的问题很有趣,但我发现了一件非常有趣的事情: Sather .

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

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

        7
  •  1
  •   erickson    16 年前

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

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

        8
  •  0
  •   RS Conley    16 年前

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

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

    Design Patterns by the Gang of Four