設計之禪——組合模式

引言

昨天我寫了一篇迭代器模式的文章,其中用到餐廳菜單的例子,如果你細想過,肯定是能發現一些問題的,比如昨天的菜單中只有一級菜單(不清楚的同學可以先看看我上一篇文章,但這只是一個引子,並不影響後面的閱讀),那當某些餐廳需要往自己的菜單中添加子菜單列表(比如甜品),之前實現的迭代器就無法正確工作了,因此我們需要新的模式來解決這個問題,也就是今天的主角——組合模式。

定義

回到問題的本質,爲什麼添加甜品後迭代器就無法工作了?因爲昨天是針對菜品實現的迭代器,而甜品是一個子菜單,並不支持菜品的某些操作(獲取價格),也就是它們的操作不一致導致迭代器需要作出更多複雜的判斷才能完成昨天同樣的功能。那爲什麼組合模式就可以解決這個問題呢?先來看看它的定義:

組合模式允許你將對象組合成樹形結構來表現“整體/部分”層次結構。組合能讓客戶以一致的方式處理個別對象及對象組合。

定義很簡單,抓住關鍵詞“樹形結構”、“一致的方式”,沒錯,組合模式的關鍵就是通過將所有對象組合爲一個樹形結構,再通過某種技巧就能讓我們以統一的方式來對待這些對象。那某些技巧是什麼呢?你覺得面向接口編程如何?
在這裏插入圖片描述
通過圖來看,甜品是新加入的,它對於整個菜單而言是一個子菜單,同時也是一個菜單項,雖然它不支持菜品的一些操作,同時菜品也不支持菜單的特有操作(如顯示所有的菜品),但是我們可以將其抽象出一個公共的接口,也就能爲他們添加默認的行爲(稍後會看到如何實現),如果子類與其父類默認行爲不符合時,將其覆蓋即可。又因爲在子菜單中還包含了許多的菜品,因此整個結構就像一棵樹一樣,這樣我們就可以採用遞歸的方式對整顆樹進行迭代顯示所有的菜品(迭代肯定需要操作統一個類型,否則就需要類型判斷等複雜的操作,回到了問題的原點)。
好吧,但你講的還是太複雜了,看不明白。
沒關係,現在讓我們忘掉過時的例子,以一個非常常見的例子來做代碼演示。

Coding

還有什麼比電腦的文件夾更能直觀的說明這個模式的呢?下面我實現一個簡單的listFiles功能。首先我們需要一個公共的抽象類(可以是接口也可以是抽象類):

public abstract class File {

    protected String name;
    protected double size;

    public File(String name) {
        this.name = name;
    }

    /**
     * 往文件夾添加文件,返回false表示不支持添加或添加失敗
     */
    public boolean addFile(File file) {
        return false;
    }

    public String getName() {
        return name;
    }

    public double getSize() {
        return size;
    }

    public boolean isFolder() {
        return false;
    }

    /**
     * 修改文件內容,返回false表示不支持修改
     */
    public boolean edit() {
        return false;
    }

    /**
     * 顯示文件夾下所有的文件,返回false表示不是文件夾不支持該操作
     */
    public boolean print() {
        return false;
    }
}

文件夾本質上也是一個文件,所以我這裏抽象出一個File抽象類,提供了添加文件、修改文件、是否是文件夾以及顯示文件夾下所有的文件等操作,當然文件和文件夾不可能都支持所有的操作,因此你可以將方法都默認拋出一個UnsupportedOperationException異常,不過我這裏是返回一個默認的boolean值(想一想爲什麼?)。接下來實現具體的文件類:

public class ImageFile extends File {

    public ImageFile(String name, double size) {
        super(name);
        this.size = size;
    }

}

public class TextFile extends File {

    public TextFile(String name, double size) {
        super(name);
        this.size = size;
    }

    @Override
    public boolean edit() {
        System.out.println("修改成功!");
        return true;
    }
}

這裏我實現了一個圖片文件和一個文本文件類,它們不支持文件夾纔有的print方法,所以使用默認的返回一個false;而文本是可以直接編輯的,因此需要覆蓋來支持該項操作。

public class Folder extends File {

    private ArrayList<File> files;

    public Folder(String name) {
        super(name);
        files = new ArrayList<>();
    }

    /**
     * 添加文件到文件夾並計算總大小
     */
    @Override
    public boolean addFile(File file) {
        this.files.add(file);
        this.size += file.getSize();
        return true;
    }

    @Override
    public boolean isFolder() {
        return true;
    }

    @Override
    public boolean print() {
        Iterator<File> iterator = files.iterator();
        while (iterator.hasNext()) {
            File file = iterator.next();
            // 當前文件是文件夾,則遞歸顯示內部子文件
            if (file.isFolder()) {
                System.out.println("Folder name: " + file.getName() + ", total size: " + file.getSize());
                file.print();
            } else {
                System.out.println("File name: " + file.getName() + ", file size: " + file.getSize());
            }
        }
        return true;
    }
}

文件夾系統則只需要實現isFolder和print方法,前者告訴調用者這是一個文件夾,後者則顯示出其下所有的文件,這裏需要注意的是如果你在處理不支持的操作時是拋出的異常,那這裏就需要捕獲異常,這樣代碼不僅不優雅,還會影響性能(異常實例的構造是相當昂貴的,若非必要,儘量不要使用,感興趣的可以參考《深入理解JVM》一書)。最後測試一下:

    public static void main(String[] args) {
    	// 構造測試數據
        File root = new Folder("root");

        File imageFile = new ImageFile("a", 10);
        File imageFile1 = new ImageFile("b", 4);
        File imageFile2 = new ImageFile("c", 12);
        Folder folder = new Folder("圖片");
        folder.addFile(imageFile);
        folder.addFile(imageFile1);
        folder.addFile(imageFile2);

        File textFile = new TextFile("1", 2);
        File textFile1 = new TextFile("2", 5);
        File textFile2 = new TextFile("3", 6);
        Folder folder1 = new Folder("文本");
        folder1.addFile(textFile);
        folder1.addFile(textFile1);
        folder1.addFile(textFile2);

        File file = new ImageFile("d", 53);
        File file1 = new TextFile("4", 34);
        root.addFile(file);
        root.addFile(file1);
        root.addFile(folder);
        root.addFile(folder1);
		
		// 顯示所有文件
        root.print();
    }

客戶端只需要調用print方法就能顯示出所有的文件夾和文件,無需知道具體的實現細節,所以對客戶而言組合(文件夾)和葉節點(文件)是透明的(也就是組合和葉節點能夠統一處理),這樣看組合模式是不是很強大?
但是組合模式也是有缺點的,它違反了單一職責原則,它需要維護整個組合的層次結構,同時還要執行相關的操作,這就讓Component有了多個改變的理由,不過以此換取對客戶的透明性還是很有必要的。

總結

通過組合模式我們學到了如何對客戶保證透明性,使得客戶能夠非常便捷的使用我們提供的方法,也使代碼變得更加整潔優美。
同時我們也應該明白不能固執的遵守設計原則,有時打破比遵守能呈現出更好的設計。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章