聊聊OOP中的設計原則以及訪問者模式

来源:https://www.cnblogs.com/dtux/archive/2022/06/02/16336818.html
-Advertisement-
Play Games

一 設計原則 (SOLID) 1. S - 單一職責原則(Single Responsibllity Principle) 1.1 定義 一個類或者模塊只負責完成一個職責(或功能), 認為“對象應該僅具有一種單一功能”的概念, 如果一個類包含了兩個或兩個以上業務沒有關聯的功能,就被認為是職責不夠單一 ...


一  設計原則 (SOLID)

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

1.1  定義

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

1.2 舉個慄子

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

file

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

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

2.1 定義

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

  • 對擴展開放

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

  • 對修改關閉

模塊對修改關閉,表示當需求變化時,應該儘量在不修改源代碼的基礎上面擴展功能

2.2 舉個慄子

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

Order

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

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

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 現實中的慄子

以電動自行車為例

file

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

file

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 對象。這是實現訪問者模式的關鍵

file

可以看到,要實現操作權轉讓到 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. 訪問者模式優缺點

優勢:

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

不足:

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

您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • CITY表: Field Type ID number NAME VARCHAR2(17) COUNTRYCODE VARCHAR2(3) DISTRICT VARCHAR2(20) POPULATION number 1.Query all columns for all American cit ...
  • Mysql 5.7的安裝搭建 首先去到官方網站的下載鏈接中找到對應你Linux伺服器版本的mysql軟體包 https://dev.mysql.com/downloads/repo/yum/ 我使用的是CentOS7所以下載Red HAT Enterprise Linux 7版本的軟體包,然後跳轉至 ...
  • 華為開發者聯盟與艾瑞咨詢聯合發佈《2022年移動應用出海趨勢洞察白皮書》,本白皮書結合多種研究方法分析移動應用的出海吸引力、海外市場選擇、出海的痛點及挑戰,為現階段移動應用企業的出海戰略決策提供市場洞察,並針對出海挑戰呈現應對策略,助力移動應用開發者出海。 華為開發者聯盟一直致力於全方位聯接全球開發 ...
  • 華為帳號服務(Account Kit)為開發者提供簡單、安全的登錄授權功能,用戶不必輸入帳號、密碼和繁瑣驗證,就可以通過華為帳號快速登錄應用,即刻使用App。這篇文章收集了開發者們集成華為帳號服務中會遇到的典型問題,並給出瞭解決方法,希望為其他遇到類似問題的開發者提供參考。 1 .redirect_ ...
  • 一. 在iview中寫一個submenu <Col span="3" type="flex" v-if="showCids"> <Menu ref="menus" theme="light" active-name="0" @on-select="selectMenu" width="auto" > ...
  • 1.背景 業務需求,需要聯動多個平臺,涉及到各平臺的模擬登錄。 已知加密前明文且正常登錄。(無驗證碼要求) 某平臺驗證驗證方式為.\login介面POST一串json字元串 { "account": "********", "password": "uR+dmpMdF9MRXfkBG3wQ+w==" ...
  • 問題描述 前端頁面載入css,和js文件的時候,經常出現ERR_CONTENT_LENGTH_MISMATCH的報錯情況 定位問題 在單獨打開hearder中css,js的網路地址是能打開的,所以排除了最簡單的地址錯誤。前端項目是由nginx代理的,所以可以查看nginx的日誌,看看有無線索。 進入 ...
  • 前言 在平常的後端項目開發中,狀態機模式的使用其實沒有大家想象中那麼常見,筆者之前由於不在電商領域工作,很少在業務代碼中用狀態機來管理各種狀態,一般都是手動get/set狀態值。去年筆者進入了電商領域從事後端開發。電商領域,狀態又多又複雜,如果仍然在業務代碼中東一塊西一塊維護狀態值,很容易陷入出了問 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...