Node.js之class vs module

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,有如下理由:

  • Node.js本身就是基於模塊構建,這種場景使用模塊的組織方式與Node.js編程哲學更一致、統一。
  • 此處使用class方式沒有用到class的優點(封裝,繼承,多態等),既不繼承其他class也不可能被其他class繼承,系統全部按照這個方式構建下去顯得面向對象像個笑話。
  • this指針在這種場景下則顯得非常雞肋,而且還會帶來一些其他問題,面向對象思想的this指針非常重要,此種方式下this沒有起到任何應有的作用。
  • http server場景複雜化之後,用面向對象思想來做行爲抽象,在JS中難度較高,可能最終系統中絕大部分的class都類似上述的Cat,既不繼承其他class又沒有被繼承的可能。系統看起來是用面向對象思想實現,實際上卻只是把class當成module的概念在用。

關於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方式來編寫代碼可能是更合理的,具體應該如何區分作者認爲可以從以下幾點思考:

  • 需要實現的事物有沒有要被new的場景?可能會被new的次數?

正常來說一個class被定義出來,最終目的都是希望被new出來使用的,而且是有必要被各種不同場景new,當出現一個class只new一個實例使用(單例模式),或者一個class內部全是static方法時,這時應該考慮選擇module方式,因爲Node.js中module是天然的單例

  • 如果使用class,定義的class有沒有extends其他class?有沒有可能被其他class繼承?

如果這兩點都不滿足且符合單例模式,99%的可能你應該選擇module而不是class

  • 如果使用module,定義的module是否有“狀態”特徵?在不同場景下是否希望module處於不同狀態?

假設開發人員需要實現一個操作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。

博客原文

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章