[date: 2019-07-14 23:09] [visits: 34]

Node.js之class vs module

class是面向对象编程中的重要概念,它强大到大家没有任何可以吐槽它的角度,但本文还是想针对Node.js对class与module做个对比,然后结合实际俩聊自己的思考。

class

通过对相似事物的共性进行抽象,从而得到class,在所有面向对象编程语言中,最重要也最有用的两个特性是:继承与多态,如果没有这两个特性面向对象必将黯然失色,用作者的理解简单介绍一下继承与多态:

一个class可以继承另一个class从而具备父class的属性与方法,这可以减少大量重复代码编写。比如一个Animal class可以抽象出动物所具有的属性与行为(方法),而猫科动物Cats calss可以继承Animal class并定义一些猫科动物的属性与行为,再往下Cat、Tiger、Leopard等class可以继承Cats class,它们只定义自己独有的属性与行为

上述举例体现了面向对象继承的思想,用不同层级的抽象减少重复代码的编写,按照此方式编程人员可以较容易的构建一个“动物世界”程序用于模拟这些动物的“吃喝拉撒”

多态离不开继承,它指的是对象类型相同但行为表现不同,假设有一个Animal类型的变量animal,它表示一个具体的动物,如果Animal calss中定义了eat行为,那么animal对象也应该有eat方法,因此程序可以调用animal.eat()表示动物吃东西的行为发生,但动物这个抽象事物没法eat,只能是Cat、Tiger、Leopard这类具体的动物可以发生eat行为,因此animal.eat()具体会发生什么取决于animal到底是哪种动物(运行时才知道)

同类型的对象针对相同的消息产生不一样的行为(方法调用),这就是多态

作者关于面向对象的编程思想,主要是早期在《Think in Java》这本书中学习的,但工作中已经很久没有用OO的思维写代码,也许对继承与多态的理解并不到位,因此若有错误或者描述不恰当还请大家见谅。

在JavaScript中,使用继承并不难,尤其是ES6之后class extends的语法可以非常快速的继承class,但JavaScript是动态类型语言,故没有多态之说。

module

相比较class,module没有严格的定义,但这里是为了和class对比可以这么定义module:“对外暴露属性与方法的封闭集合”,这个封闭集合往往与class的一个实例等同(不好理解,但确实如此)。

拿class中动物的例子来讲,假设不是构建“动物世界”程序,而是实现“动物查询”系统,Animal(动物),Cats(猫科),Cat(猫)概念依旧存在,但是否还应该用class来定义它们呢?如果是,应该在什么时机new它们呢?

上述问题并没有一个很好的回答,此时换种思路,可以选择用module的方式来组织系统,cat作为一个模块,它内部定义cat信息并提供标准接口供外部访问,当用户希望查询cat的资料时,系统交互模块负责调用cat模块的指定接口并返回对应信息。

代码示例

描述class与module时,animal的例子是凭想象的,接下来,作者尝试把该例子补充的贴合实际一些,由于平时工作主要是http server相关,所以用尽量少的代码代码来实现动物查询系统,并用分别用class与module的方式实现cat,然后再进行对比。

server

// server.js
const http = require('http');
const cat = require('./cat');

const server = http.createServer(async (req, res) => {
    if (req.url === '/cat' && req.method === 'GET') {
        res.end(await cat.get());
        return;
    }

    res.end('404');
});

server.listen(3000);

cat之class模式

// cat.js 
class Cat {
    constructor() {
        this.name = 'cat';
        this.description = 'a small domesticated carnivorous mammal with soft fur, a short snout, and retractile claws. It is widely kept as a pet or for catching mice, and many breeds have been developed.';
        this.author = 'shasharoman';
        this.updated = '2019-07-14';
    }

    async get() {
        return [
            `name: ${this.name}`,
            `description: ${this.description}`,
            `author: ${this.author}`,
            `updated: ${this.updated}`
        ].join('\n');
    }

    async put() {
        // 热心网友发现错误,对cat信息修正
    }
};

module.exports = new Cat();

cat之module模式

// cat.js
let name = 'cat';
let description = 'a small domesticated carnivorous mammal with soft fur, a short snout, and retractile claws. It is widely kept as a pet or for catching mice, and many breeds have been developed.';
let author = 'shasharoman';
let updated = '2019-07-14';

exports.get = get;
exports.put = put;

async function get() {
    return [
        `name: ${name}`,
        `description: ${description}`,
        `author: ${author}`,
        `updated: ${updated}`
    ].join('\n');
}

async function put() {
    // 热心网友发现错误,对cat信息修正
}

对比两种模式

server接收到GET /cat请求后返回cat词条的信息,而cat分别采用了class与module两种实现方式,各位读者可以在心中思考并选定自己认为合理的方式,然后继续往下。

此处作者的观点是认为module方式优于class,有如下理由:

关于class中this带来的影响这里再稍微解释一番,function作为JS中的一等公民,作用非常强大,但涉及到this的时候,事情容易变复杂,因为你在每一个function内部都需要谨慎的关注this指向,假设一个类似上述Cat的class有一个复杂的方法需要300行代码实现其目的:

class Demo {
    someMethod() {
        // 300行代码
        // 也许使用了30次this 
    }
}

module.exports = new Demo();

一个方法300行代码显然可读性较差,这时我们打算重构它,尝试将这300行代码的行为总结成5个步骤,重构后的代码如下:

class Demo {
    someMethod() {
        // 实际场景中,step1-5的名称就是他们行为的一种描述,代码可读性有所提升
        // 初次看到someMethod的开发人员,可以快速知道此方法可能需要用哪些步骤去实现其功能,并推断出BUG可能出现在哪个步骤中
        _step1();
        _step2();
        _step3();
        _step4();
        _step5();

        function _step1() {
            // 大约50行代码
        }

        function _step2() {
            // 大约50行代码
        }

        // _step3 _step4 _step5 略
    }
}

module.exports = new Demo();

乍一看仿佛重构的没有问题,但这里一个隐藏的问题是this指针,重构前300行代码直接使用this,指向的是new Demo()这个实例,重构后step1-5中的this指向的是各自function的调用者global(但在上述代码中是undefiend,why?),未避免这种this问题,Jser一般会使用self或者that在外层保留this的引用。

由于本不该关注的this问题从而引发其他相关问题,单这一点就足以让作者排斥这种使用class组织module的方式,更何况还有另外几点。虽然Java中无class不编程,可在JS中并不是这样,因此作者觉得需要多一些的思考来判断到底使用Node.js原生的模块组织方式,还是用define class的方式。

Node.js中哪些场景应该使用class

虽然前文一直在强调不使用class,但这是有前提的,即使用module可以达到目的场景没有必要使用class。而部分场景使用class方式来编写代码可能是更合理的,具体应该如何区分作者认为可以从以下几点思考:

正常来说一个class被定义出来,最终目的都是希望被new出来使用的,而且是有必要被各种不同场景new,当出现一个class只new一个实例使用(单例模式),或者一个class内部全是static方法时,这时应该考虑选择module方式,因为Node.js中module是天然的单例

如果这两点都不满足且符合单例模式,99%的可能你应该选择module而不是class

假设开发人员需要实现一个操作Redis的工具,而Redis操作肯定需要redis-server相关信息(host、port、db、password等),那这个工具就是有状态的,并且工具使用者可能需要连接不同的redis-server,所以此处应该选择使用class

假设上述Redis工具采用class方式实现后,我们希望在一个小型项目中应用且只连接一个固定的DB,那么这个module虽然有状态,但状态是固定的,因此还是可以采用module的方式

function作为一等公民的JavaScript,作者认为没必要全部按照面向对象的方式来编写代码,如果有必要,应该考虑Java等其他语言

总结

写这篇文章的原因是因为最近在一个Node.js项目中发现一种现象:“为了使用class而使用class”,让作者心中着实膈应,所以啰啰嗦嗦写了这么一大堆,希望你在实际项目中如果遇到这类现象也可以思考为什么是class而不是module。