Head First 設計模式 —— 14. 複合 (Compound) 模式

複合模式

在一個解決方案中結合兩個或多個模式,以解決一般或重複發生的問題。 P500

思考題

public interface Quackable {
    public void quack();
}

public class MallardDuck implements Quackable {
    public void quack() {
        System.out.println("Quack");
    }
}

public class Goose {
    public void honk() {
        System.out.println("Honk");
    }
}

假設我們想要在所有使用鴨子的地方使用鵝,畢竟鵝會叫、會飛、會游,和鴨子差不多。什麼模式可以讓我們輕易地將鴨子和鵝摻雜在一起呢? P503

  • 適配器模式。題目需要輕易地將一種行為轉換為另一種行為,且不要改變原有的類,所以需要使用適配器轉換。

思考題

我們要如何在不變化鴨子類的情況下,計算所有鴨子呱呱叫的總次數呢?有沒有什麼模式可以幫上忙?P505

  • 裝飾器模式。題目要求增加新的行為,且不改變原有類,所以可以使用裝飾器。
  • 代理模式。代理模式會控制訪問,而鵝經適配器後轉換的行為不應該被統計,所以可以通過代理模式進行控制。

思考題

你能夠為鵝寫一個抽象工廠嗎?創建」內鵝外鴨「的對象時,你怎麼處理? P511

  • 新建一個工廠,專門創建被適配器轉換成鴨子的鵝

    public abstract class AbstractGooseDuckFactory {
        public abstract Quackable createGooseDuck();
    }
    
    public class GooseDuckFactory extends AbstractGooseDuckFactory {
        public Quackable createGooseDuck() {
            return new GooseAdapter(new Goose());
        }
    }
    

思考題

我們需要將鴨子視為一個集合,甚至是子集合(subcollection),如果我們下一次命令,就能讓整個集合的鴨子聽命行事,那就太好了。什麼模式可以幫我們? P512

  • 迭代器模式。由於我們需要將鴨子視為一個集合,可以遍歷執行同一操作,所以可以使用迭代器模式方便遍歷。
  • 組合模式。由於鴨子集合可能會含有子集合和鴨子,並也需要支援上述行為,所以可以使用組合模式將鴨子和鴨子集合統一起來。

思考題

你能夠有辦法持續追蹤個別鴨子的實時呱呱叫嗎? P516

  • 觀察者模式。題目意思就是鴨子在呱呱叫時通知觀察人員,所以鴨子是可被觀察的,應該繼承 Observable 類,而觀察人員應該實現 Observer 介面 ,觀察人員在個別鴨子上註冊以便實時接收鴨子的呱呱叫行為。
    • 按照以上設計會修改所有的鴨子類,所以就想到可以再加一個裝飾器繼承 Observable 類,並實現 Quackable 介面,這樣改動量最小,不會改變原有鴨子類,也可以將鴨子和可被觀察解耦。但想像很美好,一去實現就會遇到很多問題:用戶程式碼必須與該裝飾器耦合,需要特判該裝飾器以執行註冊觀察者和通知觀察者的方法;該裝飾器只能最後包裝,如果被其他裝飾器包裝就無法再調用相應方法;不便於將相應的方法擴展到組合模式中的集合上。所以還是需要介面上的修改,改變所有鴨子的行為。
    • 書上設計是讓 Quackable 介面繼承 QuackObservable 介面以便所有能叫的鴨子都能被觀察;修改所有鴨子類,並將 Observable 類組合進鴨子類中,將註冊觀察者和通知觀察者的方法內部委託到 Observable 相應的方法中;同時也要修改相應的裝飾器。

思考題

我們還沒有改變一個 Quackable 的實現,即 QuackCounter 裝飾器。它也必須成為 Observable 。你何不試著寫出它的程式碼呢? P518

public class QuackCounter implements Quackable {
    Quackable duck;
    static int numberOfQuacks;
    
    public QuackCounter(Quackable duck) {
        this.duck = duck;
    }
    
    public void quack() {
        duck.quack();
        ++numberOfQuacks;
    }
    
    public static int getQuacks() {
        return numberOfQuacks;
    }
    
    public void registerObserver(Observer observer) {
        duck.registerObserver(observer);
    }
    
    public void notifyObservers() {
        duck.notifyObservers();
    }
}

思考題

萬一呱呱叫學家想觀察整個群,又該怎麼辦呢?當觀察某個組合時,就等於觀察組合內的每個東西。 P520

public class Flock implements Quackable {
    ArrayList ducks = new ArrayList();
    
    public void add(Quackable duck) {
        ducks.add(duck);
    }
    
    public void quack() {
        Iterator iterator = ducks.iterator();
        while(iterator.hasNext()) {
            Quackable duck = (Quackable) iterator.next();
            duck.quack();
        }
    }
    
    public void registerObserver(Observer observer) {
        Iterator iterator = ducks.iterator();
        while(iterator.hasNext()) {
            Quackable duck = (Quackable) iterator.next();
            duck.registerObserver(observer);
        }
    }
    
    public void notifyObservers() {
        // 鴨群註冊觀察者都委託到孩子上了,所以通知觀察者的事情並不需要鴨群做任何事
    }
}

所思所想

  • 可以通過讓原有介面繼承新介面的方式,再增加介面方法和相應的功能的同時,減少用戶修改程式碼。例如:JDK7 中就讓原有的 Closable 介面繼承 AutoClosable 介面,使得原有的用戶程式碼都不必修改就能在 JDK7中使用帶資源的 try 語句能自動關閉資源的新特性。(第一次看見 AutoClosable 介面時,直接從語義上就認為 AutoClosable 繼承了 Closable ,沒想到正相反)

本文首發於公眾號:滿賦諸機(點擊查看原文) 開源在 GitHub :reading-notes/head-first-design-patterns