当前位置:嗨网首页>书籍在线阅读

15-多继承、混合类和接口

  
选择背景色: 黄橙 洋红 淡粉 水蓝 草绿 白色 选择字体: 宋体 黑体 微软雅黑 楷体 选择字体大小: 恢复默认

9.3 多继承、混合类和接口

一些面向对象语言支持多继承,也就是说一个类可以有两个直接的父类(不像某个类有一个父类,而该父类又有一个父类)。多继承有引发冲突的风险。好比说,如果某个类继承自两个父类,而两个父类都有 greet 方法,那么子类该继承谁的方法呢?所以很多编程语言选择单继承来避免这个棘手的问题。

然而,当思考现实世界的问题时,多继承往往是有意义的。例如,汽车可能继承自交通工具和“可接受保险的”(可以给一辆汽车或房子上保险,但是房子肯定不属于交通工具)。不支持多继承的编程语言通常会引入接口的概念来绕过这个问题。一个类(Car)只能有一个父类(Vehicle),但它可以有多个接口(Insurable、Container等等)。

JavaScript是一种有趣的混合。技术上,它是一个单继承的语言,因为在原型链并不会去寻找多个父类,但是它确实提供了一些方法,有时候这些方法比多继承或者接口还要好用(当然也不全是这样)。

多继承产生问题的主要原因在于混入的概念。混入是指功能按需“混合”。因为JavaScript是一门弱类型且语法自由的语言,任何时候都可以将任意功能混合到任何对象中。

下面创建一个“可接受保险的”混合类,同时保证它可以用在汽车类上。当然要尽量简化它。除了可接受保险的混合类,还要创建一个 InsurancePolicy 类。可接受保险的混合类需要 addInsurancePolicygetInsurancePolicyisInsured 方法,接下来看看它是如何工作的:

class InsurancePolicy() {}
function makeInsurable(o) {
    o.addInsurancePolicy = function(p) { this.insurancePolicy = p; }
    o.getInsurancePolicy = function() { return this.insurancePolicy; }
    o.isInsured = function() { return !!this.insurancePolicy; }
}

现在可以让任何对象变成可接受保险的。那么对于汽车,需要做什么保险呢?大家的第一反应可能是这样子:

makeInsurable(Car);

大家可能会被下面这个粗暴的方式吓到:

const car1 = new Car();
car1.addInsurancePolicy(new InsurancePolicy());       // 报错

如果读者认为“当然是错的,因为 addInsurancePolicy 方法并不在原型链中,”那就再去看看这个类的头部。它对使汽车可以接受保险没有好处,而且没有意义:汽车这个抽象概念并不是可接受保险的,但是具体的汽车可以。所以下一步做法可能是这样的:

const car1 = new Car();
makeInsurable(car1);
car1.addInsurancePolicy(new InsurancePolicy());       // 正常运行

这样就可以正常工作了,但是现在必须记住为每个实例化的汽车调用 makeInsurable 。可以在Car类的构造方法中进行调用,这样的话,相当于是在给每一辆汽车重复这一个动作。幸好,解决该问题并不难:

makeInsurable(Car.prototype);
const car1 = new Car();
car1.addInsurancePolicy(new InsurancePolicy());       // 正常运行

现在,好像这些方法已经成为 Car 类的一部分。并且,从JavaScript的角度来看,它们确实如此。从开发的角度看,让这两个重要的类的维护变得更简单了,汽车工程组负责管理和开发 Car 类,保险组负责管理 InsurancePolicy 类和 makeInsurable 混合方法。当然,两个组仍然存在互相干扰的空间,但这总比让每个人都工作在一个巨大的 Car 类中要好的多。

混合方法并不能消除冲突:如果出于某些原因,保险组需要在他们的混合类中创建一个 shift 方法,就会破坏 Car 类。同时,不能使用 instanceof 来鉴别对象是否是可接受保险的。最佳实践是使用鸭子类型(如果它有个方法叫 addInsurancePolicy ,那么它一定是可接受保险的)。

可以使用符号来缓解这些问题。假设保险组会不断增加一些通用的方法,而且无意间破坏了 Car 类中的方法。可以要求它们使用符号作为键。它们的混合方法会是这样:

class InsurancePolicy() {}
const ADD_POLICY = Symbol();
const GET_POLICY = Symbol();
const IS_INSURED = Symbol();
const _POLICY = Symbol();
function makeInsurable(o) {
   o[ADD_POLICY] = function(p) { this[_POLICY] = p; }
   o[GET_POLICY] = function() { return this[_POLICY]; }
   o[IS_INSURED] = function() { return !!this[_POLICY]; }
}

由于符号是唯一的,这就保证了混合方法将不会干扰到Car类中已有的功能。这种用法略显笨拙,但更安全。一个折中的办法可能是,字符串用于定义方法,而符号(例如 _POLICY )用于定义数据属性。