基于 TypeScript 理解程序設(shè)計(jì)的 SOLID 原則
大家好,我是 ConardLi,今天我們來(lái)基于 TypeScript 回顧學(xué)習(xí)下程序設(shè)計(jì)中的 SOLID 原則。
說(shuō)到 SOLID 原則,可能寫(xiě)過(guò)代碼的同學(xué)們應(yīng)該都聽(tīng)過(guò)吧,這是程序設(shè)計(jì)領(lǐng)域最常用到的設(shè)計(jì)原則。SOLID 由 羅伯特·C·馬丁 在 21 世紀(jì)早期引入,指代了面向?qū)ο缶幊毯兔嫦驅(qū)ο笤O(shè)計(jì)的五個(gè)基本原則, SOLID 其實(shí)是以下五個(gè)單詞的縮寫(xiě):
- Single Responsibility Principle:?jiǎn)我宦氊?zé)原則
- Open Closed Principle:開(kāi)閉原則
- Liskov Substitution Principle:里氏替換原則
- Interface Segregation Principle:接口隔離原則
- Dependency Inversion Principle:依賴倒置原則
TypeScript 的出現(xiàn)讓我們可以用面向?qū)ο蟮乃枷刖帉?xiě)出更簡(jiǎn)潔的 JavaScript 代碼,在下面的文章中,我們將用 TypeScript 編寫(xiě)一些示例來(lái)分別解釋下這些原則。
單一職責(zé)原則(SRP)
核心思想:類的職責(zé)應(yīng)該單一,不要承擔(dān)過(guò)多的職責(zé)。
我們先看看下面這段代碼,我們?yōu)?Book 創(chuàng)建了一個(gè)類,但是類中卻承擔(dān)了多個(gè)職責(zé),比如把書(shū)保存為一個(gè)文件:
class Book {
public title: string;
public author: string;
public description: string;
public pages: number;
// constructor and other methods
public saveToFile(): void {
// some fs.write method to save book to file
}
}
遵循單一職責(zé)原則,我們應(yīng)該創(chuàng)建兩個(gè)類,分別負(fù)責(zé)不同的事情:
class Book {
public title: string;
public author: string;
public description: string;
public pages: number;
// constructor and other methods
}
class Persistence {
public saveToFile(book: Book): void {
// some fs.write method to save book to file
}
}
好處:降低類的復(fù)雜度、提高可讀性、可維護(hù)性、擴(kuò)展性、最大限度的減少潛在的副作用。
開(kāi)閉原則(OCP)
核心思想:類應(yīng)該對(duì)擴(kuò)展開(kāi)放,但對(duì)修改關(guān)閉。簡(jiǎn)單理解就是當(dāng)別人要修改軟件功能的時(shí)候,不能讓他修改我們?cè)写a,盡量讓他在原有的基礎(chǔ)上做擴(kuò)展。
先看看下面這段寫(xiě)的不太好的代碼,我們單獨(dú)封裝了一個(gè) AreaCalculator 類來(lái)負(fù)責(zé)計(jì)算 Rectangle 和 Circle 類的面積。想象一下,如果我們后續(xù)要再添加一個(gè)形狀,我們要?jiǎng)?chuàng)建一個(gè)新的類,同時(shí)我們也要去修改 AreaCalculator 來(lái)計(jì)算新類的面積,這違反了開(kāi)閉原則。
class Rectangle {
public width: number;
public height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
}
class Circle {
public radius: number;
constructor(radius: number) {
this.radius = radius;
}
}
class AreaCalculator {
public calculateRectangleArea(rectangle: Rectangle): number {
return rectangle.width * rectangle.height;
}
public calculateCircleArea(circle: Circle): number {
return Math.PI * (circle.radius * circle.radius);
}
}
為了遵循開(kāi)閉原則,我們只需要添加一個(gè)名為 Shape 的接口,每個(gè)形狀類(矩形、圓形等)都可以通過(guò)實(shí)現(xiàn)它來(lái)依賴該接口。通過(guò)這種方式,我們可以將 AreaCalculator 類簡(jiǎn)化為一個(gè)帶有參數(shù)的函數(shù),每當(dāng)我們創(chuàng)建一個(gè)新的形狀類,都必須實(shí)現(xiàn)這個(gè)函數(shù),這樣就不需要修改原有的類了:
interface Shape {
calculateArea(): number;
}
class Rectangle implements Shape {
public width: number;
public height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
public calculateArea(): number {
return this.width * this.height;
}
}
class Circle implements Shape {
public radius: number;
constructor(radius: number) {
this.radius = radius;
}
public calculateArea(): number {
return Math.PI * (this.radius * this.radius);
}
}
class AreaCalculator {
public calculateArea(shape: Shape): number {
return shape.calculateArea();
}
}
里氏替換原則(LSP)
核心思想:在使用基類的的地方可以任意使用其子類,能保證子類完美替換基類。簡(jiǎn)單理解就是所有父類能出現(xiàn)的地方,子類就可以出現(xiàn),并且替換了也不會(huì)出現(xiàn)任何錯(cuò)誤。
我們必須要求子類的所有相同方法,都必須遵循父類的約定,否則當(dāng)父類替換為子類時(shí)就會(huì)出錯(cuò)。
先來(lái)看看下面這段代碼,Square 類擴(kuò)展了 Rectangle 類。但是這個(gè)擴(kuò)展沒(méi)有任何意義,因?yàn)槲覀兺ㄟ^(guò)覆蓋寬度和高度屬性來(lái)改變了原有的邏輯。
class Rectangle {
public width: number;
public height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
public calculateArea(): number {
return this.width * this.height;
}
}
class Square extends Rectangle {
public _width: number;
public _height: number;
constructor(width: number, height: number) {
super(width, height);
this._width = width;
this._height = height;
}
}
遵循里氏替換原則,我們不需要覆蓋基類的屬性,而是直接刪除掉 Square 類并,將它的邏輯帶到 Rectangle 類,而且也不改變其用途。
class Rectangle {
public width: number;
public height: number;
constructor(width: number, height: number) {
this.width = width;
this.height = height;
}
public calculateArea(): number {
return this.width * this.height;
}
public isSquare(): boolean {
return this.width === this.height;
}
}
好處:增強(qiáng)程序的健壯性,即使增加了子類,原有的子類還可以繼續(xù)運(yùn)行。
接口隔離原則(ISP)
核心思想:類間的依賴關(guān)系應(yīng)該建立在最小的接口上。簡(jiǎn)單理解就是接口的內(nèi)容一定要盡可能地小,能有多小就多小。我們要為各個(gè)類建立專用的接口,而不要試圖去建立一個(gè)很龐大的接口供所有依賴它的類去調(diào)用。
看看下面的代碼,我們有一個(gè)名為 Troll 的類,它實(shí)現(xiàn)了一個(gè)名為 Character 的接口,但是 Troll 既不會(huì)游泳也不會(huì)說(shuō)話,所以它似乎不太適合實(shí)現(xiàn)我們的接口:
interface Character {
shoot(): void;
swim(): void;
talk(): void;
dance(): void;
}
class Troll implements Character {
public shoot(): void {
// some method
}
public swim(): void {
// a troll can't swim
}
public talk(): void {
// a troll can't talk
}
public dance(): void {
// some method
}
}
遵循接口隔離原則,我們刪除 Character 接口并將它的功能拆分為四個(gè)接口,然后我們的 Troll 類只需要依賴于我們實(shí)際需要的這些接口。
interface Talker {
talk(): void;
}
interface Shooter {
shoot(): void;
}
interface Swimmer {
swim(): void;
}
interface Dancer {
dance(): void;
}
class Troll implements Shooter, Dancer {
public shoot(): void {
// some method
}
public dance(): void {
// some method
}
}
依賴倒置原則(DIP)
核心思想:依賴一個(gè)抽象的服務(wù)接口,而不是去依賴一個(gè)具體的服務(wù)執(zhí)行者,從依賴具體實(shí)現(xiàn)轉(zhuǎn)向到依賴抽象接口,倒置過(guò)來(lái)。
看看下面這段代碼,我們有一個(gè) SoftwareProject 類,它初始化了 FrontendDeveloper 和 BackendDeveloper 類:
class FrontendDeveloper {
public writeHtmlCode(): void {
// some method
}
}
class BackendDeveloper {
public writeTypeScriptCode(): void {
// some method
}
}
class SoftwareProject {
public frontendDeveloper: FrontendDeveloper;
public backendDeveloper: BackendDeveloper;
constructor() {
this.frontendDeveloper = new FrontendDeveloper();
this.backendDeveloper = new BackendDeveloper();
}
public createProject(): void {
this.frontendDeveloper.writeHtmlCode();
this.backendDeveloper.writeTypeScriptCode();
}
}
遵循依賴倒置原則,我們創(chuàng)建一個(gè) Developer 接口,由于 FrontendDeveloper 和 BackendDeveloper 是相似的類,它們都依賴于 Developer 接口。
我們不需要在 SoftwareProject 類中以單一方式初始化 FrontendDeveloper 和 BackendDeveloper,而是將它們作為一個(gè)列表來(lái)遍歷它們,分別調(diào)用每個(gè) develop() 方法。
interface Developer {
develop(): void;
}
class FrontendDeveloper implements Developer {
public develop(): void {
this.writeHtmlCode();
}
private writeHtmlCode(): void {
// some method
}
}
class BackendDeveloper implements Developer {
public develop(): void {
this.writeTypeScriptCode();
}
private writeTypeScriptCode(): void {
// some method
}
}
class SoftwareProject {
public developers: Developer[];
public createProject(): void {
this.developers.forEach((developer: Developer) => {
developer.develop();
});
}
}
好處:實(shí)現(xiàn)模塊間的松耦合,更利于多模塊并行開(kāi)發(fā)。