聊聊OOP中的設計原則以及存取者模式

2022-06-02 12:00:21

一  設計原則 (SOLID)

1.  S - 單一職責原則(Single Responsibllity Principle)

1.1  定義

一個類或者模組只負責完成一個職責(或功能), 認為「物件應該僅具有一種單一功能」的概念, 如果一個類包含了兩個或兩個以上業務沒有關聯的功能,就被認為是職責不夠單一,可以差分成多個功能單一的類

1.2 舉個栗子

Employee 類裡面包含了多個不同的行為, 違背了單一指責原則

通過拆分出 TimeSheetReport 類, 依賴了 Employee 類, 遵循單一指責原則

2.  O - 開放關閉原則(Open-Closed Principle)

2.1 定義

軟體實體(包括類、模組、功能等)應該對擴充套件開放,但是對修改關閉, 滿足以下兩個特性

  • 對擴充套件開放

模組對擴充套件開放,就意味著需求變化時,可以對模組擴充套件,使其具有滿足那些改變的新行為

  • 對修改關閉

模組對修改關閉,表示當需求變化時,應該儘量在不修改原始碼的基礎上面擴充套件功能

2.2 舉個栗子

在訂單中需要根據不同的運輸方式計算運輸成本

Order

類中計算運輸成本,如果後續再增加新的運輸方式,就需要修改Order原來的方法getShippingCost() , 違背了OCP

根據多型的思想,可以將 shipping 抽象成一個類, 後續新增運輸方式, 無須修改Order 類原有的方法,
只需要在增加一個Shipping的派生類就可以了

3.  L - 里氏替換原則(Liskov Substitution Principle)

3.1 定義

使用父類別的地方都可以用子類替代,子類能夠相容父類別

  • 子類方法的引數型別應該比父類別方法的引數型別更抽象或者說範圍更廣
  • 子類方法的返回值型別應該比父類別方法的返回值型別更具體或者說範圍更小

3.2 舉個栗子

子類方法的引數型別應該比父類別方法的引數型別更抽象或者說範圍更廣
演示 demo

class Animal {}
class Cat extends Animal {
  faviroteFood: string;
  constructor(faviroteFood: string) {
    super();
    this.faviroteFood = faviroteFood;
  }
}

class Breeder {
  feed(c: Animal) {
    console.log("Breeder feed animal");
  }
}

class CatCafe extends Breeder {
  feed(c: Animal) {
    console.log("CatCafe feed animal");
  }
}

const animal = new Animal();

const breeder = new Breeder();
breeder.feed(animal);
// 約束子類能夠接受父類別入參
const catCafe = new CatCafe();
catCafe.feed(animal);
  • 子類方法的返回值型別應該比父類別方法的返回值型別更具體或者說範圍更小
class Animal {}

class Cat extends Animal {
  faviroteFood: string;
  constructor(faviroteFood: string) {
    super();
    this.faviroteFood = faviroteFood;
  }
}

class Breeder {
  buy(): Animal {
    return new Animal();
  }
}

class CatCafe extends Breeder {
  buy(): Cat {
    return new Cat("");
  }
}

const breeder = new Breeder();
let a: Animal = breeder.buy();

const catCafe = new CatCafe();
a = catCafe.buy();
  • 子類不應該強化前置條件
  • 子類不應該弱化後置條件

4.  I - 介面隔離原則(Interface Segregation Principle)

4.1 定義

使用者端不應該依賴它不需要的介面, 一個類對另一個類的依賴應該建立在最小的介面

4.2 舉個栗子

類 A 通過介面 I 依賴類 B,類 C 通過介面 I 依賴類 D,如果介面 I 對於類 A 和類 B 來說不是最小介面,則類 B 和類 D 必須去實現他們不需要的方法

interface I {
  m1(): void;
  m2(): void;
  m3(): void;
  m4(): void;
  m5(): void;
}

class B implements I {
  m1(): void {}
  m2(): void {}
  m3(): void {}
  //實現的多餘方法
  m4(): void {}
  //實現的多餘方法
  m5(): void {}
}

class A {
  m1(i: I): void {
    i.m1();
  }
  m2(i: I): void {
    i.m2();
  }
  m3(i: I): void {
    i.m3();
  }
}

class D implements I {
  m1(): void {}
  //實現的多餘方法
  m2(): void {}
  //實現的多餘方法
  m3(): void {}
  
  m4(): void {}
  m5(): void {}
}

class C {
  m1(i: I): void {
    i.m1();
  }
  m4(i: I): void {
    i.m4();
  }
  m5(i: I): void {
    i.m5();
  }
}

將臃腫的介面 I 拆分為獨立的幾個介面,類 A 和類 C 分別與他們需要的介面建立依賴關係

interface I {
  m1(): void;
}

interface I2 {
  m2(): void;
  m3(): void;
}

interface I3 {
  m4(): void;
  m5(): void;
}

class B implements I, I2 {
  m1(): void {}
  m2(): void {}
  m3(): void {}
}

class A {
  m1(i: I): void {
    i.m1();
  }
  m2(i: I2): void {
    i.m2();
  }
  m3(i: I2): void {
    i.m3();
  }
}

class D implements I, I3 {
  m1(): void {}
  m4(): void {}
  m5(): void {}
}

class C {
  m1(i: I): void {
    i.m1();
  }
  m4(i: I3): void {
    i.m4();
  }
  m5(i: I3): void {
    i.m5();
  }
}

4.3 現實中的栗子

以電動自行車為例

普通的電動自行車並沒有定位和檢視歷史行程的功能,但由於實現了介面 ElectricBicycle ,所以必須實現介面中自己不需要的方法。更好的方式是進行拆分

5.   D - 依賴倒置原則

5.1 定義

依賴一個抽象的服務介面,而不是去依賴一個具體的服務執行者,從依賴具體實現轉向到依賴抽象介面,倒置過來
在軟體設計中可以將類分為兩個級別:高層模組, 低層模組, 高層模組不應該依賴低層模組,兩者都應該依賴其抽象。高層模組指的是呼叫者,低層模組指的是一些基礎操作

依賴倒置基於這個事實:相比於實現細節的多變性,抽象的內容要穩定的多

5.2 舉個栗子

SoftwareProject類直接依賴了兩個低階類, FrontendDeveloperBackendDeveloper, 而此時來了一個新的低層模組,就要修改 高層模組 SoftwareProject 的依賴

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();
  }
}

可以遵循依賴倒置原則, 由於 FrontendDeveloper 和 BackendDeveloper是相似的類, 可以抽象出一個 develop 介面, 讓FrontendDeveloperBackendDeveloper 去實現它, 我們不需要在 SoftwareProject類中以單一方式初始化 FrontendDeveloper 和 BackendDeveloper,而是將它們作為一個列表來遍歷它們,分別呼叫每個 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();
    });
  }
}

二  存取者模式 (Visitor Pattern)

1.  意圖

表示一個作用於某物件結構中的各元素的操作。它使你可以在不改變各元素的類的前提下定義作用於這些元素的新操作

  • Visitor的作用,即 作用於某物件結構中的各元素的操作,也就是 Visitor 是用於操作物件元素的
  • 它使你可以在不改變各元素的類的前提下定義作用於這些元素的新操作 也就是說,你可以只修Visitor 本身完成新操作的定義,而不需要修改原本物件, Visitor設計奇妙之處, 就是將物件的操作權移交給了 Visitor

2. 場景

  • 如果你需要對一個複雜物件結構 (例如物件樹) 中的所有元素執行某些操作, 可使用存取者模式
  • 存取者模式通過在存取者物件中為多個目標類提供相同操作的變體, 讓你能在屬於不同類的一組物件上執行同一操作

3.  存取者模式結構

  • Visitor:存取者介面
  • ConcreteVisitor:具體的存取者
  • Element: 可以被存取者使用的元素,它必須定義一個 Accept 屬性,接收 visitor 物件。這是實現存取者模式的關鍵

可以看到,要實現操作權轉讓到 Visitor,核心是元素必須實現一個 Accept 函數,將這個物件拋給 Visitor

class ConcreteElement implements Element {
  public accept(visitor: Visitor) {
    visitor.visit(this)
  }
}

從上面程式碼可以看出這樣一條鏈路:Element 通過 accept函數接收到 Visitor 物件,並將自己的範例拋給 Visitor 的 visit函數,這樣我們就可以在 Visitor 的 visit 方法中拿到物件範例,完成對物件的操作

4 . 實現方式以及虛擬碼

在本例中, 存取者模式為幾何影象層次結構新增了對於 XML 檔案匯出功能的支援

4.1  在存取者介面中宣告一組 「存取」 方法, 分別對應程式中的每個具體元素類

interface Visitor {
  visitDot(d: Dot): void;
  visitCircle(c: Circle): void;
  visitRectangle(r: Rectangle): void;
}

4.2  宣告元素介面。 如果程式中已有元素類層次介面, 可在層次結構基礎類別中新增抽象的 「接收」 方法。 該方法必須接受存取者物件作為引數

interface Shape {
  accept(v: Visitor): void;
}

4.3  在所有具體元素類中實現接收方法, 元素類只能通過存取者介面與存取者進行互動,不過存取者必須知曉所有的具體元素類, 因為這些類在存取者方法中都被作為引數型別參照

class Dot implements Shape {
  public accept(v: Visitor): void {
   return v.visitDot(this)
  }
}

class Circle implements Shape {
  public accept(v: Visitor): void {
   return v.visitCircle(this)
  }
}

class Rectangle implements Shape {
  public accept(v: Visitor): void {
    return v.visitRectangle(this)
  }
}

4.4 建立一個具體存取者類並實現所有的存取者方法

class XMLExportVisitor implements Visitor {
    visitDot(d: Dot): void {
      console.log(`匯出點(dot)的 ID 和中心座標`);
    }
    visitCircle(c: Circle): void {
      console.log(`匯出圓(circle)的 ID 、中心座標和半徑`);
    }
    visitRectangle(r: Rectangle): void {
      console.log(`匯出長方形(rectangle)的 ID 、左上角座標、寬和長`);
    }
}

4.5  使用者端必須建立存取者物件並通過 「接收」 方法將其傳遞給元素

const application = (shapes:Shape[],visitor:Visitor) => {
  // ......
   for (const shape of  allShapes) {
      shape.accept(visitor);
    }
  // ......
}
	
const allShapes = [
    new Dot(),
    new Circle(),
    new Rectangle()
];

const xmlExportVisitor = new XMLExportVisitor();
application(allShapes, xmlExportVisitor);

4.6 完整程式碼預覽

interface Visitor {
    visitDot(d: Dot): void;
    visitCircle(c: Circle): void;
    visitRectangle(r: Rectangle): void;
}

interface Shape {
   accept(v: Visitor): void;
}

class Dot implements Shape {
  public accept(v: Visitor): void {
     return v.visitDot(this)
  }
}

class Circle implements Shape {
  public accept(v: Visitor): void {
    return v.visitCircle(this)
  }
}

class Rectangle implements Shape {
  public accept(v: Visitor): void {
    return v.visitRectangle(this)
  }
}

class XMLExportVisitor implements Visitor {
    visitDot(d: Dot): void {
      console.log(`匯出點(dot)的 ID 和中心座標`);
    }
    visitCircle(c: Circle): void {
      console.log(`匯出圓(circle)的 ID 、中心座標和半徑`);
    }
    visitRectangle(r: Rectangle): void {
      console.log(`匯出長方形(rectangle)的 ID 、左上角座標、寬和長`);
    }
}

const allShapes = [
    new Dot(),
    new Circle(),
    new Rectangle()
];

const application = (shapes:Shape[],visitor:Visitor) => {
  // ......
for (const shape of  allShapes) {
    shape.accept(visitor);
  // .....
}
	
const xmlExportVisitor = new XMLExportVisitor();
application(allShapes, xmlExportVisitor);

5. 存取者模式優缺點

優勢:

  • 開閉原則。 你可以引入在不同類物件上執行的新行為, 且無需對這些類做出修改
  • 單一職責原則 可將同一行為的不同版本移到同一個類中

不足:

  • 每次在元素層次結構中新增或移除一個類時, 你都要更新所有的存取者
  • 在存取者同某個元素進行互動時, 它們可能沒有存取元素私有成員變數和方法的必要許可權