多型(Polymorphism)&繼承(Inheritance)能解決什麼問題? — 用 TypeScript 學物件導向
市面上很多文章、影片分享了物件導向要如何應用。 卻很少人分享這些概念最初是為了解決什麼問題,所以很多人就算學完了物件導向的概念,卻依舊沒有把它應用在手上的專案。 因此這個系列文中,筆者會先分享「直線思考下」我們會設計出怎樣糟糕的程式…
這篇文章寫於 2022 年,文中的工具與版本可能已經更新,實作時請以官方文件為準。
本文目錄
市面上很多文章、影片分享了物件導向要如何應用。
卻很少人分享這些概念最初是為了解決什麼問題,所以很多人就算學完了物件導向的概念,卻依舊沒有把它應用在手上的專案。
因此這個系列文中,筆者會先分享「直線思考下」我們會設計出怎樣糟糕的程式;接著再來討論如何應用物件導向的概念對程式做 Refactoring,提升程式未來的擴充性、維護性。

大綱
一、建立一個讓不同職業交流的平台
二、老闆想要再多加幾個職業
三、[Refactoring]將「介紹技能」程式移轉到 Class 變成 Function
四、平台經營的很成功,要擴增「技能交換、讀書會」的功能
五、[Refactoring]老闆…我改不動了,打算用繼承進行重構
六、應用多型讓不同職業的工作者進行交流
七、所以「多型(Polymorphism)&繼承(Inheritance)」的概念幫我們解決了什麼問題?
一、建立一個讓不同職業交流的平台
既然要建立不同職業交流的平台,那工作者就是必不可少的元素;我們先建立一個「工作者(Worker)」的 Class,並設計「name(姓名)、jobType(職業類型)」作為屬性。
因為這個系統只是試水溫的階段,所以在一開始只開放「工程師(ENGINEER)、設計師(DESIGNER)、律師(LAWYER)」這幾個職業(Job),我們用列舉(enum)的方式來實現。
export enum Job { // 職業對應的常數
ENGINEER = 0,
DESIGNER = 1,
LAWYER = 2,
}
export class Worker {
private _name: string; // 姓名
private _jobType: number; // 職業類型
constructor(theName: string, theJobType: number) {
this._name = theName;
this._jobType = theJobType;
}
public get name() {
return this._name;
}
public set name(theName: string) {
this._name = theName;
}
public get jobType() {
return this._jobType;
}
public set jobType(theJobType: number) {
this._jobType = theJobType;
}
}
原始碼:Worker-init.ts(GitHub Gist)
在 Worker 的 Class 建立完後,我們便可以將對應的「職業」實體化;既然我們要讓不同的職業進行交流,就模擬一小段讓他們「介紹技能」的程式。
let workers = [
new Worker("Dean", Job.ENGINEER), // 產生工作者
new Worker("Amy", Job.DESIGNER),
new Worker("Peter", Job.LAWYER),
];
workers.forEach((worker) => {
console.log(worker.name);
switch (worker.jobType) {
case Job.ENGINEER: {
console.log("我超會寫程式!");
break;
}
case Job.DESIGNER: {
console.log("我超會UI設計!");
break;
}
case Job.LAWYER: {
console.log("我超會打官司!");
break;
}
}
});
原始碼:introSkill-init.ts(GitHub Gist)
二、老闆想要再多加幾個職業
系統上線後反應熱烈,老闆希望再新增「魔術師(MAGICIAN)、畫家(PAINTER)」這兩個職業,於是我們在 Job 裡面新增這兩個職業的常數。
export enum Job { // 職業對應的常數
ENGINEER = 0,
DESIGNER = 1,
LAWYER = 2,
MAGICIAN = 3,
PAINTER = 4
}
原始碼:job-add.ts(GitHub Gist)
在 Worker 的 Class 調整完畢後,我們也要同步調整讓工作者「介紹技能」的程式。
let workers = [
new Worker("Dean", Job.ENGINEER), // 產生工作者
new Worker("Amy", Job.DESIGNER),
new Worker("Peter", Job.LAWYER),
new Worker("Davy", Job.MAGICIAN),
new Worker("Angela", Job.PAINTER),
];
workers.forEach((worker) => {
console.log(worker.name);
switch (worker.jobType) {
case Job.ENGINEER: {
console.log("我超會寫程式!");
break;
}
case Job.DESIGNER: {
console.log("我超會UI設計!");
break;
}
case Job.LAWYER: {
console.log("我超會打官司!");
break;
}
case Job.MAGICIAN: {
console.log("我會變魔術!");
break;
}
case Job.PAINTER: {
console.log("我會畫畫!");
break;
}
}
});
原始碼:introSkill-add.ts(GitHub Gist)
我想程式寫到這裡,應該有很多人發覺不對勁了,如果每新增一個職業,我們就要在另一個檔案中調整程式,這樣伴隨系統的成長,我們會越來越難維護它。
三、[Refactoring]將「介紹技能」程式移轉到 Class 變成 Function
為了避免日後我們要在多個地方維護程式,我們把判斷職業的 switch 轉移到 Worker 的 Class 中,並新增一個「introSkill」的 Function 讓外部使用。
export class Worker {
private _name: string; // 姓名
private _jobType: number; // 職業類型
constructor(theName: string, theJobType: number) {
this._name = theName;
this._jobType = theJobType;
}
public get name() {
return this._name;
}
public set name(theName: string) {
this._name = theName;
}
public get jobType() {
return this._jobType;
}
public set jobType(theJobType: number) {
this._jobType = theJobType;
}
public introSkill() {
switch (this.jobType) {
case Job.ENGINEER: {
console.log("我超會寫程式!");
break;
}
case Job.DESIGNER: {
console.log("我超會UI設計!");
break;
}
case Job.LAWYER: {
console.log("我是傳奇律師!");
break;
}
case Job.MAGICIAN: {
console.log("我是魔術師!");
break;
}
case Job.PAINTER: {
console.log("我是畫家!");
break;
}
}
}
}
原始碼:worker-addFun.ts(GitHub Gist)
經過上面的調整後,不同職業的工作者如果想「介紹技能」,呼叫 introSkill 的 Function 即可。
let workers = [
new Worker("Dean", Job.ENGINEER), // 產生工作者
new Worker("Amy", Job.DESIGNER),
new Worker("Peter", Job.LAWYER),
new Worker("Davy", Job.MAGICIAN),
new Worker("Angela", Job.PAINTER),
];
workers.forEach((worker) => {
console.log(worker.name);
worker.introSkill() // 改成函式後就不用這樣改來改去了
});
原始碼:worker-refactoring.ts(GitHub Gist)
四、平台經營的很成功,要擴增「技能交換、讀書會」的功能
為了讓各行各業交流的更加熱烈、深化自身技能,老闆請你增加「技能交換(skillExchange)、讀書會(studyGroup)」的功能,如果你依照原本的邏輯繼續設計程式會長這樣。
export class Worker {
private _name: string; // 姓名
private _jobType: number; // 職業類型
constructor(theName: string, theJobType: number) {
this._name = theName;
this._jobType = theJobType;
}
public get name() {
return this._name;
}
public set name(theName: string) {
this._name = theName;
}
public get jobType() {
return this._jobType;
}
public set jobType(theJobType: number) {
this._jobType = theJobType;
}
public introSkill() {
switch (this.jobType) {
case Job.ENGINEER: {
console.log("我超會寫程式!");
break;
}
case Job.DESIGNER: {
console.log("我超會UI設計!");
break;
}
case Job.LAWYER: {
console.log("我是傳奇律師!");
break;
}
case Job.MAGICIAN: {
console.log("我是魔術師!");
break;
}
case Job.PAINTER: {
console.log("我是畫家!");
break;
}
}
}
public skillExchange(jobType: number) {
switch (jobType) {
case Job.ENGINEER: {
console.log("我想學程式!");
break;
}
case Job.DESIGNER: {
console.log("我想學UI設計!");
break;
}
case Job.LAWYER: {
console.log("我想學法律!");
break;
}
case Job.MAGICIAN: {
console.log("我想學魔術!");
break;
}
case Job.PAINTER: {
console.log("我想學畫畫!");
break;
}
}
}
public studyGroup() {
switch (this.jobType) {
case Job.ENGINEER: {
console.log("程式讀書會");
break;
}
case Job.DESIGNER: {
console.log("UI設計讀書會!");
break;
}
case Job.LAWYER: {
console.log("法律讀書會");
break;
}
case Job.MAGICIAN: {
console.log("魔術讀書會");
break;
}
case Job.PAINTER: {
console.log("畫畫讀書會");
break;
}
}
}
}
原始碼:worker-ugly-code.ts(GitHub Gist)
我們現在才 5 個職業、3 個功能就把 Worker 這個 Class 搞的這麼冗長了,現在先來分析這個程式日後會遭遇的問題:
- 維護性:日後增加新的職業,需要在每個 switch 設計對應的 case;如果職業繁多,那產生 Bug 是完全可以預期的事情。
- 可讀性:如果這個平台由多位工程師共同開發,那其他工程師使用到 Worker 這個 Class 時可能會很抖,因為無論閱讀理解還是改寫都很困難。
- 擴充性:如果未來需要根據特定職業設計獨有的 Function(比如只允許工程師使用 Programing),那可能就要動到 Class 的結構了。
五、[Refactoring]老闆…我改不動了,打算用繼承進行重構
既然我們已經體會到「新增職業」帶來的痛苦,不如先用繼承的概念重構,將「工作者(Worker)」設計為基礎類別(父類別)。
因為職位有「多樣性」,所以我們將 Worker 定義為「抽象類別(abstract class)」,並將「skill、skillExchange、studyGroup」這些功能抽象化,讓繼承的衍生類別(子類別)去實現。
export abstract class Worker {
private _name: string; // 姓名
constructor(theName: string) {
this._name = theName;
}
public get name() {
return this._name;
}
public set name(theName: string) {
this._name = theName;
}
abstract introSkill(): void;
abstract skillExchange(): void;
abstract studyGroup(): void;
}
原始碼:woker-inheritance.ts(GitHub Gist)
日後我們想新增一個職業時,只要讓他繼承「工作者(Worker)」的 Class,並實作抽象的功能即可;下面舉一個新增「工程師(Engineer)」這個職業的例子。
class Engineer extends Worker {
introSkill() {
console.log("我超會寫程式!");
}
skillExchange() {
console.log("我想學程式!");
}
studyGroup() {
console.log("程式讀書會");
}
}
let engineer = new Engineer("Dean");
engineer.studyGroup();
原始碼:engineer.ts(GitHub Gist)
程式重構後,我們新增職業不但變得輕鬆,還大幅降低產生 Bug 的風險;因爲繼承 Worker 的 Class 後,會明確告訴你需要實作的功能有哪些(如果沒有實作就會報錯😅)。
六、應用多型讓不同職業的工作者進行交流
接著我們回到讓不同職業的工作者「介紹技能」的程式。
這次我們在陣列中塞了兩種職業(Engineer、Designer);不過因為這些職業都是 Worker 的衍生類別,所以在 forEach 中我們可以用「Worker」進行解析,進而存取 Worker 所提供的方法(introSkill)。
let workers = [new Engineer("Dean"), new Designer("Amy")];
workers.forEach((worker: Worker) => {
console.log(worker.name);
worker.introSkill();
});
原始碼:worker-polymorphism.ts(GitHub Gist)
備註 1:如果今天我們在工程師(Engineer)的 Class 新增 programming 的方法,那這個方法在 forEach 裡面是無法使用的(因為裡面僅允許 Worker 的方法)。
備註 2:如果我們今天在陣列中增加一個並非 Worker 的衍生類別(ex:new Animal()),那 forEach 會因為偵測到你放了非 Worker 的衍生類別而報錯。
七、所以「多型(Polymorphism)&繼承(Inheritance)」的概念幫我們解決了什麼問題?
這邊筆者就分享自己使用下來最直觀的感受:
- 繼承的概念讓我們的程式可以被重複使用。
- 多形的概念幫我們把功能抽象化,根據實際的需求去執行。
參考資源:Java物件導向 重觀念迅速教學 3 Polymorphism 什麼是繼承? 什麼是多型?為何多型以及如何設計多型?
▶︎ 用 TypeScript 學物件導向
▋ 封裝(Encapsulation)能解決什麼問題?
▋ 多型(Polymorphism)&繼承(Inheritance)能解決什麼問題?(本篇)
留言載入中…