設計類和物件的結構型模式
持續創作,加速成長!這是我參與「掘金日新計劃 · 6 月更文挑戰」的第 5 天,點選檢視活動詳情。
- 原文地址:Design Patterns: Structural Patterns of Design Classes and Objects
- 原文作者:Trung Anh Dang
- 譯文出自:掘金翻譯計劃
- 本文永久連結:https://github.com/xitu/gold-miner/blob/master/article/2020/design-patterns-structural-patterns-of-design-classes-and-objects.md
- 譯者:lhd951220、jaredliw
- 校對者:PingHGao、jackwener、finalwhy
介面卡模式、裝飾器模式、代理模式、資訊專家、組合模式、橋接模式、享元模式、受保護變化和門面模式
結構性設計模式主要關注如何通過組合類與物件形成更大的結構。它們能讓你在不重寫或者不自定義程式碼的情況下建立系統,因為這些模式提高了系統的複用性和健壯性。
每一個模式都描述了一種在我們周圍環境中反覆出現的問題與解決此類問題的方案的核心。通過這樣的方式,你可以反覆地使用這些方案,而不需要重複相同的(尋找解決之道的)工作。—— 克里斯托弗·亞歷山大
本文將討論以下幾種設計模式和原則。
- 介面卡模式
- 橋接模式
- 組合模式
- 裝飾器模式
- 享元模式
- 門面模式
- 代理模式
- 資訊專家
- 受保護變化
介面卡(Adapter)、橋接(Bridge)、組合(Composite)和裝飾器(Decorator)模式
介面卡模式
目的
介面卡模式是結構型設計模式,它能讓具有不相容介面的物件協同運作。
解決方案
它實現了一個客戶端已知的介面,為客戶端提供訪問某一未知類的例項的訪問途徑。
AdapterClient
:客戶端程式碼。Adapter
:將呼叫轉發到Adaptee
類。Adaptee
:需要被適配的舊程式碼。Target
:支援的新介面。
實際案例
原電壓是 220 V,需要適配到 100 V 才能運作。
下面的程式碼會解決這個問題。它定義了一個 HighVoltagePlug
(被適配者),一個 Plug
介面和一個 AdapterPlug
(介面卡)。
Target
:Plug.java
java
public interface Plug {
public int recharge();
}
Adaptee
:HighVoltagePlug.java
java
public class HighVoltagePlug {
public int recharge() {
// 電壓是 220 V
return 220;
}
}
Adapter
:AdapterPlug.java
java
public class AdapterPlug implements Plug {
@Override
public int recharge() {
HighVoltagePlug bigPlug = new HighVoltagePlug();
int v = bigPlug.recharge();
v = v - 120;
return v;
}
}
AdapterClient
:AdapterClient.java
```java public class AdapterClient { public static void main(String[] args) { HighVoltagePlug oldPlug = new HighVoltagePlug(); System.out.println(plug.recharge() + " too much voltage");
Plug newPlug = new AdapterPlug();
System.out.println("Adapter into " + plug.recharge() + " voltage");
}
} ```
用例
- 當你想要使用一個已經存在的類,但是它實現的介面與你的需求不匹配時。
- 當你想要建立一個可複用的類,且這個類需要與不相關或介面不相容的類協同運作時。
- 必須在多個源之間進行介面轉換的情況。
橋接模式
目的
它將複雜的元件分為兩個既相互獨立又相互關聯的層次結構,分別是功能的抽象和內部的實現。
解決方案
下面的圖展示了一個可行的橋接實現。
Abstraction
:這是抽象元件。Implementor
:這是抽象的實現。RefinedAbstraction
:這是精確化後元件。ConcreateImplementors
:這些是具體的實現。
實際案例
不同的人(比如:男人、女人、男孩和女孩)會穿不同的衣服。
AbstractionImpl
:Person.java
```java public abstract class Person { protected String name; protected Clothing clothes;
public Person(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Clothing getClothes() {
return clothes;
}
public void setClothes(Clothing clothes) {
this.clothes = clothes;
}
} ```
Implementor
:Clothing.java
```java public abstract class Clothing { protected String name;
public Clothing(String name) {
super();
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
} ```
ConcreteImplementor
:Jacket.java
java
public class Jacket extends Clothing {
public Jacket(String name) {
super(name);
}
}
RefinedAbstraction
:Woman.java
```java public class Woman extends Person { public Woman(String name) { super(name); }
@Override
public void dress() {
System.out.println(name + " wear " + clothes.getName());
}
} ```
Client
:BridgeClient.java
```java public class BridgeClient { public static void main(String[] args) { Person woman = new Woman("Woman");
Clothing jacket = new Jacket("Jacket");
// 一個穿夾克的婦女
woman.setClothes(jacket);
woman.dress();
}
} ```
用例
- 避免永久繫結抽象類和實現類。
- 抽象及其實現都應該可以通過子類進行擴充套件。
- 改變抽象中的實現不應該對客戶端產生影響;也就是說,客戶端的程式碼不必重新編譯。
組合模式
目的
組合模式能讓你建立複雜程度不同的樹形結構,同時讓結構中的每個元素在統一的介面下運作。
解決方案
組合模式通過樹形結構將物件進行組合,以此來表現整個或部分的等級結構。
Component
是對整個樹形結構中各個組分的抽象。它定義了任何組分必須實現的介面。Leaf
是沒有children
屬性的物件。他們實現了由Component
介面描述的服務。Composite
儲存子元件,並且實現了Component
介面定義的方法。它通過委派子元件來實現Component
介面中定義的方法。除此之外,它也提供了新增、刪除和獲取元件的方法。Client
使用元件介面來控制層級中的物件。
實際案例
組織中會有總經理;在總經理之下,還有經理;在經理之下,還有開發人員。現在,你可以設定一個樹形結構,然後要求每個節點執行通用的操作,比如:printStructure()
。
Component
:IEmployee.java
java
interface IEmployee {
void printStructure();
int getEmployeeCount();
}
Composite
:CompositeEmployee.java
```java class CompositeEmployee implements IEmployee { private int employeeCount = 0; private String name; private String dept;
// 子物件的容器
private List<IEmployee> controls;
public CompositeEmployee(String name, String dept) {
this.name = name;
this.dept = dept;
controls = new ArrayList<IEmployee>();
}
public void addEmployee(IEmployee e) {
controls.add(e);
}
public void removeEmployee(IEmployee e) {
controls.remove(e);
}
@Override
public void printStructure() {
System.out.println("\t" + this.name + " works in " + this.dept);
for (IEmployee e: controls) {
e.printStructure();
}
}
@Override
public int getEmployeeCount() {
employeeCount = controls.size();
for (IEmployee e: controls) {
employeeCount += e.getEmployeeCount();
}
return employeeCount;
}
} ```
Leaf
:Employee.java
```java class Employee implements IEmployee { private String name; private String dept; private int employeeCount = 0;
public Employee(String name, String dept) {
this.name = name;
his.dept = dept;
}
@Override
public void printStructure() {
System.out.println("\t\t" + this.name + " works in " + this.dept);
}
@Override
public int getEmployeeCount() {
return employeeCount;
}
} ```
用例
- 你想要表示物件的整個或部分層次結構。客戶端(在訪問時)可以忽略物件組合與獨立物件之間的差異。
- 你可以將這個模式應用在具有任意複雜度等級的結構上。
裝飾器模式
目的
裝飾器模式能讓你在不改變物件外觀和已有功能的情況下新增和刪除物件的功能。
解決方案
它通過使用原始類的子類例項來將操作委託給原始物件,從而以對客戶端透明的方式來改變一個物件的功能。
Component
是物件的介面,可以動態新增功能。ConcreteComponent
定義了一個可以新增其他職責的物件。Decorator
儲存了一個Component
物件的引用,並且定義了一個符合Component
介面的介面。ConcreteDecorator
通過新增狀態或行為來擴充套件元件的功能。
實際案例
你已經擁有一個自己的房子。現在,你決定在這基礎上修建額外的一個樓層。你也許希望更改新樓層的結構設計,而又不想對現有的結構造成影響。
用例
- 動態並且透明地為單個物件新增職責,而不影響其他物件。
- 當你想為之後可能會更改的物件新增職責時。
- 在無法通過靜態子類進行擴充套件的情況。
享元(Flyweight)、門面(Farcade)和代理(Proxy)模式
享元模式
目的
享元模式通過共享物件來減少系統中低階且詳細的物件。
解決方案
下面的圖展示了從池(pool
)中返回享元物件(ConcreteFlyweight
)並對其進行操作(doOperation()
)的流程;該方法需要一個外部狀態引數(extrinsicState
)。
Client
:客戶端程式碼。FlyweightFactory
:如果享元物件不存在則會建立;如果存在,則直接從池中返回。Flyweight
:抽象享元物件。ConcreateFlyweight
:享元物件,與同級物件共享狀態。
實際案例
這種用法的一個典型用例是文書處理器。在這個場景中,每一個字元都是一個享元物件,都共享了渲染所需的資料。因此,在文件中,只有有字元的位置會消耗額外的記憶體。
用例
當滿足以下所有條件時,你應該使用享元模式。
- 應用程式中了使用大量的物件。
- 高儲存成本(因物件數量過多)。
- 應用程式不依賴於物件標識。
門面模式
目的
門面模式為子系統中的一組介面提供了統一的介面。
解決方案
它定義了一個更高階的介面,使子系統更易於使用(因為介面只有一個)。
實際案例
門面為子系統定義了一個統一的、高階的介面,使其更易於使用。舉例來說,在訂購商品時,消費者撥打服務熱線並與客服通話。這時客服扮演了“門面”的角色,它提供了訂單執行部門,結賬部門和運輸部門的介面。
用例
- 當你想為一個複雜的子系統提供一個簡單的介面時。
- 當客戶端與抽象類的實現之間存在很多依賴時。
- 當你想要為你的子系統分層時。
代理模式
代理模式有多種實現方式,其中屬遠端代理和虛擬代理最為常見。
目的
代理模式提供了一個替代物件或者佔位物件,以此來控制對原始物件的訪問。
解決方案
實際案例
一個現實世界中的案例可以是一張支票或者一張銀行卡;它們代理了我們的銀行賬戶。它們可以用來代替現金,並在你需要時提供一種獲取現金的方法。這正是代理模式的作用 —— “控制和管理其保護物件的訪問”。
用例
當你需要比普通指標更通用或更復雜的物件引用時。
GRASP 設計原則
GRASP 羅列並描述了 9 種分配職責的基本原則,本文僅對其中有關的兩種進行討論。
資訊專家(Information Expert)
讓我們看看看專家原則(或著資訊專家原則)。這個原則很簡單,但也非常重要。
目的
為物件分配職責的基本原則是什麼?
解決方案
將職責分配給具有履行職責所需資訊的類。
實際案例
以大富翁遊戲為例。假設一個物件要引用一個給定名字的棋格。誰應該負責獲取這個棋格的資訊?
最有可能的選項是遊戲棋盤,因為它是由一個個棋格組成的。
在這個場景中,棋盤是資訊專家,它具有履行此職責所需要的全部資訊。
用例
將設計模式中的物件視為你管理的工作人員。如果你要分配一個任務,你會把它分配給誰?
- 你會將任務分配給對這個任務擁有最多資訊的人。
- 有時,任務的資訊會散佈在多個物件上;你可以通過物件之間的交流來完成任務,但通常完成任務的職責只在一個物件上。
保護變化(Protected Variations)
目的
如何設計物件、系統以及子系統才能避免其中的可變性和不穩定性對元素產生不良影響?
解決方案
識別可變和不穩定的點,分配職責並圍繞這些點建立一個穩定的介面。
“不要跟陌生人交流”原則指出一個物件的方法應該僅給那些直接相關的物件傳送資訊。
相關閱讀:迪米特法則
實際案例
資料封裝、介面、多型性、間接引用和標準都基於受保護變化原則。
用例
受保護變化是一個根本原則;它推動著程式設計和設計中的大多數機制和模式,為資料、行為、硬體、軟體及作業系統中的變化提供靈活性和保護。
總結
結構型設計模式以各種方式影響著應用。舉例來說,介面卡模式能讓兩個毫不相干的系統進行交流;門面模式能為使用者提供一個簡化的介面,同時不用移除系統中所有可用的選項。
設計結構並不難,你說是吧?
如果發現譯文存在錯誤或其他需要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可獲得相應獎勵積分。文章開頭的 本文永久連結 即為本文在 GitHub 上的 MarkDown 連結。
掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。
- 如何為空間索引建立高效的資料結構?
- 什麼是移動端 DevOps,為什麼值得你關注?
- 設計類和物件的結構型模式
- Android 開發中的 SQLDelight 入門
- 使用了三個月的 Github Copilot,這是我的一些看法……
- HTTP/3 為什麼這麼快?
- 【譯】3 個能優化網站可用性但被忽視的細節
- 【譯】為什麼你應該使用 PUT 請求而不是 POST 請求?
- pipe 庫原始碼剖析 | Python 類裝飾器和運算子過載
- pipe:向 map 和 filter 說拜拜!
- 使用 Python 建立你自己的 NFT 集合
- 【譯】IDOR 攻擊與賬戶接管:我在 16 歲時寫的第一個漏洞報告
- 一個 Python 中讀寫 Markdown 的庫:mdutils
- 盤一盤 Python 3.10 和 3.11 的新特性:Python 竟開始支援 switch-case 語句了?!