面向对象编程有哪些流派?

30

Smalltalk和Simula的面向对象编程(OOP)有哪些哲学差异?

这是一个间接涉及Java和C#与C++之间关系的问题。据我了解,C++基于Simula,而Java和C#基本上来自Smalltalk家族。


好问题。虽然这其中有些微妙之处是主观的,但值得指出的是,答案应该尽量避免明确陈述一个比另一个更好的实证事实。大量不同的实现存在,并且进一步被广泛使用,这应该清楚地表明,在这个领域里并没有一种大小适合所有的解决方案。 - ShuggyCoUk
9个回答

66

在更广泛的面向对象编程中,存在几个关键的“风格”差异。

在所有情况下,关于静态类型系统或动态类型系统的陈述主要意味着其中之一,但问题远非明确或清晰定义。 此外,许多语言选择模糊这些选择之间的界限,因此这不是任何二元选择的列表。

多态的晚期绑定

或者说,“foo.Bar(x)是什么意思?”

  1. 类型的层次结构被展开为每个实例的特定实现(通常通过vtable完成),并且通常允许明确引用基类的实现。
    • 从概念上讲,你看调用点上foo所在的最具体类型。如果它有一个Bar的参数x的实现被调用,如果没有,就选择foo的父级,并重复这个过程。
    • 例如:C++/Java/C#,经常使用"Simula style"。
  2. 纯消息传递。处理名为"Bar"的消息的代码在foo中被要求接受x。只有名称是重要的,而不是调用点可能对Bar的确切含义的任何假设。与前一种样式相比,在前一种样式中,所讨论的方法是已知的Bar,是在编译时定义的类型层次结构中已知的某些内容(尽管在层次结构中的确切位置留到了运行时)。

1经常在静态类型框架中使用,如果在编译时检查到不存在这样的实现,则会出现错误。此外,这些语言通常区分Bar(x)和Bar(y)如果x和y是不同的类型。这是方法重载,具有相同名称的结果方法被视为完全不同。

2经常在动态语言中使用(这些语言倾向于避免方法重载),因此,在运行时,foo的类型可能没有针对名为“Bar”的消息的“处理程序”,不同的语言以不同的方式处理这种情况。

如果需要,两者都可以以相同的方式在后台实现(通常第二个Smalltalk样式的默认值是调用函数,但并非在所有情况下都定义了这种行为)。 由于前一种方法经常可以轻松实现为简单的指针偏移函数调用,因此它可以更容易地相对较快地实现。这并不意味着其他风格不能也快速实现,但在这样做时可能需要更多的工作,以确保不会损害更大的灵活性。

继承/重用

或“婴儿从哪里来?”

  1. 基于类
    • 方法实现被组织成称为类的组。当需要实现继承时,定义一个类来扩展父类。这样,它获得了父类的所有公开方面(字段和方法),并可以选择更改某些/所有这些方面,但不能删除任何方面。您可以添加和更新,但不能删除。
    • 示例:C++/Java/C# (请注意,SmallTalk和Simula都使用此方法)
  2. 基于原型
    • 任何对象的实例只是一组标识的方法(通常由名称标识)和状态(以命名字段的形式)。每当需要新的此“类型”的实例时,可以使用现有实例来克隆一个新实例。这个新类保留了先前类的状态和方法的副本,但然后可以修改以删除、添加或更改现有的命名字段和方法。
    • 示例:Self/JavaScript

再次强调,1倾向于发生在静态语言中,2倾向于动态语言,尽管这不是必要条件,它们只是适合这种风格。

基于接口或类

或“是什么还是怎么做?”

  1. 接口列出所需的方法。它们是一种契约。
    • 例如:VB6
  2. 类列出所需的方法,但可以选择提供其实现。
    • 例如:Simula

这绝非二元选择。大多数基于类的语言允许抽象方法的概念(尚未实现的方法)。如果您有一个所有方法都是抽象的类(在C++中称为纯虚拟),那么该类的总量基本上就是一个接口,尽管可能还定义了一些状态(字段)。真正的接口不应该有状态(因为它仅定义了可能性,而不是如何发生)。

只有较旧的面向对象编程语言倾向于仅依赖其中之一。
VB6仅具有接口,并且没有实现继承。
Simula允许您声明纯虚拟类,但您可以实例化它们(在使用时会出现运行时错误)。

单继承或多继承

或者说"谁是父亲?"

  1. 单继承
    • 只能有一个类型作为另一个类型的父类。在基于类的形式中,你只能从一个类型继承(获取实现)。通常,这种形式包括将接口作为语言的一级方面来弥补这一缺陷。
    • 优点包括更清晰的元数据和内省、更简单的语言规则。
    • 复杂性包括使有用的方法难以进入范围(像MixInsExtension methods这样的东西试图减轻这种问题)。
    • 示例:C#/java
  2. 多继承 - 你可以继承多个类
    • 优点包括某些结构更容易建模和设计。
    • 复杂性包括对冲突解决的复杂规则,尤其是当存在可以采用任何一个父类型的重载方法时。
    • 示例:C++/Eiffel

这个问题引发了相当大的争议,尤其是它是区分 C++ 的 OOP 实现和被视为可能的继任者的许多现代静态类型语言之间的关键因素,如 c# 和 java。

Mutability

或者说,“你想对我做什么?”

  1. 可变
    • 对象一旦创建,其状态可以更改。
  2. 不可变
    • 对象一旦创建,就不能更改。
经常情况下,这不是全有或全无的问题,而只是一个默认设置(大多数使用面向对象编程语言默认为可变)。这可能会对语言的结构产生很大影响。许多主要是函数式语言,包括面向对象编程功能的语言将对象默认为具有不可变状态。
他们的OOP的“纯度”或“一切皆为对象”:
1. 系统中的所有内容都被视为对象(甚至可以到方法本身只是另一种对象,并且可以像其他对象一样进行交互)。
例如:SmallTalk
2. 并不是所有东西都是对象,您不能向所有东西传递消息(尽管系统可能会跳过障碍,使其看起来像您可以)
例如:C++/C#/Java(见注释*)
由于像自动装箱之类的技术使一切似乎都是对象,因此这相当复杂,但您会发现存在几个边界案例,其中发现了这种“编译器魔法”,并且在幕后找到了所谓的奥兹巫师,导致问题或错误。在默认情况下具有不可变性的语言中,这种情况不太可能发生,因为对象的关键方面(它们包含方法和状态)意味着类似于对象但不完全相同的事物具有较少的复杂性可能性。
  • 关于Java/C#,自动装箱(或在c#中)系统使您可以将任何变量在语法上视为对象,但实际上并非如此,在尝试锁定自动装箱对象的情况下会出现问题(编译器拒绝,因为这是明显的错误)。

静态或动态

或者说"你认为你是谁?"

语言设计中更普遍的方面之一,本文不再讨论,但这个决策中的选择影响了OOP的许多方面,就像前面提到的那样。

多态后期绑定的各个方面可能取决于:

  • 消息传递对象的类型(在编译时/运行时)
  • 传递的参数(在编译时/运行时)的类型

语言越动态,这些决策就越复杂,但相反地,语言用户而不是语言设计者对决策有更多的输入。 在这里举例子有些愚蠢,因为静态类型的语言可以被修改以包括动态方面(例如c# 4.0)。


9
我最大的抱怨是,《继承》的字幕应该是“如何形成婴儿?” - Thomas Owens
2
我正在背叛我的英国特色 - ShuggyCoUk
1
像原型继承这样的东西,我从未理解的好比较 :) - Khaled Alshaya
如果这是被接受的答案,那么是时候提供一些链接了。 - ShuggyCoUk
10
那才是我所说的答案! - Jay Atkinson

6

我认为Java和C#也可以归类为Simula编程语言:

  • Smalltalk是动态类型的,与你提到的其他四种语言有很大不同。

  • Smalltalk是结构类型(又称鸭子类型),而其他四种语言是名义类型。

(Java和C#与Smalltalk的共同之处在于它们主要基于虚拟机,但对编程风格的影响很小)。


不仅是动态类型的,还基于消息传递的。 - Wojciech Bederski
@AraK 不,这是暴露给外部世界的(在Java中自动装箱之前更是如此),因此是设计而非实现。 将所有东西都设计为对象,就像语言用户所看到的那样,然后在幕后进行性能优化是可以的(自Java 6以来,这实际上已经发生了),但这不是Java所做的。它明确指出有这些称为原始类型的东西,它们看起来有点像对象,但在几个关键方面具有不同的语义。 - ShuggyCoUk
@ShuggyCoUk 虽然自动装箱不能透明地处理基本类型,但它确实允许我们在概念上将基本类型视为对象(尽管我们并不总是可以在语法上将它们视为对象)。从这个角度来看,我可以看出Java确实有一切皆为对象的概念;即使不是所有东西都是对象,这个概念也存在。 - Imagist
方法调用始终作为函数是一种固定形式的消息传递,这种方式排除了其他实现。请注意,在C#中的LINQ在某种程度上开始混淆,但只是以编译时静态方式,因此不会从根本上改变与OOP相关的方面。 - ShuggyCoUk
自动装箱实际上是将它们在语法上视为对象(因此称为“auto”)。装箱试图让您在概念上将它们视为对象,但只能通过复制来实现(因此远远不能声称对象中的所有内容都是可接受的)。 在Java中,更准确地说,一切都是类,因为变量中保存的任何值都被呈现为具有详细状态和行为的后备类。只要您无法确定,JVM通常会假定大部分行为,这对于该概念并不会有任何损害。 - ShuggyCoUk
显示剩余6条评论

4

Java和C#绝对不是Smalltalk家族的成员。Alan Kay甚至说,在他创造面向对象编程时,并没有像Java或C++那样的东西在他的脑海中。Java、C#和C++几乎以相同的方式解释面向对象编程。

像Smalltalk和Ruby这样的语言具有基于消息传递的根本不同的模型。在C++中,类本质上是方法和状态的命名空间。方法调用在编译时绑定。而在Smalltalk中,“方法调用”直到运行时才绑定。这导致在C++中

foo->bar

这句话的意思是"在foo对象上调用bar方法"。如果bar方法不是虚函数,我想地址会被明确引用。

在Smalltalk中

foo bar

意思是“将消息栏发送到foo对象”。当消息到达时,foo可以对该消息进行任何操作。默认行为是调用名为bar的方法,但这不是必需的。Ruby利用此属性进行ActiveRecord列访问器。当您拥有一个ActiveRecord对象并向其发送数据库表中某个列的名称作为消息时,如果没有定义该名称的方法,则会检查该表上是否有该名称的列,如果有,则返回值。
消息传递可能看起来像微小而无关紧要的细节,但它引导了面向对象编程的其余部分。
“对我来说,OOP只意味着消息传递、本地保留和保护以及隐藏状态过程,并且所有东西都具有极端的后期绑定。它可以在Smalltalk和LISP中完成。可能还有其他系统可以做到这一点,但我不知道。”——Smalltalk的创建者Alan Kay

4

Eiffel是一种静态类型、编译型、多继承的纯面向对象编程语言。

http://dev.eiffel.com


1
Bertrand Meyer曾经有过一个短暂而辉煌的时刻,当他出版了他的第一本面向对象编程书籍并提出了契约式编程。然后他推出了《面向对象软件构造》的第二版,这是一本1200页的庞然大物,自那以后就没有再引起关注。Eiffel从未走得很远。 - duffymo

3

在现代(这里的“现代”用词不当)面向对象编程语言中,Objective C 最像 Smalltalk。

消息:

在 C++、C# 和 Java 中:消息在编译时绑定。
你可以把方法调用看作是发送给对象的一条消息。

在 Objective C 和 Smalltalk 中:消息在运行时绑定。


2
我认为静态类型和动态类型的面向对象编程是面向对象编程中的两个独立的学科。

2
Java、C# 和 C++ 都遵循相似的面向对象编程 (OOP) 策略,基于编译时绑定的函数调用。依赖于调用,在编译时会固定直接函数调用或偏移表中的一个条目。相比之下,Smalltalk 的 OOP 是基于消息传递的。概念上,每个方法调用都是发送给接收对象的一条消息,询问它是否有名为 "Foo" 的方法。
Smalltalk 没有接口的概念,只有看起来相似的方法。在 C++ 语言组中,一切都与接口相关联。不能实现 AddRef 和 Release 而不实现 QueryInterface(即使它只是一个桩函数),因为它们都是 IUnknown 接口的一部分。在 Smalltalk 中,没有 IUnknown 接口,只有三个函数的集合,其中任何一个都可以被实现或不被实现。

2

我认为,基于类的面向对象编程(Smalltalk、Simula、C#和Java都是这种方式的例子)与基于原型的面向对象编程(Self起源并在JavaScript中最为广泛)在概念上存在相当大的差异。


1
除了上述要点外,Smalltalk与Simula之间还存在概念上的区别。
从概念上讲,“Smalltalk风格”通常表示当调用消息时运行的方法是在运行时确定的,有助于多态性。
另一方面,“Simula风格”通常似乎表明所有方法调用实际上只是编写重载函数调用的一种便捷方式——没有运行时多态性。(如果我错了,请纠正我。)
在中间,我们有Java:默认情况下所有方法都是虚拟的,但具有静态类型和编译时类型分派。
例如:
// C++
class Base {
  void doSomething() {
    cout << "Base::doSomething() called!\n";
  }
}
class Derived : Base {
  void doSomething() {
     cout << "Derived::doSomething() called!\n";
  }
}
int main() {
  Base* b = new Base();
  Derived* d = new Derived();
  b->doSomething(); // prints "Base::doSomething() called!"
  d->doSomething(); // prints "Derived::doSomething() called!"
  Base* d2 = d;     // OK; Liskov substitution principle.
  d2->doSomething(); // prints "Base::doSomething called!"  (!)
  delete b;
  delete d;
  return 0;
}

VS:

// Objective-C
//Base.h
@interface Base
{
}
-(void)doSomething
@end
//Base.m
#import "Base.h"
@implementation Base
-(void) doSomething {
  printf("doSomething sent to Base!");
}
@end
//Derived.h
#import "Base.h"
#import "Base.m"
@interface Derived : Base
{
}
@end
//Derived.m
#import "Derived.h"
@implementation Derived
-(void) doSomething {
  printf("doSomething sent to Derived!")
}
@end

//Main.m
#import "Base.h"
#import "Base.m"
#import "Derived.h"
#import "Derived.m"
int main() {
  Base* b = [[Base alloc] init];
  Derived* d = [[Derived alloc] init];
  [b doSomething]; // prints "doSomething sent to Base!"
  [d doSomething]; // prints "doSomething sent to Derived!"
  Base* d2 = d;
  [d2 doSomething]; // prints "doSomething sent to Derived!"
  [b release];
  [d release];
  return 0;
}

Simula肯定有运行时多态性。实际上,你可以实例化一个没有方法实现的类,调用它会导致运行时错误。 - ShuggyCoUk

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