使用組合的設計模式 | 美顏相機中的裝飾者模式

唐子玄發表於2019-05-27

這是設計模式系列的第二篇,系列文章目錄如下:

  1. 一句話總結殊途同歸的設計模式:工廠模式=?策略模式=?模版方法模式

  2. 使用組合的設計模式 —— 美顏相機中的裝飾者模式

  3. 使用組合的設計模式 —— 找物件要用的遠端代理模式

  4. 用設計模式去掉沒必要的狀態變數 —— 狀態模式

幾乎所有的設計模式都是通過增加一層抽象來解決問題。

上一篇中提到的三個設計模式通過相同的手段來達到相同的目的:它們通過介面和抽象方法來新增抽象層以應對變化。

這一系列的後續幾篇中會提到的四個設計模式通過相同的手段來達到不同的目的:它們通過新增一個類並持有原有類的方式實現對其擴充套件或限制。

這一篇先來看看裝飾者模式。

裝飾者模式就好像美顏相機,通過新增不同的裝飾品,它可以讓你變成另一個你。(雖然可能面目全非,但本質上還是你)

只複用型別

假設有四種飾品:耳環、鑽石、黃金、羽毛。不同裝飾品有不同價格,通常我們會這樣做抽象:

//抽象飾品
public abstract class Accessory {
    public abstract String name();//飾品名稱
    public abstract int cost();//飾品價格
}

//耳環
public class Ring extends Accessory {
    @Override
    public String name() { return "Ring"; }
    @Override
    public int cost() { return 20; }
}

//鑽石
public class Diamond extends Accessory {
    @Override
    public String name() { return "Diamond"; }
    @Override
    public int cost() { return 1000; }
}

//黃金
public class Gold extends Accessory {
    @Override
    public String name() { return "Gold"; }
    @Override
    public int cost() { return 300; }
}

//羽毛
public class Feather extends Accessory {
    @Override
    public String name() { return "Feather"; }
    @Override
    public int cost() { return 90; }
}
複製程式碼

現推出兩款新飾品:黃金耳環,羽毛黃金耳環。同樣的思路,使用繼承可以解決問題:

public class GoldRing extends Accessory {
    @Override
    public String name() { return "GoldRing"; }
    @Override
    public int cost() { return 320; }
}

public class FeatherGoldRing extends Accessory {
    @Override
    public String name() {  "FeatherGoldRing"; }
    @Override
    public int cost() { return 1110; }
}
複製程式碼
  • 如果繼續推出更多的新品,比如羽毛耳環,鑽石耳環,羽毛鑽石耳環。。。每個新產品都用一個新的類表示,這樣就會遇到子類膨脹的問題。
  • 除此之外,繼承還有一個更致命的缺點:對單個型別的飾品沒有統一的控制力。如果黃金漲價了,我們需要分別修改GoldRingFeatherGoldRing的價格,如果和黃金相關的飾品有好幾十個,那簡直是一場噩夢。
  • 在計算GoldRing價格的時候,我們並沒有複用現有程式碼,即沒有複用GoldRing已經定義的cost()行為,而只是通過繼承複用了型別(GoldRing是一個Accessory)。只複用型別而沒有複用行為的後果是:當Gold漲價時,GoldRing無感知。

有沒有一種比繼承更好的方案在現有飾品基礎上擴充套件新的飾品?

既複用型別又複用行為

採用組合的方式就可以實現既複用型別又複用行為:

public class Gold extends Accessory {
    private Accessory accessory;
    public Gold(Accessory accessory) { this.accessory = accessory; }
    
    @Override
    public String name() {
        return "Gold " + accessory.name();
    }
    @Override
    public int cost() {
        return 300 + accessory.cost();
    }
}

public class Feather extends Accessory {
    private Accessory accessory;
    public Feather(Accessory accessory) { this.accessory = accessory; }

    @Override
    public String name() {
        return "Feather " + accessory.name();
    }
    @Override
    public int cost() {
        return 90 + accessory.cost();
    }
}
複製程式碼
  • 上述四種飾品其實分為兩類,耳環屬於基本飾品,而羽毛、黃金、鑽石屬於附加飾品,附加飾品可以裝飾基本飾品。
  • 附加飾品和基礎飾品擁有相同的超型別Accessory,但附加飾品還通過組合的方式持有一個超型別例項,這樣就可以通過注入超型別的方式將其和任意基礎飾品組合到一起形成新的飾品。

用組合的方式實現羽毛黃金耳環:

Accessory ring = new Gold(new Feather(new Ring()));
複製程式碼
  • 為了說明裝飾與被裝飾的關係,使用了帶有俄羅斯套娃既視感的程式碼(雖然這樣的程式碼可讀性較差)。
  • Ring作為基礎飾品被Feather裝飾成羽毛耳環,羽毛耳環接著被Gold裝飾成換羽毛黃金耳環。
  • 過程中並沒有為羽毛黃金耳環新增一個叫FeatherGoldRing的子類,而是複用了現有的FeatherGold的行為。這樣就解決了子類氾濫和控制力的問題。如果黃金漲價,只需要修改Gold.cost(),所有被Gold裝飾的飾品價格都會隨之而漲。
  • 這個方案還有一個更有用的好處:在執行時動態新增型別。通過繼承新增的型別都是在編譯時定死的,而通過組合的方式只要新增一行俄羅斯套娃式的程式碼,程式執行起來後就新增了一個型別,比如要新增“雙倍黃金羽毛耳環”這個型別,只需要如下的程式碼:
Accessory ring = new Gold(new Gold(new Feather(new Ring())));
複製程式碼

抽象的裝飾者?

新的需求來了:基礎飾品鑲嵌附加飾品收取 10% 的一次性加工費。我們可以為所有附加飾品增加一層抽象:

public abstract class Decorator extends Accessory{
    private Accessory accessory;
    public Decorator(Accessory accessory) { this.accessory = accessory; }

    @Override
    public int cost() {
        return  1.1 * accessory.cost();
    }
}
複製程式碼
  • Decorator通過組合持有超型別Accessory且規定了在構造時必須注入超型別,它還定義了鑲嵌加工費的收費標準。
  • 現在就可以像這樣重新定義附加飾品:
public class Gold extends Decorator {
    public Gold(Accessory accessory){ super(accessory); }

    @Override
    public String name() {
        return "Gold " + accessory.name();
    }
    @Override
    public int cost() {
        return 300 + super.cost();
    }
}
複製程式碼
  • 其實對於裝飾者模式來說,為裝飾者定義一個抽象的父類不是必須的,只要滿足繼承超型別,以及持有超型別引用這兩點就是裝飾者模式。除非需要統一操作所有裝飾者,比如在美顏相機這個場景中,需要通過遍歷找出所有附加飾品。

題外話

使用裝飾者模式後,GoldFeather中有一些樣板程式碼,如果使用Kotlin可以將程式碼簡化如下:

class Feather(val accessory: Accessory) : Accessory by accessory {
    override fun name(): String = "Feather" + accessory.name()
    override fun cost(): Int = 90 + accessory.cost()
}

class Gold(val accessory: Accessory) : Accessory by accessory {
    override fun name(): String = "Gold" + accessory.name()
    override fun cost(): Int = 300 + accessory.cost()
}
複製程式碼

通過by關鍵詞把類委託給一個具有超型別的成員變數accessory,如果不重寫,類中的name()cost()的預設實現都將轉發給accessory

總結

  • 裝飾者模式是一種複用原有類並對其進行擴充套件的方式,它是繼承的替代方法。
  • 裝飾者模式通過繼承原有型別實現複用型別。這一點很重要,因為所有使用原有型別的地方不需要修改程式碼就可以替換成裝飾者。
  • 裝飾者模式通過組合持有原有類例項實現複用行為。
  • 裝飾者模式通過在呼叫原有類方法的前後插入新的邏輯實現功能擴充套件。
  • 裝飾者模式符合開閉原則,即在新增功能的時候沒有修改原有程式碼。
  • 裝飾者模式特別適用於子型別之間可以有隨機組合的場景,比如美顏相機的各種道具組合之後形成新的道具。

運用組合的設計模式不止裝飾者一個,該系列的後續文章會繼續分析“組合”在設計模式中的運用。

推薦閱讀

相關文章