第一個主題先來暖暖身子,要講的不是一個 backend 專屬的技術或概念,更像是屬於寫出優質程式碼的一種方法或者說模式,畢竟不管是前端後端,或是任何領域的工程師,寫出優質、易擴展、易維護的程式碼都非常重要啊! Let's Go!
學習寫程式一段時間後,漸漸會明白除了寫出能 work 的 code 之外,更重要的是寫出一份好 code,而程式碼的品質不外乎可以從可讀性、可維護性、可擴展性等幾個面向去看,在物件導向程式設計中,如果能遵守 SOLID 原則,寫出來的 code 的品質也較有保障,也較易於進行測試。
SOLID 原則為:
而接下來的兩篇文章則是會聚焦在 Dependency inversion principle (依賴反轉原則)。
在物件導向中,難免會需要建立一大堆的 class(類別),而如果類別間的依賴很複雜(例如 A class 需要 new 一個 B class 去使用 B 的功能,那麼萬一 B class 改動了很可能 A 的邏輯也需要更改),那麼假使專案擴展後,就會造成高耦合性,此時離我們撰寫優質程式碼的目標似乎更加遙遠了。
而控制反轉(Inversion Of Control)就是代表了避免類別間產生依賴的設計原則與概念,依賴注入則是實現控制反轉的手段之一。
大體而言實現控制反轉可以獲得以下幾項好處:
首先要來說個故事,老莫是一個剛畢業的社會新鮮人,他的夢想是成為一個頂尖的軟體工程師,而熱愛生活的他也培養出了很多興趣,用 class 可以如下表示:
export class Person {
name: string = '';
}
class KyleMo extends Person {
name = 'Kyle Mo';
private age: number = 23;
// 一堆興趣
read() { console.log('read') }
watchMovie() { console.log('watchMovie') }
coding() { console.log('coding') }
// 還有很多...
}
const kyle = new KyleMo()
kyle.coding();
既然要成為一名軟體工程師,電腦可就是少不了的配備啊,老莫興奮的拿著外婆贊助的三萬元,買了一台 Asus 牌 Windows 系統的筆電,畢竟以前桌機也都是 Windows 系統,能夠快速上手沒有問題!
export class Notebook {
status: string = 'normal';
coding() {
console.log('coding');
}
broken() {
this.status = 'broken';
}
}
export class AsusNotebook extends Notebook {
name: string = 'AsusNotebook';
constructor() {
super();
console.log(this.name);
}
coding() {
console.log(`coding using ${this.name}`);
}
}
export class Person{
name: string = '';
}
class KyleMo extends Person {
name = 'Kyle Mo';
private age: number = 23;
// 一堆興趣
read() { console.log('read') }
watchMovie() { console.log('watchMovie') }
coding() {
const notebook = new AsusNotebook();
notebook.coding();
}
// 還有很多...
}
const kyle = new KyleMo()
kyle.coding();
使用 Windows 筆電開發一段時間後,老莫發現使用較接近 Linux 系統的 MacOS 在開發上的效率對他而言會提升不少,於是他忍痛用自己的存款又買了一台 MacOS 來開發,希望可以加速達成他成為頂尖軟體工程師的夢想。
程式碼的變動大概如下
// 新增 Macbook 的 class
export class Macbook extends Notebook {
name: string = 'Macbook Pro';
constructor() {
super();
console.log(this.name);
}
coding() {
console.log(`coding using ${this.name}`);
}
}
// 將 KyleMo class 中 coding method 中的電腦改為 Mac
class KyleMo extends Person {
name = 'Kyle Mo';
private age: number = 23;
// 一堆興趣
read() { console.log('read') }
watchMovie() { console.log('watchMovie') }
coding() {
// 改這裡
const notebook = new Macbook();
notebook.coding();
}
// 還有很多...
}
聰明的老莫馬上發現一個問題:每次換一個裝置,除了新增新裝置的類別外,還要去改變自身的邏輯。懶惰的老莫馬上思考要怎麼解決這個問題,畢竟每次都改變自己體內的構造真的會壞掉呀!這樣實在太依賴筆電了,完全被筆電牽著鼻子走,不如讓筆電注入到自己身上吧!
class KyleMo extends Person {
name = 'Kyle Mo';
private age: number = 23;
private readonly notebook: Notebook;
constructor(notebook: Notebook) {
super();
this.notebook = notebook;
}
// 一堆興趣
read() { console.log('read') }
watchMovie() { console.log('watchMovie') }
coding() {
this.notebook.coding();
}
// 還有很多...
}
const notebook = new Macbook();
const kyle = new KyleMo(notebook);
kyle.coding();
如此一來老莫總算是脫離對筆電的依賴了,不用更改自身的邏輯,反正外面注入什麼樣的筆電跟他沒有關係,用就對了!
因為老莫總是習慣將所有還會再用到的應用程式保持開啟的狀態,常常導致 CPU 的使用率過高,造成筆電效能低落,因此他決定從台中老家加他以前的寶物 : 8核心的桌上型電腦給搬到台北來使用(用途假設為跟筆電一樣主要拿來開發)。
聰明的你可能會想到以下寫法
export class Pc {
name: string = 'PC';
constructor() {
console.log(this.name);
}
coding() {
console.log(`coding using ${this.name}`);
}
}
class KyleMo extends Person {
name = 'Kyle Mo';
private age: number = 23;
private readonly notebook: Notebook;
private readonly pc: Pc;
constructor(notebook: Notebook, pc: Pc) {
super();
this.notebook = notebook;
this.pc = pc;
}
// 一堆興趣
read() { console.log('read') }
watchMovie() { console.log('watchMovie') }
coding_with_notebook() {
this.notebook.coding();
}
cosing_with_pc() {
this.pc.coding();
}
// 還有很多...
}
const notebook = new Macbook();
const pc = new Pc();
const kyle = new KyleMo(notebook, pc);
kyle.coding_with_notebook();
kyle.coding_with_pc();
就算老莫再愛寫程式,一天能夠運用的時間也是有限的,不太可能會同時使用桌機跟筆電開發,再者兩者的功能都是一樣的(coding),也許我們可以提出更抽象的要求,沒有必要將筆電跟桌電都同時注入到 KyleMo class 中。
更改後的程式碼大致為
interface CodingDevice {
status: string;
coding: () => void;
broken: () => void;
}
export class Notebook {
status: string = 'normal';
coding() {
console.log('coding');
}
broken() {
this.status = 'broken';
}
}
export class Macbook extends Notebook implements CodingDevice {
name: string = 'Macbook Pro';
constructor() {
super();
console.log(this.name);
}
coding() {
console.log(`coding using ${this.name}`);
}
}
export class Pc implements CodingDevice {
status: string = 'normal';
name: string = 'PC';
constructor() {
console.log(this.name);
}
coding() {
console.log(`coding using ${this.name}`);
}
broken() {
this.status = 'broken';
}
}
export class Person{
name: string = '';
}
class KyleMo extends Person {
name = 'Kyle Mo';
private age: number = 23;
private readonly codingDevice: CodingDevice;
constructor(codingDevice: CodingDevice) {
super();
this.codingDevice = codingDevice;
}
// 一堆興趣
read() { console.log('read') }
watchMovie() { console.log('watchMovie') }
coding() {
this.codingDevice.coding();
}
// 還有很多...
}
let codingDevice: CodingDevice;
let kyle: KyleMo;
codingDevice = new Pc();
kyle = new KyleMo(codingDevice);
kyle.coding();
如此一來老莫就可以輕鬆換自己要 coding 的裝置,也解決依賴的問題了。
老莫發現未來還有很多東西需要做 DI (依賴注入),不過自己實作真的有點累啊,有沒有辦法透過其他方式統一管理呢?
下一篇將介紹使用 InversifyJS ,讓實作 IOC 更加方便與擴展。
想盡辦法當好一個Junior Backend Developer
用舒服的姿勢開發 Python Project