一句話總結異曲同工的設計模式:工廠模式=?策略模式=?模版方法模式

這是設計模式系列的第一篇,系列文章目錄以下:程序員

  1. 一句話總結異曲同工的設計模式:工廠模式=?策略模式=?模版方法模式算法

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

  3. 使用組合的設計模式 —— 找對象要用的遠程代理模式canvas

  4. 用設計模式去掉不必的狀態變量 —— 狀態模式設計模式

雖然不一樣的設計模式解決的問題各不相同,但從一個更高的抽象層次來看,它們經過相同的手段來實現相同的目的。本文將以更抽象的視角剖析工廠模式、策略模式、模版方法模式,以及這些模式所遵循的設計原則。先用一句話總結它們的共同點:bash

它們增長了一層抽象將變化封裝起來,而後對抽象編程,並利用多態應對變化。框架

應用這些設計模式的項目實戰能夠移步下面的連接:ide

  1. 不再要和產品經理吵架了——Android自定義單選按鈕
  2. 這樣寫代碼就能和產品經理成爲好朋友——策略模式實戰

工廠模式

1. 變化是什麼

對工廠模式來講,變化就是構建對象的方式,舉個例子:post

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的細節耦合在一塊兒。ui

若是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(new Action1());
        robot1.setAction(new Action2());
    }
}
複製代碼

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

Android 中的各類監聽器都採用了策略模式,好比View.setOnClickListener(),但下面這個更偏向於觀察者模式,他們的區別是策略模式的意圖在於動態替換行爲,當第二次調用setOnClickListener()時,以前的行爲被替換,而觀察者模式是動態添加觀察者:

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》是一本讓我對設計模式理解升級的書,強烈推薦(本篇中工廠模式的例子摘自其中)。

相關文章
相關標籤/搜索