繼承與組合都是面向?qū)ο笾写a復用的方式,了解各自有什么特點,可以讓我們寫出更簡潔的代碼沮尿,設計出更好的代碼架構穴豫。
這是一篇翻譯文章,作者是serhiirubets
“我應該怎么使用繼承和組合”這是一個常見的問題围辙,不僅是JavaScript相關,但是本篇我們只討論JavaScript相關的內(nèi)容和示例。
如果你不知道什么是組合或繼承赎败,我強烈推薦你去查看相關內(nèi)容,因為本文的主要講的就是怎么使用和如何選擇它們蠢甲,但是為了確定我們在一個頻道僵刮,讓我們先了解一下組合和繼承吧。
繼承是面向?qū)ο缶幊毯诵母拍钪火信#梢詭椭覀儽苊獯a重復搞糕。主要的思想是我們可以創(chuàng)建一個包含邏輯的基類,可以被子類重用曼追。我們來看個示例:
class Element {
remove() {}
setStyles() {}
}
class Form extends Element {}
class Button extends Element {}
我們創(chuàng)建了一個基類“Element”窍仰,子類會繼承Element中的通用邏輯。
繼承存在is-a關系:Form
是一個Element
, Button
也是一個Element
礼殊。
組合:和繼承不同驹吮,組合使用的是has-a關系鲫忍,將不同的關系收集到一起。
class Car {
constructor(engine, transmission) {
this.engine = engine;
this.transmission = transmission;
}
}
class Engine {
constructor(type) {
this.type = type;
}
}
class Transmission {
constructor(type) {
this.type = type;
}
}
const petrolEngine = new Engine('petrol');
const automaticTransmission = new Engine('automatic');
const passengerCar = new Car(petrolEngine, automaticTransmission) ;
我們創(chuàng)建了使用Engine
和Transmission
創(chuàng)建了Car
钥屈,我們不能說Engine
是一個Car
悟民,但是可以說Car
包含Engine
。希望上面的例子可以幫助你理解什么是繼承篷就,什么是組合射亏。
我們再來看兩個不同的示例,對比一下使用類的方法實現(xiàn)和函數(shù)方法實現(xiàn)有什么區(qū)別竭业。
假設我們正在使用文件系統(tǒng)智润,想實現(xiàn)讀取、寫入和刪除的功能未辆。我們可以創(chuàng)建一個類:
class FileService {
constructor(filename) {
this.filename = filename;
}
read() {}
write() {}
remove() {}
}
目前可以滿足我們想要的功能窟绷,之后我們可能想加入權限控制,一些用戶只有讀取權限咐柜,其他人可能有寫入權限兼蜈。我們應該怎么辦?一個解決方案是我們可以把方法劃分為不同的類:
class FileService {
constructor(filename) {
this.filename = filename;
}
}
class FileReader extends FileService {
read() {}
}
class FileWriter extends FileService {
write() {}
}
class FileRemover extends FileService {
remove() {}
}
現(xiàn)在每個用戶可以使用其需要的權限拙友,但是還有一個問題为狸,如果我們需要給一些人同時分配讀取和寫入權限,應該怎么辦遗契?同時分配讀取和刪除權限怎么辦辐棒?使用當前的實現(xiàn),我們做不到牍蜂,應該怎么解決?
第一個想到的方案可能是:為讀取和寫入創(chuàng)建一個類漾根,為讀取和刪除創(chuàng)建一個類。
class FileReaderAndWriter extends FileService {
read() {}
write() {}
}
class FileReaderAndRemover extends FileService {
read() {}
remove() {}
}
按照這種做法鲫竞,我們可能還需要以下類: FileReader, FileWriter, FileRemove, FileReaderAndWriter, FileReaderAndRemover辐怕。
這不是一個好的實現(xiàn)方式:第一,我們可能不僅有3種贡茅,而是10秘蛇、20種方法其做,還需要在他們之間有大量的組合顶考。第二是我們的類中存在重復的邏輯,F(xiàn)ileReader類包含讀取方法妖泄,F(xiàn)ileReaderAndWriter也包含同樣的代碼驹沿。
這不是一個很好的解決方案,還有其他的實現(xiàn)方法嗎蹈胡?多重繼承渊季?JavaScript中沒有這個特性朋蔫,而且也不是很好的方案:A類繼承了B類,B類可能繼承了其他類...却汉,這樣的設計會非逞蓖混亂,不是一個良好的代碼架構合砂。
怎么解決呢青扔?一個合理的方法是使用組合:我們把方法拆分為單獨的函數(shù)工廠。
function createReader() {
return {
read() {
console.log(this.filename)
}
}
}
function createWriter() {
return {
write() {
console.log(this.filename)
}
}
}
上面的示例中翩伪,我們有兩個函數(shù)創(chuàng)建了可以讀取和寫入的對象∥⒉現(xiàn)在就可以在任何我們地方使用它們,也可以將它們進行組合:
class FileService {
constructor(filename) {
this.filename = filename ;
}
}
function createReadFileService (filename ) {
const file = new FileService(filename);
return {
...file,
...createReader()
}
}
function createWriteFileService (filename) {
const file = new FileService(filename);
return {
...file,
...createWriter(),
}
}
上面的例子中缘屹,我們創(chuàng)建了讀取和寫入服務凛剥,如果我們想組合不同的權限:讀取、寫入和刪除轻姿,我們可以很容易的做到:
function createReadAndWriteFileService (filename) {
const file = new FileService(filename);
}
return {
...file,
...createReader(),
...createWriter()
}
const fileForReadAndWriter = createReadAndWriteFileService('test');
fileForReadAndWriter.read();
fileForReadAndWriter.write();
如果我們有5犁珠、10、20種方法互亮,我們可以按照我們想要的方式進行組合盲憎,不會有重復的代碼問題,也沒有令人困惑的代碼架構胳挎。
我們再來看一個使用函數(shù)的例子饼疙,假設我們有很多員工,有出租車司機慕爬、健身教練和司機:
function createDriver(name) {
return {
name,
canDrive: true,
}
}
function createManager(name) {
return {
name,
canManage: true
}
}
function createSportCoach(name) {
return {
name,
canSport: true
}
}
看起來沒有問題窑眯,但是假設有一些員工白天當健身教練,晚上去跑出租医窿,我們應該怎么調(diào)整呢磅甩?
function createDriverAndSportCoach(name) {
return {
name,
canSport: true,
canDriver: true
}
}
可以實現(xiàn),但是和第一個例子一樣姥卢,如果我們有多種類型混合卷要,就會產(chǎn)生大量重復的代碼。我們可以通過組合來進行重構:
function createEmployee(name,age) {
return {
name,
age
}
}
function createDriver() {
return {
canDrive: true
}
}
function createManager() {
return {
canManage: true
}
}
function createSportCoach() {
return {
canSport: true
}
}
現(xiàn)在我們可以根據(jù)需要組合所有工作類型独榴,沒有重復代碼僧叉,也更容易理解:
const driver = {
...createEmployee('Alex', 20),
...createDriver()
}
const manager = {
...createEmployee('Max', 25),
...createManager()
}
const sportCoach = {
...createEmployee('Bob', 23),
...createSportCoach()
}
const sportCoachAndDriver = {
...createEmployee('Robert', 27) ,
...createDriver(),
...createSportCoach()
}
希望你現(xiàn)在已經(jīng)可以理解繼承和組合之間的區(qū)別,一般來說棺榔,繼承可以用于is-a
關系瓶堕,組合可以用于has-a
。
但在實踐中症歇,繼承有時候并不是一個好的解決方法:就像示例中郎笆,司機是員工(is-a
關系)谭梗,經(jīng)理也是員工,如果我們需要把不同的部分進行混合宛蚓,組合確實比繼承更合適激捏。
最后我想強調(diào)的是:繼承和組合都是很好實現(xiàn),但是你應該正確的使用他們凄吏。一些場景組合可能更合適缩幸,反之亦然。
當然竞思,我們可以將繼承和組合結合在一起表谊,比如我們有is-a
關系,但想添加不同的值或方法:我們可以創(chuàng)建一些基類盖喷,為實例提供所有通用功能爆办,然后使用組合來添加其他特定功能。
歡迎關注“混沌前端”公眾號