設計模式詳解

Phantasia1116發表於2024-05-09

本文結構圖:

除了類本身,設計模式更強調多個類/物件之間的關係和互動過程——比介面/類複用的粒度更大

建立型模式(Creational patterns)

工廠方法模式(Factory Method pattern)

工廠方法也被稱作虛擬構造器(Virtual Constructor)

即定義一個用於建立物件的介面,讓子類類決定例項化哪個類,也就是說,工廠方法允許類將例項化推遲到子類

什麼時候用?

  • 當使用者不確定要建立哪個具體類的具體例項,或者不想在程式碼中指明要具體建立的例項時,用工廠方法

舉例

設計一個Trace介面:

public interface Trace {
    // turn on and off debugging
    public void setDebug( boolean debug );
    // write out a debug message
    public void debug( String message );
    // write out an error message
    public void error( String message );
}

我們可能要設計兩種實現,比如將資訊直接列印到螢幕,或者輸出到檔案:

// Concrete product 1
public class FileTrace implements Trace {
    private PrintWriter pw;
    private boolean debug;
    public FileTrace() throws IOException {
        pw = new PrintWriter( new FileWriter( "t.log" ) );
    }
    public void setDebug( boolean debug ) {
        this.debug = debug;
    }
    public void debug( String message ) {
        if( debug ) {
            pw.println( "DEBUG: " + message );
            pw.flush();
        }
    }
    public void error( String message ) {
        pw.println( "ERROR: " + message );
        pw.flush();
    }
}

// Concret product 2
public class SystemTrace implements Trace {
    private boolean debug;
    public void setDebug( boolean debug ) {
        this.debug = debug;
    }
    public void debug( String message ) {
        if( debug )
            System.out.println( "DEBUG: " + message );
    }
    public void error( String message ) {
        System.out.println( "ERROR: " + message );
    }
}

那麼使用者要怎麼使用呢?

//... some code ...
Trace log1 = new SystemTrace();
log1.debug("entering log");
Trace log2 = new FileTrace();
log2.debug("... ");

這樣做,客戶端在建立物件時就要指定具體實現類,也就是說客戶端的程式碼與實現類緊密耦合,如果有了新的具體類新增時,客戶端的程式碼很可能也要修改

那麼利用工廠方法該怎麼實現呢?

先設計一個用於建立物件的介面:

interface TraceFactory {
    Trace getTrace();
    void otherOperation();
}

然後分別設計兩個工廠類,並設計工廠方法返回對應的具體類:

public class SystemTraceFactory implements TraceFactory {
    public Trace getTrace() {
        ... //other operations
        return new SystemTrace();
    }
}
public class FileTraceFactory implements TraceFactory {
    public Trace getTrace() {
        return new FileTrace();
    }
}

客戶端使用工廠方法來建立例項,這樣在有新的具體產品類加入時,可以增加新的工廠類或修改已有工廠類,不會影響到客戶端程式碼,這樣使用:

//... some code ...
Trace log1 = new SystemTraceFactory().getTrace();
log1.debug("entering log");
Trace log2 = new FileTraceFactory().getTrace();
log2.debug("...");

當然也可以有另一種實現方式,根據型別決定建立哪個具體產品

interface TraceFactory {
    Trace getTrace(String type);
    void otherOperation();
}
public class Factory implements TraceFactory {
    public getTrace(String type) {
        if(type.equals("file" )
            return new FileTrace();
        else if (type.equals("system")
            return new SystemTrace();
    }
}

客戶端程式碼:

Trace log = new Factory().getTrace("system");
log.setDebug(false);
log.debug("...");

甚至可以直接用靜態工廠方法實現:

public class SystemTraceFactory{
    public static Trace getTrace() {
        return new SystemTrace();
    }
}
public class TraceFactory {
    public static Trace getTrace(String type) {
        if(type.equals("file")
            return new FileTrace();
        else if (type.equals("system")
            return new SystemTrace();
    }
}

這樣客戶端直接用類名呼叫就可以得到相應的物件

相比於透過構造器(new)建立物件:

  • 靜態工廠方法可具有指定的更有意義的名稱
  • 不必在每次呼叫的時候的都建立新的工廠物件
  • 可以返回原返回型別的任意子型別

優點與不足分析

優點

  • 不會在客戶端程式碼中含有特定的實現類
  • 程式碼只處理介面,所以它可以與使用者定義的實現類一起使用

不足:

  • 每增加一種產品就需要增加一個新的工廠子類

結構型模式(Structural patterns)

介面卡模式(Adapter)

介面卡模式意圖將某個類/介面轉換為使用者期望的其他形式,它可以

  • 解決類之間介面不相容的問題
  • 透過增加一個介面,將以存在的子類封裝起來,使用者只需面向介面程式設計,從而隱藏了具體子類

說白了,就是將舊的元件包裝(wrapper)一下,用於新系統,下圖就很直觀:

介面卡可以用兩種形式實現:繼承(Inheritance)和委託(Delegation)

舉例

比如有一個顯示矩形圖案的程式,LegacyRectangle中的display()方法接受左上角座標(x, y),寬(w),高(h)四個引數來實現功能

但是客戶端想要透過傳遞左上角和右下角的座標來實現功能

這樣就造成了介面與使用者期望的不協調,這種不協調就可以透過一個介面卡物件利用委託機制來解決:

程式碼:

// Adaptor類實現抽象介面
interface Shape {
    void display(int x1, int y1, int x2, int y2);
}
//具體實現方法的適配
class Rectangle implements Shape {
    void display(int x1, int y1, int x2, int y2) {
        new LegacyRectangle().display(x1, y1, x2-x1, y2-y1);
    }
}
//原始的類
class LegacyRectangle {
    void display(int x1, int y1, int w, int h) {...}
}
//對介面卡程式設計,與LegacyRectangle隔離
class Client {
    Shape shape = new Rectangle();
    public display() {
        shape.display(x1, y1, x2, y2);
    }
}

裝飾器模式(Decorator)

裝飾模式允許透過將物件放入包含行為的特殊封裝物件中來為原物件繫結新的行為

假設要為物件增加不同側面的特性,那麼就可以透過裝飾器模式來解決,為每一個特性構造子類,透過委託機制增加到物件上

  • Component介面定義裝飾器執行的公共操作
  • ConcreteComponent起始物件,在其基礎上增加功能,將通用的方法放到此物件中
  • Decorator抽象類使所有裝飾類的基類,裡面包含的成員變數component指向了被裝飾的物件
  • ConcreteDecorator使新增了功能的類,每個類都代表一個可以新增的特性

舉例

比如,假設想要擴充套件Stack資料結構:

  • UndoStack:可以撤銷之前poppush操作的棧
  • SecureStack:需要密碼的棧
  • SynchronizedStack:序列化併發訪問的棧

我們可以透過繼承原始的父類來實現這些特性

但是如果需要這些特性的組合呢?比如:

  • SecureUndoStack:需要密碼的棧,還允許撤消之前的操作
  • SynchronizedUndoStack:序列化併發訪問的棧,還允許撤消之前的操作

那麼這個時候來怎麼辦呢?如果簡簡單單地繼續往下繼承:

這樣不僅會使繼承樹變得很深,還會在子類中多次實現相同的功能,有大量的程式碼重複

這時就能用裝飾器模式解決問題:

首先實現介面,完成最基礎的Stack功能:

interface Stack {
    void push(Item e);
    Item pop();
}
public class ArrayStack implements Stack {
        ... //rep
    public ArrayStack() {...}
    public void push(Item e) {
        ...
    }
    public Item pop() {
        ...
    }
        ...
}

然後設計Decorator基類:

public abstract class StackDecorator implements Stack {
    protected final Stack stack;
    public StackDecorator(Stack stack) {
        this.stack = stack;
    }
    public void push(Item e) {
        stack.push(e);
    }
    public Item pop() {
        return stack.pop();
    }
        ...
}

具體功能的實現類就透過委託基類實現:

public class UndoStack extends StackDecorator
{
    private final UndoLog log = new UndoLog();
    public UndoStack(Stack stack) {
        // 委託
        super(stack);
    }
    public void push(Item e) {
        super.push(e);
        
        // 新特性
        log.append(UndoLog.PUSH, e);
    }
    public void undo() {
        //implement decorator behaviors on stack
    }
        ...
}

如果客戶端只需要最基礎的功能:

Stack s = new ArrayStack();

如果需要某個特性,比如撤銷功能:

Stack t = new UndoStack(new ArrayStack());

如果既需要撤銷,又需要密碼功能SecureUndoStack,就可以這樣實現:

Stack t = new SecureStack(
                new SychronizedStack(
                        new UndoStack(s))
        )

客戶端需要的多個特性透過一層一層的裝飾來實現,就像一層一層的穿衣服

有同學可能對這個程式碼執行過程還存有疑問,我們再舉一個更直觀的例子:

我們設計一個冰淇凌,冰淇淋的頂部可以有多種水果:

分別設計頂層介面,基礎實現,和裝飾器基類:

public interface IceCream { //頂層介面
    void AddTopping();
}
public class PlainIceCream implements IceCream{ //基礎實現,無填加的冰淇淋
    @Override
    public void AddTopping() {
        System.out.println("Plain IceCream ready for some
                toppings!");
    }
}
/*裝飾器基類*/
public abstract class ToppingDecorator implements IceCream{
    protected final IceCream input;
    public ToppingDecorator(IceCream i){
        this.input = i;
    }
    public abstract void AddTopping(); //留給具體裝飾器實現
}

然後再新增不同的特性:

public class CandyTopping extends ToppingDecorator{
    public CandyTopping(IceCream i) {
        super(i);
    }
    public void AddTopping() {
        input.AddTopping(); //decorate others first
        System.out.println("Candy Topping added!");
    }
}
public class NutsTopping extends ToppingDecorator{
    //similar to CandyTopping
}
public class PeanutTopping extends ToppingDecorator{
    //similar to CandyTopping
}

當我們的客戶端需要一個新增Candy,Nuts,Peanut的冰淇淋時就可以這樣呼叫:

IceCream a = new PlainIceCream();
IceCream b = new CandyTopping(a);
IceCream c = new PeanutTopping(b);
IceCream d = new NutsTopping(c);
d.AddTopping();

當呼叫d.AddTopping()時:

  • 由於d是子類NutsTopping的物件,所以先會呼叫NutsTopping中的AddTopping()方法,而該方法先呼叫input.AddTopping()也就是上一層的AddTopping()方法
  • 所以,d會先從基礎實現開始依次實現特定功能

這時一個類似於遞迴的過程,最後輸出如下:

Plain IceCream ready for some toppings!
Candy Topping added!
Peanut Topping added!
Nuts Topping added!

Decorator vs. inheritance

裝飾器:

  • 執行時將特性組合起來
  • 由多個物件協作組成
  • 可以混合多種特性

繼承:

  • 編譯時將特性組合
  • 繼承產生一個單一形式明確的物件
  • 多繼承是很難實現的

java.util.Collections中的裝飾器

Java中的一些可變聚合型別(List, Set, Map),可以利用Collections類中的裝飾器新增某些特性

比如變成不可變型別

List<Trace> ts = new LinkedList<>();
List<Trace> ts2 = (List<Trace>) Collections.unmodifiableCollection(ts);

行為類模式(Behavioral patterns)

策略模式(Strategy)

策略模式要解決這樣的問題:定義一系列演算法, 並將每種演算法分別放入獨立的類中, 以使演算法的物件能夠相互替換

比如,排序有很多演算法(冒泡,歸併,快排)

我們可以為不同的實現演算法構造抽象介面,利用委託,在執行時動態傳入客戶端傾向的演算法類例項

優點

  • 可以輕鬆擴充套件新的演算法實現
  • 將演算法從客戶端程式碼中分離出來

舉例

在電子商務應用中需要實現各種支付方法, 客戶選中希望購買的商品後需要選擇一種支付方式: Paypal 或者信用卡

先設計支付策略介面:

public interface PaymentStrategy {
    public void pay(int amount);
}

實現信用卡支付策略:

public class CreditCardStrategy implements PaymentStrategy {
    private String name;
    private String cardNumber;
    private String cvv;
    private String dateOfExpiry;
    public CreditCardStrategy(String nm, String ccNum,
                              String cvv, String expiryDate){
        this.name=nm;
        this.cardNumber=ccNum;
        this.cvv=cvv;
        this.dateOfExpiry=expiryDate;
    }
    @Override
    public void pay(int amount) {
        System.out.println(amount +" paid with credit card");
    }
}

實現 Paypal 支付策略:

public class PaypalStrategy implements PaymentStrategy {
    private String emailId;
    private String password;
    public PaypalStrategy(String email, String pwd){
        this.emailId=email;
        this.password=pwd;
    }
    @Override
    public void pay(int amount) {
        System.out.println(amount + " paid using Paypal.");
    }
}

然後利用委託呼叫相應演算法:

public class ShoppingCart {
    ...
    public void pay(PaymentStrategy paymentMethod){
        int amount = calculateTotal();
        paymentMethod.pay(amount);
    }
}

客戶端程式碼:

public static void main(String[] args) {
    ShoppingCart cart = new ShoppingCart();
    Item item1 = new Item("1234",10);
    Item item2 = new Item("5678",40);
    cart.addItem(item1);
    cart.addItem(item2);
    //pay by paypal
    cart.pay(new PaypalStrategy("myemail@exp.com", "mypwd"));
    //pay by credit card
    cart.pay(new CreditCardStrategy("Alice", "1234", "786", "12/18"));
}

模板模式(Template Method)

模板方法模式在超類中定義了一個演算法的框架, 允許子類在不修改結構的情況下重寫演算法的特定步驟

就是做事情的步驟一樣,但是具體方法不同:

比如從不同型別的檔案中進行讀操作,對於不同檔案,這個操作的步驟相同,但是具體實現不同

這時,可以把共性的步驟放在抽象類內公共實現,差異化的步驟在各個子類中實現。模板方法定義了一個演算法的步驟,並允許子類為一個或多個步驟提供實現

模板模式透過繼承和重寫來實現,這種模式廣泛用於各種框架(frameworks)中

舉例

我們需要實現兩種汽車生產的過程,顯然步驟都相同,但是具體實現會有差異

先實現一個抽象類,BuildCar中給出了生產車輛的步驟:

public abstract class CarBuilder {
    protected abstract void BuildSkeleton();
    protected abstract void InstallEngine();
    protected abstract void InstallDoor();
    // Template Method that specifies the general logic
    public void BuildCar() { //通用邏輯
        BuildSkeleton();
        InstallEngine();
        InstallDoor();
    }
}

然後再為車進行具體實現:

public class PorcheBuilder extends CarBuilder {
    protected void BuildSkeleton() {
        System.out.println("Building Porche Skeleton");
    }
    protected void InstallEngine() {
        System.out.println("Installing Porche Engine");
    }
    protected void InstallDoor() {
        System.out.println("Installing Porche Door");
    }
}
public class BeetleBuilder extends CarBuilder {
    protected void BuildSkeleton() {
        System.out.println("Building Beetle Skeleton");
    }
    protected void InstallEngine() {
        System.out.println("Installing Beetle Engine");
    }
    protected void InstallDoor() {
        System.out.println("Installing Beetle Door");
    }
}

再比如白盒框架,框架已經將某個功能的步驟再抽象類中寫好了,我們使用該框架時只需要重寫對應的方法即可

迭代器模式(Iterator)

解決這樣的問題:在不暴露集合底層表現形式 (列表、 棧和樹等) 的情況下遍歷集合中所有的元素

結構

該模式的結構:

  • Iterator介面:宣告瞭遍歷集合所需的操作: 獲取下一個元素、 獲取當前位置和重新開始迭代等
  • Concrete Iterators實現類:實現遍歷集合的一種特定演算法。 迭代器物件必須跟蹤自身遍歷的進度。 這使得多個迭代器可以相互獨立地遍歷同一集合
  • Collection介面:宣告一個或多個方法來獲取與集合相容的迭代器。 注意, 返回方法的型別必須被宣告為迭代器介面, 因此具體集合可以返回各種不同種類的迭代器
  • Concrete Collections實現類:在客戶端請求迭代器時返回一個特定的具體迭代器類實體

簡單來說,就是讓自己的集合類實現Iterable介面,並實現自己的獨特Iterator迭代器(hasNext, next, remove),允許客戶端利用這個迭代器進行顯式或隱式的迭代遍歷:

for (E e : collection) { … }
Iterator<E> iter = collection.iterator();
while(iter.hasNext()) { … }

舉例

public class Pair<E> implements Iterable<E> {
    private final E first, second;
    public Pair(E f, E s) { first = f; second = s; }
    public Iterator<E> iterator() {
        return new PairIterator();
    }
    private class PairIterator implements Iterator<E> {
        private boolean seenFirst = false, seenSecond = false;
        public boolean hasNext() { return !seenSecond; }
        public E next() {
            if (!seenFirst) { seenFirst = true; return first; }
            if (!seenSecond) { seenSecond = true; return second; }
            throw new NoSuchElementException();
        }
        public void remove() {
            throw new UnsupportedOperationException();
        }
    }
}

訪問者模式(Visitor)

它能將演算法與其所作用的物件隔離開來

這種模式可以為 ADT 預留一個將來可擴充套件功能的接入點,外部實現的功能程式碼可以在不改變 ADT 本身的情況下在需要時透過委託接入 ADT

舉例

想想這樣一個問題,需要檢視超市貨架的物品

先設計資料操作的抽象介面:

/* Abstract element interface (visitable) */
public interface ItemElement {
    public int accept(ShoppingCartVisitor visitor);
}

然後為每種商品具體實現:

/* Concrete element */
public class Book implements ItemElement{
    private double price;
	...
    int accept(ShoppingCartVisitor visitor) {
        // 處理資料的功能委託給visitor
        visitor.visit(this);
    }
}
public class Fruit implements ItemElement{
    private double weight;
	...
    int accept(ShoppingCartVisitor visitor) {
        visitor.visit(this);
    }
}

設計訪問者介面:

/* Abstract visitor interface */
public interface ShoppingCartVisitor {
    int visit(Book book);
    int visit(Fruit fruit);
}

客戶端實現一種visitor

public class ShoppingCartVisitorImpl implements ShoppingCartVisitor {
    public int visit(Book book) {
        int cost=0;
        if(book.getPrice() > 50){
            cost = book.getPrice()-5;
        }else
            cost = book.getPrice();
        System.out.println("Book ISBN::"+book.getIsbnNumber() + " cost ="+cost);
        return cost;
    }
    public int visit(Fruit fruit) {
        int cost = fruit.getPricePerKg()*fruit.getWeight();
        System.out.println(fruit.getName() + " cost = "+cost);
        return cost;
    }
}

客戶端均透過visitor的實現類來訪問資料,比如設計計算價格和的方法:

public class ShoppingCartClient {
    public static void main(String[] args) {
        ItemElement[] items = new ItemElement[]{
                new Book(20, "1234"),new Book(100, "5678"),
                new Fruit(10, 2, "Banana"), new Fruit(5, 5, "Apple")};
        int total = calculatePrice(items);
        System.out.println("Total Cost = "+total);
    }
    private static int calculatePrice(ItemElement[] items) {
        ShoppingCartVisitor visitor = new ShoppingCartVisitorImpl();
        int sum=0;
        for(ItemElement item : items)
            sum = sum + item.accept(visitor);
        return sum;
    }
}

這樣,如果後續想要改變演算法,只要更換visitor即可

Visitor vs. Iterator

這兩者功能很相似,我們來做個對比:

  • 迭代器:以遍歷的方式訪問集合資料而無需暴露其內部表示,將遍歷這項功能委託給到外部的iterator物件
  • 訪問者:在特定 ADT 上執行某種特定操作,但該操作不在 ADT 內部實現,而是委託給獨立的visitor物件,客戶端可靈活擴充套件/改visitor的操作演算法,而不影響 ADT

Strategy vs. Visitor

相同點:

兩者都是透過委託建立兩個物件的動態聯絡

區別:

  • Visitor 強調的是外部定義某種對 ADT 的操作,該操作與 ADT 自身關係不大(只是訪問ADT),故ADT內部只需要開放accept(visitor)即可,客戶端透過它設定visitor操作並在外部呼叫
  • Strategy則強調是對 ADT 內部某些要實現的功能的相應演算法的靈活替換。這些演算法是 ADT 功能的重要組成部分,只不過是委託到外部strategy類而已

設計模式的共性

共性樣式:繼承

特點就是隻使用繼承,而不使用委託

比如,介面卡模式(Adaptor)

適用場合:已經有了一個類,但是其方法與目前客戶端的需求不一致

根據 OCP 原則,不能修改這個類,所以擴充套件一個adptor和一個統一的介面

模板模式(Template)

適用場合:有共性的演算法流程,但演算法各步驟有不同的實現,典型的“將共性提升至超型別,將個性保留在子型別”

注意:如果某個步驟不需要有多種實現,直接在該抽象類裡寫出共性實現即可(需要時將方法設定為final,不允許override

共性樣式:繼承+委託

特點就是有兩顆繼承樹,有兩個層次的委託

比如,策略模式(Strategy)

根據 OCP 原則,想有多個演算法的實現,在右側樹裡擴充套件子型別即可,在左側子型別裡傳入不同的型別例項

迭代器模式(Iterator)

工廠方法模式(Factory Method)

左右兩棵樹的子型別一一對應,如果工廠方法裡使用Type表徵右側的子型別,那麼左側的子型別只要 1 個即可

訪問者模式(Visitor)

左右兩側的兩棵樹的子型別,基本上是一一對應,但左側樹中的不同子型別可能對應右側樹中的同一個子型別visitor

總結

  • Creational patterns

  • Factory method: 建立物件而不指定確切的類

  • Structural patterns

  • Adapter: 透過將自己的介面包裝在已有的類來使不相容介面的類一起工作

  • Decorator: 動態新增/重寫類中的方法

  • Behavioral patterns

  • Strategy: 允許在執行時選擇演算法

  • Template method: 將演算法的骨架定義為抽象類,讓子類來實現

  • Iterator: 順序訪問物件的元素而不暴露其底層表示

  • Visitor: 將某些方法分離出來,委派給獨立的物件

本文使用 Zhihu On VSCode 創作併發布

相關文章