探讨面向对象的两大流派。

1. 继承的分类

1.1 类继承和原型继承

OOP有两大类别:class-based和prototype-based。

对于C++和Java来说,他们的面向对象基于:类和实例,

  • 一个类(class)定义了某一对象集合所具有的特征性属性。类是抽象的,而不是其所描述的对象集合中的任何特定的个体。例如 Employee 类可以用来表示所有雇员的集合。
  • 一个实例(instance)是一个的实例化。例如, Tom 可以是 Employee 类的一个实例,表示一个特定的雇员个体。实例具有和其父类完全一致的属性,不多也不少。

对于JavaScript来说,它只有对象。基于原型的语言具有所谓原型对象(prototypical object)的概念。原型对象可以作为一个模板,新对象可以从中获得原始的属性。


举例对比:

  • Class-based:假设 Employee 类只有 namedept 属性,而 ManagerEmployee 的子类并添加了 reports 属性。这时,Manager 类的实例将具有所有三个属性:namedeptreports
  • Prototype-based:
    1. 首先,定义Employee构造函数,在该构造函数内定义name、dept属性;
    2. 接下来,定义Manager构造函数,在该构造函数内调用Employee构造函数,并定义reports属性。
    3. 最后,将一个获得了Employee.prototype(Employee构造函数原型)的新对象赋予manager构造函数,以作为Manager构造函数的原型。之后当你创建新的Manager对象实例时,该实例会从Employee对象继承name、dept属性。

总结:

1.2 哲学思想

Class-based强调belongs to,prototype-based强调is a。在后者的思想里,其实本质上没有真正的继承关系,只是声明自己支持某个协议/接口/prototype,然后想办法真的去支持这个协议即可。

在《设计模式:可复用面向对象软件的基础》一书的开头,作者提出了面向对象设计的两大基本原则:

  • Program to an interface, not an implementation. 面向接口而非面向实现
  • Favor object composition over class inheritance. 使用对象组合而非继承

形象一点说就是:类继承类似于高达模型。模型的零件必须按照正确的方式组装起来,如果装错了,整个体系就搭建不起来。组合更像是乐高积木。各式各样的零件并不只能与指定的零件组合。相反,每一块积木都被设计为可以与其它零件任意组合。

类继承会带来4个问题:

  • 强耦合。父类的方法和属性完全暴露给子类。
  • 多重继承十分复杂
  • 脆弱的架构。由于强耦合的存在,通常很难对一个使用了“错误”设计的类进行重构,因为有太多既有功能依赖这些既有设计。
  • 大猩猩与香蕉问题。你想要一个香蕉,而你却必须抱着一个拿着香蕉的大猩猩。(只想使用父类的一些属性和方法,却必须拥有整个父类)

晚出现的一些语言,比如go,直接就走了prototype-based的道路。

2. 原型继承举例

利用原型继承,实现如下结构:

img

在原型继承中,第一步是思考如何创建构造器:

function Employee () {
  this.name = "";
  this.dept = "general";
}

在 JavaScript 中,会添加一个原型实例作为构造器函数prototype 属性的值,然后将该构造函数原型的构造器重载为其自身

function Manager() {
  Employee.call(this);
  this.reports = [];
}
Manager.prototype = Object.create(Employee.prototype);

function WorkerBee() {
  Employee.call(this);
  this.projects = [];
}
WorkerBee.prototype = Object.create(Employee.prototype);

在使用New创建实例var Tom = new WorkerBee;时,实际上发生了以下事情:

  1. 开辟内存,创建一个普通对象
  2. 将这个普通对象中的prototype指向WorkerBee.prototype
  3. 把这个普通对象设置为执行 WorkerBee 构造函数时 this 的值。
  4. 执行完毕后,JavaScript 返回之前创建的对象,通过赋值语句将它的引用赋值给变量 mark

当访问Tom的属性时,

  1. 检查自身是否含有这个属性
  2. 顺着原型链检查其中的对象是是否含有这个属性
  3. 如果都没有,则返回undefined

3.菱形继承的问题

image-20200627130127799

容易出现二义性的问题:

class A{
public:
    A():a(1){};
    void printA(){cout<<a<<endl;}
    int a;
};

class B : public A{
};

class C : public A{
};

class D:  public B ,  public C{
};

D d;
d.a=10;////error C2385: 对“a”的访问不明确

C++使用了虚继承的方法来解决:对给定的虚基类,无论该类在派生层次中作为虚基类出现多少次,只继承一个共享的基类子对象。下面的代码中在派生类 D 中就只保留了一份成员变量 m_a,直接访问就不会再有歧义了。

//间接基类A
class A{
protected:
    int m_a;
};
//直接基类B
class B: virtual public A{  //虚继承
protected:
    int m_b;
};
//直接基类C
class C: virtual public A{  //虚继承
protected:
    int m_c;
};
//派生类D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};