在上一篇文章中提到,我們可以將不同類別當中的共同屬性或方法,提取出來放在 parent 類別當中,然後透過繼承的方式,實現這些屬性或方法,同時也可以加入額外的屬性或方法。
以上次提到例子來說,BaseballPlayer
是一個 parent 類別,包含了 name
屬性和 hit
方法
class BaseballPlayer {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit`)
}
}
接著,我們建立 Shortstop
類別來繼承 BaseballPlayer
class Shortstop extends BaseballPlayer {
run() {
console.log(`${this.name} can run`)
}
}
當然我們也可以建立另外一個 Outfielder
類別來繼承 BaseballPlayer
,創造出更多不同類型的 baseball players
class Outfielder extends BaseballPlayer {
run() {
console.log(`${this.name} can run very fast`)
}
}
最後,我們就可以實際創造出類別的實例,並呼叫其方法
const lindor = new Shortstop('lindor')
const betts = new Outfielder('betts')
lindor.hit() // lindor can hit
lindor.run() // lindor can run
betts.hit() // betts can hit
betts.run() // betts can run very fast
奇怪的繼承
這時候,隔壁棚傳來新的需求,想要建立一個同樣可以實作出 name
屬性和 hit
方法的網球選手,於是就直接讓 TennisPlayer
去繼承 BaseballPlayer
class TennisPlayer extends BaseballPlayer {
serve() {
console.log(`${this.name} can serve`)
}
}
const federer = new Golfer('federer')
federer.hit() // johnny can hit
federer.walk() // johnny can serve
網球選手繼承棒球選手?雖然實作出來的結果如預期,但是看起來就非常的奇怪,而且沒有邏輯。如果今天我們繼續充實 BaseballPlayer
類別,譬如加入 pitch
方法,變成
class BaseballPlayer {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit`)
}
pitch() {
console.log(`${this.name} can pitch the ball`)
}
}
結果就會發現網球選手 federer 也會開始投球了
const federer = new Golfer('federer')
federer.pitch() // federer can pitch
A 是 B 的一種
雖然我們可以任意的分類、抽取屬性和方法建立 parent 類別,然後任意的繼承某個類別來取得需要的屬性和方法,但這樣的「整理方式」,最終只會造成無限的混亂和錯誤發生。
所以通常在建立 parent 類別和繼承的時候,會遵循著「A 是 B 的一種」(is-a)的規則,譬如
- Shortstop 是 BaseballPlayer 的一種
- Outfielder 是 BaseballPlayer 的一種
- 智人是人屬的一種
- 靈長目是哺乳綱的一種
- child 類別是 parent 類別的一種
- ...
這樣一來,就能有邏輯的模擬真實世界的狀況,也不會有意外的錯誤發生。
如何整理和規範?
現在我們知道 TennisPlayer
不應該直接繼承 BaseballPlayer
,不過這看起來好像也不是什麼大問題,只要直接建立兩個完全獨立的類別,然後分別實作各種方法,像是 hit
, pitch
, serve
等等。
但是我們還是希望可以稍微整理一下,讓這個共同的方法在某種程度上被「抽取出來」或「規範」,並在未來建立其他新的類別的時候可以被使用。
接下來我們來看看幾種不同的實作方式:
1. 建立 parent 類別
class Athlete {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit`)
}
}
class BaseballPlayer extends Athlete {
pitch() {
console.log(`${this.name} can pitch the ball`)
}
}
class TennisPlayer extends Athlete {
serve() {
console.log(`${this.name} can serve`)
}
}
這裡我們建立了一個 Athlete
類別,並讓 BaseballPlayer
和 TennisPlayer
分別繼承他的屬性和方法,如此一來,baseball player 和 tennis player 都可使用同樣的 hit
方法
const jeter = new BaseballPlayer('jeter')
const federer = new TennisPlayer('federer')
const someone = new Athlete('someone')
jeter.hit() // jeter can hit
federer.hit() // federer can hit
someone.hit() // someone can hit
不過這時候又發現了幾個小問題,一個是雖然我們希望 baseball player 和 tennis player 都可以使用 hit
,但是兩者實際實作 hit
的方式和細節可能不太一樣,譬如我們希望變成
jeter.hit() // jeter can hit baseball
federer.hit() // federer can hit tennis
另外一個是,也許我們不需要為 Athlete
建立實例 (e.g., someone)。我們期待各種運動選手都應該來自於各種選手的類別,然後這些類別共同繼承Athlete
。
2. 使用「抽象類別」
為了解決剛才提到的問題,所以現在我們換個方式做。相對於建立一個 parent 類別,這裡我們建立一個抽象類別 Athlete
。
abstract class Athlete {
name: string
constructor(name: string) {
this.name = name
}
abstract hit(): void;
}
class BaseballPlayer extends Athlete {
hit() {
console.log(`${this.name} can hit baseball`)
}
pitch() {
console.log(`${this.name} can pitch the ball`)
}
}
class TennisPlayer extends Athlete {
hit() {
console.log(`${this.name} can hit tennis`)
}
serve() {
console.log(`${this.name} can serve`)
}
}
跟剛剛不一樣的地方是,我們無法透過抽象類別來建立實例,另一方面,我們只在這個抽象類別定義了 hit
這個方法的存在,但是沒有定義 hit
的實作細節。所以 baseball player 和 tennis player 可以有同樣的 hit
方法但是有不同的結果
const jeter = new BaseballPlayer('jeter')
const federer = new TennisPlayer('federer')
const someone = new Athlete('someone') // error
jeter.hit() // jeter can hit baseball
federer.hit() // federer can hit tennis
3. 使用「介面」
除了抽象類別之外,我們還可以使用介面 (interface)。介面本身定義了 hit
方法的存在,但是沒有定義它的實作方式。hit
實作的方式被定義在使用 (implements) 該介面的類別當中
interface Hit {
hit(): void;
}
class BaseballPlayer implements Hit {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit baseball`)
}
pitch() {
console.log(`${this.name} can pitch the ball`)
}
}
class TennisPlayer implements Hit {
name: string
constructor(name: string) {
this.name = name
}
hit() {
console.log(`${this.name} can hit tennis`)
}
serve() {
console.log(`${this.name} can serve`)
}
}
和抽象類別一樣,介面無法建立實例
const jeter = new BaseballPlayer('jeter')
const federer = new TennisPlayer('federer')
const someone = new Hit() // error
jeter.hit() // jeter can hit baseball
federer.hit() // federer can hit tennis
不過,究竟什麼是「抽象類別」和「介面」,就讓我們下一篇文章多談一些吧!
鐵人賽發表網址:幫自己搞懂物件導向和設計模式