探讨面向对象的两大流派。
1. 继承的分类
1.1 类继承和原型继承
OOP有两大类别:class-based和prototype-based。
对于C++和Java来说,他们的面向对象基于:类和实例,
- 一个类(class)定义了某一对象集合所具有的特征性属性。类是抽象的,而不是其所描述的对象集合中的任何特定的个体。例如
Employee
类可以用来表示所有雇员的集合。 - 一个实例(instance)是一个类的实例化。例如,
Tom
可以是Employee
类的一个实例,表示一个特定的雇员个体。实例具有和其父类完全一致的属性,不多也不少。
对于JavaScript来说,它只有对象。基于原型的语言具有所谓原型对象(prototypical object)的概念。原型对象可以作为一个模板,新对象可以从中获得原始的属性。
举例对比:
- Class-based:假设
Employee
类只有name
和dept
属性,而Manager
是Employee
的子类并添加了reports
属性。这时,Manager
类的实例将具有所有三个属性:name
,dept
和reports
。 - Prototype-based:
- 首先,定义Employee构造函数,在该构造函数内定义name、dept属性;
- 接下来,定义Manager构造函数,在该构造函数内调用Employee构造函数,并定义reports属性。
- 最后,将一个获得了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. 原型继承举例
利用原型继承,实现如下结构:
在原型继承中,第一步是思考如何创建构造器:
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;
时,实际上发生了以下事情:
- 开辟内存,创建一个普通对象
- 将这个普通对象中的
prototype
指向WorkerBee.prototype
- 把这个普通对象设置为执行
WorkerBee
构造函数时this
的值。 - 执行完毕后,JavaScript 返回之前创建的对象,通过赋值语句将它的引用赋值给变量
mark
。
当访问Tom
的属性时,
- 检查自身是否含有这个属性
- 顺着原型链检查其中的对象是是否含有这个属性
- 如果都没有,则返回
undefined
3.菱形继承的问题
容易出现二义性的问题:
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;
};