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。