Mixin、多重继承与装饰者模式-演道网

疑问

最早接触mixin这个概念,是在使用React的时候。那时候对mixin的认知是这样的:“React不同的组件类可能需要相同的功能,比如一样的getDefaultPropscomponentDidMount等。

我们知道,到处重复地编写同样的代码是不好的。为了将同样的功能添加到多个组件当中,你需要将这些通用的功能包装成一个mixin,然后导入到你的模块中。

出处: React Mixin 的使用

那时候我以为mixin是React独创的一个概念,直到后来我在另外的很多资料中也发现mixin的踪影,比如Vue中也有mixin。仔细研究了一番,才猛地发现:原来mixin是一种模式,有着广泛的应用。
下面我们就来一一理清思路。

Mixin的由来

关于JS中mixin是怎么来的,有两派观点。一派是模仿类,另一派是多重继承

模仿类

众所周知,JS没有真正意义的类。为什么这门语言在设计之初就没有类?为什么非要通过”别扭“的原型链来实现继承?阮一峰老师的这篇文章给出了答案。真正的类在继承中是复制的。因此,虽然JS中没有类,但是无法阻挡众多JS开发者模仿类的复制行为,由此引入了mixin(混入)。
下面举个例子。Vehicle是一个交通工具”类“,Car是一个小汽车”类“,Car要继承于Vehicle,最常见的方法应该是通过原型链。但是,下面的代码却没有这么做。

function mixin(sourceObj, targetObj){
    for(let key in sourceObj){
        if(!(key in targetObj)){
            targetObj[key] = sourceObj[key];
        }
    }
    return targetObj;
}

let Vehicle = {
    engines: 1,
    ignition () {
        console.log("Turning on my engine.");
    },
    drive () {
        this.ignition();
        console.log("Steering and moving forward!");
    }
};

let Car = mixin(Vehicle, {
    wheels: 4,
    drive () {
        Vehicle.drive.call(this);
        console.log(`Rolling on all ${this.wheels} wheels`);
    }
})

Car.drive();

仔细观察上面的代码,我们能够发现:Vehicle类的方法都被复制到Car的属性中(当然,避开了同名属性)。进一步思考,我们最终想要得到的结果是Car能够访问到原先Vehicle有的属性,比如ignition。

  1. 如果是通过常见的原型链,Car固然能够访问到ignition,不过那是沿着原型链向上查找才找到的。
  2. 如果是通过不常见的mixin,Car也能访问到ignition,不过这次是直接在Car的属性上找到的。

so,这就是为了模仿类而引入的mixin。下面我们来看另一派的观点。

多重继承

在面向对象的语言中(如C++、Java和JavaScript),单一继承都是非常常见的。但是,如果想同时继承多于一个结构,例如”猫“在继承”动物“的同时,又想继承”宠物“,那怎么办?
C++给出的答案是:多重继承
然而,多重继承也是一把双刃剑。它在解决问题的同时,却又增加了程序的复杂性和含糊性,最为典型的当属钻石问题。因此,Java和JavaScript都不支持多重继承。然而,多重继承所要解决的问题依然存在,那么,他们各自又是如何解决的呢?
Java的解决方案是:通过原生的接口继承来间接实现多重继承。
JS的解决方案是:原生JS没有解决方案(别忘了,设计之初,这门语言就是定位为很简单的脚本语言,甚至连”类“都不愿意引入,怎么可能会考虑多重继承呢?)。所以,众多JS开发者引入了mixin来解决这个问题。
举个例子。如下面代码所示,有两个超类,SuperTypeAnotherSuperType,有一个子类SubType。一开始SubType继承于SuperType,但是,现在我又想让SubType实例化的某个对象obj也拥有超类AnotherSuperType的方法,怎么办?

// 超类
function SuperType(name){
    this.name = name;
}

// 子类
function SubType(name) {
    SuperType.call(this, name);
}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

let obj = new SubType("youngwind");

function mixin(sourceObj, targetObj){
    for(let key in sourceObj){
        if(!(key in targetObj)) {
           targetObj[key] = sourceObj[key];
        }
    }
}

// 另一个超类
function AnotherrSuperType(){};

AnotherrSuperType.prototype.sayHi = function() {
    console.log(`hi,${this.name}.`);
}

let anotherObj = new AnotherrSuperType();

mixin(anotherObj, obj);

obj.sayHi();  // hi...

看,这就是为了替代多重继承所引入的mixin。

异曲同工

纵观上面两派的观点,虽然各自要解决的问题和应用场景不尽相同,但是,有一处是相同的,那就是:mixin(混合)本质上是将一个对象的属性拷贝到另一个对象上面去,其实就是对象的融合。
这时候我们回过头来考察React和Vue中的mixin,就容易理解多了。而且,不仅是React和mixin,很多js库都有类似的功能,一般定义在util工具类中。有时候会换个名字,不叫mixin,叫extend,不过它们本质上是一样的。另外,说到对象融合,Object.assign也是常用的方法,它跟mixin有一个重大的区别在于:mixin会把原型链上的属性一并复制过去(因为for...in),而Object.assign则不会。

寄生式继承

什么?mixin居然也跟寄生式继承有关?
是的。正是结合mixin与工厂模式,才诞生了寄生式继承。
《高程》章节6.3中,介绍了很多种继承方式,其中我一直都记不住寄生式继承,因为我无法理解为什么会有这么一种继承方式(连名字都是怪怪的)。
下面来看这个例子。

// 传统类
function Vehicle(){
    this.engines = 1;
}

Vehicle.prototype.ignition = function(){
    console.log("Turning on my engine");
}

Vehicle.prototype.drive = function(){
    this.ignition();
    console.log("Steering and moving forward!");
}

// 寄生类
function Car(){
    // 首先,Car是一个Vehicle
    let car = new Vehicle();
    
    // 接着,我们对Car进行定制
    car.wheels = 4;
    
    // 保存Vehicle::drive()的函数引用
    let vehDrive = car.drive;
    
    // 重写drive方法
    car.drive = function(){
        vehDrive.call(this);
        console.log(`Rolling on all ${this.wheels} wheels!`);
    }
    
    return car;
}

let myCar = new Car();

myCar.drive();

仔细观察代码,我们能够发现寄生式继承的原理:

创建一个仅用于封装继承过程的函数(Car),该函数在内部以某种方式(添加属性、重定义方法)来增强对象,最后再像真地是它做了所有工作一样返回对象。(对于一般的构造函数,一般是默认返回this的,无须指定。)

出处:《高程》章节6.3.5,第171页

PS,此处对寄生式继承进行阐述,并非代表我推荐使用这种继承模式,我只是希望通过结合mixin来帮助理解为什么会产生这种继承方式。恰恰相反,我从未在生产环境中用过这种(怪怪的)模式,我个人常用的还是经典的组合模式。

ES7装饰器

为什么我会由mixin联想到ES7装饰器呢?
因为我记得以前刚开始用React创建组件的时候,还是老的语法,const demo = React.createClass,这种情况下是可以用mixin的。后来React用ES6重写之后,就有了class demo extends React.Component这样新的写法,这种情况就用不了mixin了。
why?大概说来,就是ES6的class语法不支持,具体的可以参考这篇文章。也正是由此我发现了ES7有装饰器(decorator)这一功能。仔细看了一些,并未能完全掌握,加之decorator这东西太高级了,距离我生产环境太远,就先作罢,暂不深究。
然而,正是“装饰器”这一名字,让我想起了设计模式中有一种就叫装饰者模式,这东西就非常有实用价值了。

装饰者模式

接手别人的项目总是不可避免的(很可能是常见的),对于一些别人早就写好的函数,当我们想往里面添加一些功能的时候,该怎么办呢?
举个例子,我们的目标是在原有的函数doSomething里面的最后再输出一些其他的东西,下面是不好的(可能是常用的)的做法。

// 修改前
let doSomething = function() {
  console.log(1);
}

// 修改后
let doSomething = function() {
  console.log(1);
  console.log(2);
}

可以看到,这种做法非常简单粗暴,我们直接修改了别人的函数,这违反了开放-封闭原则,并非正途。

下面是好一些的做法。

let doSomething = function() {
  console.log(1);
}

let _doSomething = doSomething;

doSomething = function() {
  _doSomething();
  console.log(2);
}

doSomething();

仔细分析代码,我们能发现其原理:用一个临时变量暂存原函数,然后定义一个新函数拓展原有的函数。这就是装饰者模式:为函数(对象)动态增加职责,又不直接修改这个函数(对象)本身。
然而,我们同时也能发现这个方案的缺点:需要增加一个临时变量来存储原函数。当需要装饰的函数越来越多的时候,临时变量的数量呈线性增长,代码将变得越来越难以维护。
怎么办?请看下面的例子。

Function.prototype.before = function(beforefn){
    let _self = this;
    return function(){
        beforefn.apply(this, arguments);
        return _self.apply(this, arguments);
    }
}

Function.prototype.after = function(afterfn){
    let _self = this;
    return function(){
        let ret = _self.apply(this, arguments);
        afterfn.apply(this, arguments);
        return ret;
    }
}

let doSomething = function() {
  console.log(1);
}

doSomething = doSomething.before(() => {
    console.log(3);
}).after(() => {
    console.log(2);
});

doSomething();  // 输出 312

这样,有了beforeafter方法,我们就能在函数的前面和后面动态添加功能,而且非常的优雅。

这一部分摘抄自曾探所著《JavaScript设计模式与开发实践》第二部分第15章,书中还列举了很多在业务上常见的情况,比如在已经写好的按钮点击事件上添加数据打点功能,比如把字段校验功能移到submit函数外边等等,这些例子都非常的生动实用,建议读者直接阅读原著。

总结

长长的思路到此终于结束,让我们回想一下:无论是复制类的mixin、多重继承的mixin、寄生式继承、ES7的装饰器、设计模式中的装饰者模式,它们都有一个共同点,那就是:在不修改原有的类/对象/函数的前提下,为新的类/对象/函数添加新的职责,以增强其功能。其实这些都是以下两个程序设计原则的外化:开发-封闭原则单一职责原则

转载自演道,想查看更及时的互联网产品技术热点文章请点击http://go2live.cn

发表评论

电子邮件地址不会被公开。 必填项已用*标注