那些殊途同歸的設計模式,讓我用一句話總結它們

唐子玄發表於2019-05-12

雖然不同的設計模式解決的問題各不相同,但從一個更高的抽象層次來看,它們通過相同的手段來實現相同的目的,用一句話總結:

它們增加了一層抽象變化封裝起來,然後對抽象程式設計,並利用多型應對變化

本文將以更抽象的視角剖析工廠模式、策略模式、模版方法模式,以及這些模式所遵循的設計原則。

工廠模式

1. 變化是什麼

對工廠模式來說,變化就是構建物件的方式,舉個例子:

public class PizzaStore{
    public Pizza orderPizza(String type){
        Pizza pizza ;
    
        //構建具體pizza物件
        if(type.equals("cheese")){
            pizza = new CheesePizza();
        }else if(type.equals("bacon")){
            pizza = new BaconPizza();
        }
    
        //使用pizza物件
        pizza.prepare();
        pizza.bake();
        return pizza ;
    }
}
複製程式碼

抽象的反義詞是具體,對於工廠模式,具體就是用new來構建物件。這樣做的後果是PizzaStore不僅需要引入具體的Pizza類,而且和構建Pizza的細節耦合在一起。

如果PizzaStore一輩子只做這兩種Pizza,上面的程式碼就很好,不需要重構。但如果需要新增Pizza型別,就不得不修改orderPizza(),向其中增加if-else

2. 如何應對變化

通過將變化封裝在一層新的抽象中,實現了上層程式碼和變化的隔離,達到解耦的目的。對於工廠模式來說,新建的抽象叫做工廠,它將物件的構建和物件的使用分隔開,讓使用物件的程式碼不依賴於構建物件的細節。

解耦的好處是:“當變化發生時上層程式碼不需要改動”,這句話也可以表達成:“在不修改既有程式碼的情況下擴充套件功能”。這就是著名的 “開閉原則”

  • “對修改關閉”的意思是:當需要為類擴充套件功能時,不要想著去修改類的既有程式碼,這是不允許的! 為啥不允許?因為既有程式碼是由數位程式設計師的努力,歷經了多個版本的迭代,好不容易才得到的正確程式碼。其中蘊含著博大精深的知識,和你不曾瞭解的細節,修改它一定會出bug的!
  • “對擴充套件開放”的意思是:類的程式碼應該具備良好的抽象,使得擴充套件類的時候,不需要修改類的既有程式碼。

現實的問題來了,如果專案中的既有類不具備擴充套件性,甚至是牽一髮動全身的那種類。在一個時間較緊的迭代中需要往裡新增新功能,你會怎麼做?是違背“對修改關閉”,還是咬牙重構?(歡迎討論~~)

3. 三種封裝變化方式

  1. 簡單工廠模式

既然目的是消除orderPizza()中構建具體pizza的細節,那最直接的做法是,將他們提取出來放到另一個類中:

public class PizzaFactory{
    public static Pizza createPizza(String type){
        Pizza pizza ;
        if(type.equals("cheese")){
            pizza = new CheesePizza();
        }else if(type.equals("bacon")){
            pizza = new BaconPizza();
        }
        return pizza ;
    }
}
複製程式碼

然後使用Pizza的程式碼就變成:

public class PizzaStore{
    public Pizza orderPizza(String type){
        Pizza pizza = PizzaFactory.createPizza(type);
        pizza.prepare();
        pizza.bake();
        return pizza ;
    }
}
複製程式碼

等等,這和我們平時將一段常用程式碼抽離出來放到Util類中有什麼區別嗎?

是的,沒有任何區別。從嚴格意義上說,這不是一個設計模式,更像是一種程式設計習慣。雖然只是程式碼搬家,但這種習慣的好處是:它隱藏了構建物件的細節,因為構建物件是經常會發生變化的,所以它還封裝了變化,最後它還可以被複用,比如選單類也需要構建 pizza 物件並獲取他們的價格。

使用靜態方法是這類封裝常用的技巧,它的好處是不需要新建工廠物件就可以實現呼叫,但缺點是不具備擴充套件性(靜態方法不能被重寫)。

  1. 工廠方法模式

簡單工廠模式中,工廠能夠構建幾種物件是在編譯之前就定義好的,如果想要新增另一種新物件,必須修改既有的工廠類。這不符合開閉原則。 所以簡單工廠模式對於新增物件型別這個場景來說顯得不夠有彈性。

有沒有辦法不修改既有類就新增物件型別?

工廠方法模式就可以做到,因為它採用了繼承:

//抽象pizza店
public abstract class PizzaStore{
    public Pizza orderPizza(String type){
        Pizza pizza = createPizza(type);
        pizza.prepare();
        pizza.bake();
        return pizza ;
    }
    //不同地區的pizza店可以推出地方特色的pizza
    protected abstract Pizza createPizza(String type) ;
}

//A商店提供芝士和培根兩種pizza
public class PizzaStoreA extends PizzaStore{
    @Override
    protected Pizza createPizza(String type){
        Pizza pizza ;
        if(type.equals("cheese")){
            pizza = new CheesePizza();
        }else if(type.equals("bacon")){
            pizza = new BaconPizza();
        }
        return pizza ;
    }
}
複製程式碼

簡單工廠模式將構建物件的細節封裝在一個靜態方法中(靜態方法無法被繼承),而工廠方法模式將其封裝在一個抽象方法中,這樣子類可以通過重寫抽象方法新增 pizza。

現在是介紹另一個設計原則的絕佳時機,它就是 “依賴倒置原則” :上層元件不能依賴下層元件,並且它們都不能依賴具體,而應該依賴抽象。

上面的例子中PizzaStore是上層元件,CheesePizza是下層元件,如果直接在PizzaStore中構建CheesePizza就違反了依賴倒置原則,經過工廠模式的重構,PizzaStore依賴於Pizza這個抽象,同時CheesePizza也依賴於這個抽象。所以違反依賴倒置會讓程式碼缺乏彈性,不易擴充套件。

Android 中RecyclerView.Adapter就運用了工廠方法模式:

public abstract static class Adapter<VH extends ViewHolder> {
    //封裝了各式各樣ViewHolder的構建細節,延遲實現構建細節到子類中
    public abstract VH onCreateViewHolder(ViewGroup parent, int viewType);
}
複製程式碼
  1. 抽象工廠模式

如果需要構建一組物件怎麼辦?

抽象工廠模式用來處理這種情況,它將構建一組物件的細節封裝在一個介面中:

//抽象原料工廠(原材料構建者)
public interface IngredientFactory{
    void Flour createFlour() ;
    void Sause createSause();
}

//原材料使用者
public class Pizza{
    private Flour flour;
    private Sause sause;
    //使用組合持有構建者
    private IngredientFactory factory;
    
    //注入一個具體構建者
    //同一種pizza,在不同地區可能會有不同口味
    //那是因為雖然用的是同類原材料(抽象),當產地不同味道就不同(具體)
    public Pizza(IngredientFactory factory){
        this.factory = factory;
    }
    
    //使用具體工廠構建原材料(發生多型的地方)
    public void prepare(){
        flour = factory.createFlour();
        sause = factory.createSause();
    }
    
    public void bake(){}
    public void cut(){}
}

//具體工廠
public class FactoryA implements IngredientFactory{
    public Flour createFlour(){
        return new FlourA();
    }
    
    public Sause createSause(){
        return new SauseA();
    }
}

//構建pizza的時候傳入具體工廠
public class PizzaStoreA extends PizzaStore{
    @Override
    protected Pizza createPizza(String type){
        Pizza pizza ;
        FactoryA factory = new FactoryA();
        if(type.equals("cheese")){
            pizza = new CheesePizza(factory);
        }else if(type.equals("bacon")){
            pizza = new BaconPizza(factory);
        }
        return pizza ;
    }
}
複製程式碼

如果地區B開了一家新pizza店,只需要新建FactoryB並在其中定義地區B原材料的構建方式,然後傳入Pizza類,整個過程不需要修改Pizza基類。

抽象工廠模式 和 工廠方法模式 的區別在於:

  1. 前者適用於構建多個物件,並且使用組合。
  2. 後者適用於構建單個物件,並且使用繼承。

策略模式

1. 變化是什麼

對策略模式來說,變化就是一組行為,舉個例子:

public class Robot{
    public void onStart(){
        goWorkAt9Am();
    }
    public void onStop(){
        goHomeAt9Pm();
    }
}
複製程式碼

機器人每天早上9點工作。晚上9點回家。公司推出了兩款新產品,一款早上8點開始工作,9點回家。另一款早上9點工作,10點回家。

面對這樣的行為變化,繼承是可以解決問題的,不過你需要新建兩個Robot的子類,過載一個子類的onStart(),過載另一個子類的onStop()。如果每次行為變更都通過繼承來解決,那子類的數量就會越來越多(膨脹的子類)。更重要的是,新增增子類是在編譯時新增行為, 有沒有辦法可以在執行時動態的修改行為?

2. 如何應對變化

通過將變化的行為封裝在介面中,就可以實現動態修改:

//抽象行為
public interface Action{
    void doOnStart();
    void doOnStop();
}

public class Robot{
    //使用組合持有抽象行為
    private Action action;
    //動態改變行為
    public void setAction(Action action){
        this.action = action;
    }
    
    public void onStart(){
        if(action!=null){
            action.doOnStart();
        }
    }
    public void onStop(){
        if(action!=null){
            action.doOnStop();
        }
    }
}

//具體行為1
public class Action1 implements Action{
    public void doOnStart(){
        goWorkAt8Am();
    }
    public void doOnStop(){
        goHomeAt9Pm();
    }
}

//具體行為2
public class Action2 implements Action{
    public void doOnStart(){
        goWorkAt9Am();
    }
    public void doOnStop(){
        goHomeAt10Pm();
    }
}

//將具體行為注入行為使用者(執行時動態改變)
public class Company{
    public static void main(String[] args){
        Robot robot1 = new Robot();
        robot1.setAction(robot1);
        Robot robot2 = new Robot();
        robot2.setAction(robot2);
    }
}
複製程式碼

策略模式將具體的行為和行為的使用者隔離,這樣的好處是,當行為發生變化時,行為的使用者不需要變動。

Android 中的各種監聽器都採用了策略模式,比如:

public class RecyclerView{
    //使用組合持有抽象滾動行為
    private List<OnScrollListener> mScrollListeners;
    
    //抽象滾動行為
    public abstract static class OnScrollListener {
        public void onScrollStateChanged(RecyclerView recyclerView, int newState){}
        public void onScrolled(RecyclerView recyclerView, int dx, int dy){}
    }
    
    //動態修改滾動行為
    public void addOnScrollListener(OnScrollListener listener) {
        if (mScrollListeners == null) {
            mScrollListeners = new ArrayList<>();
        }
        mScrollListeners.add(listener);
    }
    
    //使用滾動行為
    void dispatchOnScrollStateChanged(int state) {
        if (mLayout != null) {
            mLayout.onScrollStateChanged(state);
        }
        onScrollStateChanged(state);

        if (mScrollListener != null) {
            mScrollListener.onScrollStateChanged(this, state);
        }
        if (mScrollListeners != null) {
            for (int i = mScrollListeners.size() - 1; i >= 0; i--) {
                mScrollListeners.get(i).onScrollStateChanged(this, state);
            }
        }
    }
}
複製程式碼

列表滾動後的行為各不相同,所以使用抽象類將其封裝起來(其實和介面是一樣的)。

模版方法模式

1. 變化是什麼

從嚴格意義上講,模版方法模式並不能套用開篇的那句話:“它們增加了一層抽象變化封裝起來,然後對抽象程式設計,並利用多型應對變化”。因為如果這樣說,就是在強調目的是 “應對變化” 。但模版方法的目的更像是 “複用演算法”,雖然它也有應對變化的成分。

對模版方法模式來說,變化就是演算法的某個步驟,舉個例子:

public class View{
    public void draw(Canvas canvas) {
        ...
        // skip step 2 & 5 if possible (common case)
        if (!verticalEdges && !horizontalEdges) {
            // Step 3, draw the content
            if (!dirtyOpaque) onDraw(canvas);

            // Step 4, draw the children
            dispatchDraw(canvas);

            drawAutofilledHighlight(canvas);

            // Overlay is part of the content and draws beneath Foreground
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            // Step 6, draw decorations (foreground, scrollbars)
            onDrawForeground(canvas);

            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);

            if (debugDraw()) {
                debugDrawFocus(canvas);
            }

            // we’re done...
            return;
        }
        ...
    }
    
    protected void dispatchDraw(Canvas canvas) {

    }
}
複製程式碼

節選了View.draw()方法中的某一片段,從註釋中可以看出draw()定義了一個繪圖的演算法框架,一共有七個步驟,所有步驟都被抽象成一個方法,其中的變化在於,每個步驟對於不同型別的View都可能是不同的。所以為了讓不同View複用這套演算法框架,就把它定義在了父類中,子類可以通過重寫某一個步驟來定義不同的行為。

模版方法模式一種常用的重構方法,它將子類的共用邏輯抽象到父類中,並將子類特有子邏輯設計成抽象方法供子類重寫。

2. 對比

  • 模版方法模式 vs 工廠方法模式

從程式碼層面看,模版方法的實現方式和工廠方法模式幾乎一樣,都是通過子類重寫父類的方法。唯一的不同是,工廠方法模式父類中的方法必須是抽象的,也就是說強制子類實現,因為子類不實現父類就無法工作。而模版方法模式父類中的方法可以是不抽象的,也就是說子類可以不實現,父類照樣能工作。這種在父類中空實現的方法有一個專門的名字叫 “hook(鉤子)” ,鉤子的存在,可以讓子類有能力對演算法的流程進行控制,比如ViewGroup.onInterceptTouchEvent()

  • 模版方法模式 vs 策略模式

從產出來看,模版方法模式和策略模式是一個陣營的,因為他們產出的都是一組行為(演算法),而工廠模式產出的是一個物件。但它們倆對演算法的控制力不同,策略模式可以輕鬆的替換掉整個演算法,而模版方法模式只能替換掉演算法中的某個步驟。從程式碼層面來看,它們的實現方式也不同,策略模式使用組合,而模版方法模式使用繼承。組合比繼承更具有彈性,因為它可以在執行時動態的替換行為。

後續

這個系列會持續分享對其他設計模式的理解。

《Head First》是一本讓我對設計模式理解升級的書,強烈推薦(本篇中工廠模式的例子摘自其中)。

相關文章