繼承與組合都是物件導向中程式碼複用的方式,瞭解各自有什麼特點,可以讓我們寫出更簡潔的程式碼,設計出更好的程式碼架構。
這是一篇翻譯文章,作者是serhiirubets
“我應該怎麼使用繼承和組合”這是一個常見的問題,不僅是JavaScript相關,但是本篇我們只討論JavaScript相關的內容和示例。
如果你不知道什麼是組合或繼承,我強烈推薦你去檢視相關內容,因為本文的主要講的就是怎麼使用和如何選擇它們,但是為了確定我們在一個頻道,讓我們先了解一下組合和繼承吧。
繼承是物件導向程式設計核心概念之一,可以幫助我們避免程式碼重複。主要的思想是我們可以建立一個包含邏輯的基類,可以被子類重用。我們來看個示例:
class Element {
remove() {}
setStyles() {}
}
class Form extends Element {}
class Button extends Element {}
我們建立了一個基類“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) ;
我們建立了使用Engine
和Transmission
建立了Car
,我們不能說Engine
是一個Car
,但是可以說Car
包含Engine
。希望上面的例子可以幫助你理解什麼是繼承,什麼是組合。
我們再來看兩個不同的示例,對比一下使用類的方法實現繼承和函式方法實現組合有什麼區別。
假設我們正在使用檔案系統,想實現讀取、寫入和刪除的功能。我們可以建立一個類:
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() {}
}
現在每個使用者可以使用其需要的許可權,但是還有一個問題,如果我們需要給一些人同時分配讀取和寫入許可權,應該怎麼辦?同時分配讀取和刪除許可權怎麼辦?使用當前的實現,我們做不到,應該怎麼解決?
第一個想到的方案可能是:為讀取和寫入建立一個類,為讀取和刪除建立一個類。
class FileReaderAndWriter extends FileService {
read() {}
write() {}
}
class FileReaderAndRemover extends FileService {
read() {}
remove() {}
}
按照這種做法,我們可能還需要以下類: FileReader, FileWriter, FileRemove, FileReaderAndWriter, FileReaderAndRemover。
這不是一個好的實現方式:第一,我們可能不僅有3種,而是10、20種方法,還需要在他們之間有大量的組合。第二是我們的類中存在重複的邏輯,FileReader類包含讀取方法,FileReaderAndWriter也包含同樣的程式碼。
這不是一個很好的解決方案,還有其他的實現方法嗎?多重繼承?JavaScript中沒有這個特性,而且也不是很好的方案:A類繼承了B類,B類可能繼承了其他類...,這樣的設計會非常混亂,不是一個良好的程式碼架構。
怎麼解決呢?一個合理的方法是使用組合:我們把方法拆分為單獨的函式工廠。
function createReader() {
return {
read() {
console.log(this.filename)
}
}
}
function createWriter() {
return {
write() {
console.log(this.filename)
}
}
}
上面的示例中,我們有兩個函式建立了可以讀取和寫入的物件。現在就可以在任何我們地方使用它們,也可以將它們進行組合:
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(),
}
}
上面的例子中,我們建立了讀取和寫入服務,如果我們想組合不同的許可權:讀取、寫入和刪除,我們可以很容易的做到:
function createReadAndWriteFileService (filename) {
const file = new FileService(filename);
}
return {
...file,
...createReader(),
...createWriter()
}
const fileForReadAndWriter = createReadAndWriteFileService('test');
fileForReadAndWriter.read();
fileForReadAndWriter.write();
如果我們有5、10、20種方法,我們可以按照我們想要的方式進行組合,不會有重複的程式碼問題,也沒有令人困惑的程式碼架構。
我們再來看一個使用函式的例子,假設我們有很多員工,有計程車司機、健身教練和司機:
function createDriver(name) {
return {
name,
canDrive: true,
}
}
function createManager(name) {
return {
name,
canManage: true
}
}
function createSportCoach(name) {
return {
name,
canSport: true
}
}
看起來沒有問題,但是假設有一些員工白天當健身教練,晚上去跑出租,我們應該怎麼調整呢?
function createDriverAndSportCoach(name) {
return {
name,
canSport: true,
canDriver: true
}
}
可以實現,但是和第一個例子一樣,如果我們有多種型別混合,就會產生大量重複的程式碼。我們可以通過組合來進行重構:
function createEmployee(name,age) {
return {
name,
age
}
}
function createDriver() {
return {
canDrive: true
}
}
function createManager() {
return {
canManage: true
}
}
function createSportCoach() {
return {
canSport: true
}
}
現在我們可以根據需要組合所有工作型別,沒有重複程式碼,也更容易理解:
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()
}
希望你現在已經可以理解繼承和組合之間的區別,一般來說,繼承可以用於is-a
關係,組合可以用於has-a
。
但在實踐中,繼承有時候並不是一個好的解決方法:就像示例中,司機是員工(is-a
關係),經理也是員工,如果我們需要把不同的部分進行混合,組合確實比繼承更合適。
最後我想強調的是:繼承和組合都是很好實現,但是你應該正確的使用他們。一些場景組合可能更合適,反之亦然。
當然,我們可以將繼承和組合結合在一起,比如我們有is-a
關係,但想新增不同的值或方法:我們可以建立一些基類,為例項提供所有通用功能,然後使用組合來新增其他特定功能。
歡迎關注“混沌前端”公眾號