Google提出了一个类似的问题,并给出了我认为很好的答案。我在下面引用了它。
在我链接的库克文章中,还解释了另一个潜伏的区别。
对象不是实现抽象的唯一方法。并非所有事物都是对象。对象实现某些人称为过程数据抽象的东西。抽象数据类型实现了另一种抽象形式。
当考虑二进制方法/函数时,会出现一个关键的区别。使用过程数据抽象(对象),您可以为Int set接口编写如下代码:
interface IntSet {
void unionWith(IntSet s);
...
}
现在考虑IntSet的两种实现,例如一种由列表支持的方法,以及一种由更有效的二叉树结构支持的方法:
class ListIntSet implements IntSet {
void unionWith(IntSet s){ ... }
}
class BSTIntSet implements IntSet {
void unionWith(IntSet s){ ... }
}
请注意,unionWith必须采用IntSet参数。不是更具体的类型,如ListIntSet或BSTIntSet。这意味着BSTIntSet实现不能假定其输入是BSTIntSet并使用该事实来给出有效的实现。(它可以使用一些运行时类型信息进行检查,如果可以的话,可以使用效率更高的算法,但是仍然可以将其传递给ListIntSet,并且必须使用效率较低的算法)。
将此与ADT进行比较,您可以在ADT中在签名或头文件中编写类似于以下内容的内容:
typedef struct IntSetStruct *IntSetType;
void union(IntSetType s1, IntSetType s2);
我们针对此接口进行编程。值得注意的是,该类型保留为抽象。您不知道它是什么。然后我们有一个BST实现,然后提供了一个具体的类型和操作:
struct IntSetStruct {
int value;
struct IntSetStruct* left;
struct IntSetStruct* right;
}
void union(IntSetType s1, IntSetType s2){ ... }
现在,union实际上知道s1和s2的具体表示,因此可以利用它进行有效的实现。我们还可以编写一个列表支持的实现,然后选择与之链接。
我已经编写了C(ish)语法,但是您应该查看例如标准ML,以便正确完成抽象数据类型(例如,您可以在同一程序中实际使用多个ADT实现,大致可以通过限定类型:BSTImpl)。比如说IntSetStruct和ListImpl.IntSetStruct)
相反的是,过程数据抽象(对象)使您可以轻松地引入可以与旧的实现一起使用的新实现。例如,您可以编写自己的自定义LoggingIntSet实现,并将其与BSTIntSet合并。但这是一个折衷方案:您会丢失二进制方法的信息类型!与ADT实现相比,通常您不得不在界面中暴露更多的功能和实现细节。现在,我觉得我只是在重新整理库克论文,所以,请真正阅读它!
我想为此添加一个示例。
Cook建议使用C语言中的模块作为抽象数据类型的示例。实际上,C语言中的模块涉及信息隐藏,因为存在通过头文件导出的公共功能,而没有通过头文件导出的静态(私有)功能。另外,通常会有构造函数(例如list_new())和观察者(例如list_getListHead())。
例如,使名为LIST_MODULE_SINGLY_LINKED的列表模块成为ADT的关键点在于,该模块的功能(例如list_getListHead())假定输入的数据已由LIST_MODULE_SINGLY_LINKED的构造函数创建,而不是任何“等价的”清单的执行方式(例如LIST_MODULE_DYNAMIC_ARRAY)。这意味着LIST_MODULE_SINGLY_LINKED的功能可以在其实现中采用特定的表示形式(例如,单链接列表)。
LIST_MODULE_SINGLY_LINKED无法与LIST_MODULE_DYNAMIC_ARRAY进行互操作,因为我们无法将使用LIST_MODULE_DYNAMIC_ARRAY的构造函数创建的数据馈送给LIST_MODULE_SINGLY_LINKED的观察者,因为LIST_MODULE_SINGLY_LINKED假定对象仅表示一个列表(表示一个对象)。
这类似于抽象代数的两个不同组无法互操作的方式(也就是说,您不能将一个组的元素与另一组的元素的乘积取整)。这是因为组具有组的闭包属性(组中元素的乘积必须在组中)。但是,如果我们可以证明两个不同的组实际上是另一个组G的子组,那么我们可以使用G的乘积来添加两个元素,两个组中的每一个都可以。
比较ADT和对象
Cook将ADT和对象之间的差异部分与表达问题联系起来。粗略地说,ADT与通常以功能性编程语言实现的泛型函数耦合,而对象与通过接口访问的Java“对象”耦合。出于本文的目的,泛型函数是一个带有一些参数ARGS和类型TYPE(前提条件)的函数;基于TYPE,它选择适当的函数,并使用ARGS(后置条件)对其进行评估。泛型函数和对象都实现了多态性,但是对于泛型函数,程序员知道该函数将由泛型函数执行,而无需查看泛型函数的代码。另一方面,对于对象,除非程序员查看对象的代码,否则程序员不知道对象将如何处理参数。
通常,表达问题是根据“我有很多表示形式”来考虑的吗?与“我有很多功能却很少代表”吗?在第一种情况下,应该通过表示来组织代码(这是最常见的,尤其是在Java中)。在第二种情况下,应该按功能组织代码(即,让一个通用功能处理多种表示形式)。
如果您代表组织代码,然后,如果你想添加额外的功能,你是被迫的功能添加到对象的每个表示; 从这个意义上说,添加功能不是“添加”。如果你按功能组织代码,然后,如果你想添加一个额外的代表性-你是被迫的表示添加到每个对象; 从这个意义上说,不是以“加法”形式添加表示形式。
ADT相对于对象的优势
对象相对于ADT的优势
动态调度是OOP的关键
现在显而易见,动态分配(即后期绑定)对于面向对象的编程至关重要。这样就可以以通用方式定义过程,而无需采用特定的表示形式。具体来说-面向对象的编程在python中很容易,因为可以以不采用特定表示方式的方式来编程对象的方法。这就是为什么python不需要Java之类的接口的原因。
在Java中,类是ADT。但是,通过其实现的接口访问的类是一个对象。
附录:Alan Kay的OOP版本
艾伦·凯(Alan Kay)明确将对象称为“代数族”,而库克则认为ADT是代数。因此,凯很可能意味着一个对象是ADT族。也就是说,对象是满足Java接口的所有类的集合。
但是,库克画的物体的图片比艾伦·凯的想象要严格得多。他希望物体表现为网络中的计算机或生物细胞。这个想法是将最小承诺原则应用到编程中-这样,一旦使用ADT构建了高层,就很容易更改它们的底层。考虑到此图,Java接口的限制性太强,因为它们不允许对象解释消息的含义,甚至完全不予理ignore。
总而言之,对于凯来说,对象的关键思想不是说它们是一个代数族(正如库克所强调的那样)。相反,Kay的关键思想是将适用于大型(网络中的计算机)的模型应用于小型(程序中的对象)的模型。
编辑:关于凯的OOP版本的另一个说明:对象的目的是向声明性理想靠拢。我们应该告诉对象该怎么做-而不是告诉它如何通过微观管理实现状态,这是过程编程和ADT的惯例。在此处,此处,此处和此处可以找到更多信息。
编辑:我发现OOP的阿兰凯的定义的一个非常,非常好的博览会在这里。