應用設計模式和產品經理成爲好朋友——策略模式實戰

變化是永恆的,產品需求穩定不變是不可能的,和產品經理互懟是沒有用的,但有一個方向是能夠努力的:讓代碼更有彈性,以不變應萬變。git

繼上一次發版前忽然變動單選按鈕樣式以後,又新增了兩個和選項按鈕有關的需求。它們分別是多選和菜單選。多選相似於原生CheckBox,而菜單選是多選和單選的組合,相似於西餐點菜,西餐菜單將食物分爲前菜、主食、湯,每種只能選擇 1 個(即同組內單選,多組間多選)。github

上一篇中的自定義單選按鈕Selector + SelectorGroup完美 hold 住按鈕樣式的變化,這一次可否從容應對新增需求?spring

自定義單選按鈕

回顧下Selector + SelectorGroup的效果: 設計模式

selector.gif

其中每個選項就是Selector,它們的狀態被SelectorGroup管理。bash

這組自定義控件突破了原生單選按鈕的佈局限制,選項的相對位置能夠用 xml 定義(原生控件只能是垂直或水平鋪開),並且還能夠方便地更換按鈕樣式以及定義選中效果(上圖中選中後有透明度動畫)dom

實現關鍵邏輯以下:ide

  1. 單個按鈕是一個抽象容器控件,它能夠被點擊並藉助View.setSelected()記憶按鈕選中狀態。按鈕內元素佈局由其子類填充。
public abstract class Selector extends FrameLayout implements View.OnClickListener {
    //'按鈕惟一標示符'
    private String tag;
    //'按鈕所在組的標示符,單選的按鈕應該設置相同的groupTag'
    private String groupTag;
    private SelectorGroup selectorGroup;

    public Selector(Context context) {
        super(context);
        initView(context, null);
    }

    private void initView(Context context, AttributeSet attrs) {
        //'構建視圖(延遲到子類進行)'
        View view = onCreateView();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(view, params);
        this.setOnClickListener(this);
    }
    
    //'構建視圖(在子類中自定義視圖)'
    protected abstract View onCreateView();
    
    //'設置按鈕的按鈕組'
    public Selector setGroup(String groupTag, SelectorGroup selectorGroup) {
        this.selectorGroup = selectorGroup;
        this.groupTag = groupTag;
        return this;
    }
    
    @Override
    public void setSelected(boolean selected) {
        //'設置按鈕選中狀態'
        boolean isPreSelected = isSelected();
        super.setSelected(selected);
        if (isPreSelected != selected) {
            onSwitchSelected(selected);
        }
    }
    
    //'按鈕選中狀態變動(在子類中自定義變動效果)'
    protected abstract void onSwitchSelected(boolean isSelect);
    
    @Override
    public void onClick(View v) {
        //'通知選中組,當前按鈕被選中'
        if (selectorGroup != null) {
            selectorGroup.onSelectorClick(this);
        }
    }
}
複製代碼

Selector經過模版方法模式,將構建按鈕視圖和按鈕選中效果延遲到子類構建。因此當按鈕內部元素佈局發生改變時不須要修改Selector,只須要新建它的子類。佈局

  1. 單選組會保存上一次選中的按鈕,以便新的按鈕被選中時取消以前按鈕的選中狀態。
public class SelectorGroup {
    //'持有上次選中的按鈕組'
    private HashMap<String, Selector> selectorMap = new HashMap<>();
    
    //'獲取上次選中按鈕'
    public Selector getPreSelector(String groupTag) {
        return selectorMap.get(groupTag);
    }

    //'取消上次選中按鈕'
    private void cancelPreSelector(Selector selector) {
        String groupTag = selector.getGroupTag();
        Selector preSelector = getPreSelector(groupTag);
        if (preSelector != null) {
            preSelector.setSelected(false);
        }
    }
    
    void onSelectorClick(Selector selector) {
        //'選中當前按鈕'
        selector.setSelected(true);
        //'取消以前按鈕'
        cancelPreSelector(selector);
        //'將此次選中按鈕保存在map中'
        selectorMap.put(selector.getGroupTag(), selector);
    }
}
複製代碼

剝離行爲

選中按鈕後的行爲被寫死在SelectorGroup.onSelectorClick()中,這使得SelectorGroup中的行爲沒法被替換。post

每次行爲擴展都從新寫一個SelectorGroup怎麼樣?不行!由於Selector是和SelectorGroup耦合的,這意味着Selector的代碼也要跟着改動,這不符合開閉原則。動畫

SelectorGroup中除了會變的「選中行爲」以外,也有不會變的成分,好比「持有上次選中按鈕」。是否是能夠增長一層抽象將變化的行爲封裝起來,使得SelectorGroup與變化隔離?

接口是封裝行爲的最佳選擇,能夠運用策略模式將選中行爲封裝起來

策略模式的詳細介紹能夠點擊這裏

這樣就能夠在外部構建具體的選中行爲,再將其注入到SelectorGroup中,以實現動態修改行爲:

public class SelectorGroup {
    private ChoiceAction choiceMode;

    //'注入具體選中行爲'
    public void setChoiceMode(ChoiceAction choiceMode) {
        this.choiceMode = choiceMode;
    }
    
    //'當按鈕被點擊時應用選中行爲'
    void onSelectorClick(Selector selector) {
        if (choiceMode != null) {
            choiceMode.onChoose(selector, this, onStateChangeListener);
        }
    }
    
    //'選中後的行爲被抽象成接口'
    public interface ChoiceAction {
        void onChoose(Selector selector, SelectorGroup selectorGroup, StateListener stateListener);
    }
}
複製代碼

將具體行爲替換成接口後就好像是在本來嚴嚴實實的SelectorGroup中挖了一個洞,只要符合這個洞形狀的東西均可以塞進來,這樣就很靈活了。

若是每次使用SelectorGroup,都須要從新自定義選中行爲也很費力,因此在其中添加了最經常使用的單選和多選行爲:

public class SelectorGroup {
    public static final int MODE_SINGLE_CHOICE = 1;
    public static final int MODE_MULTIPLE_CHOICE = 2;
    private ChoiceAction choiceMode;
    
    //'經過這個方法設置默認行爲'
    public void setChoiceMode(int mode) {
        switch (mode) {
            case MODE_MULTIPLE_CHOICE:
                choiceMode = new MultipleAction();
                break;
            case MODE_SINGLE_CHOICE:
                choiceMode = new SingleAction();
                break;
        }
    }
    
    //'單選行爲'
    private class SingleAction implements ChoiceAction {
        @Override
        public void onChoose(Selector selector, SelectorGroup selectorGroup, StateListener stateListener) {
            selector.setSelected(true);
            cancelPreSelector(selector);
            if (stateListener != null) {
                stateListener.onStateChange(selector.getSelectorTag(), true);
            }
        }
    }
    
    //'多選行爲'
    private class MultipleAction implements ChoiceAction {
        @Override
        public void onChoose(Selector selector, SelectorGroup selectorGroup, StateListener stateListener) {
            boolean isSelected = selector.isSelected();
            selector.setSelected(!isSelected);
            if (stateListener != null) {
                stateListener.onStateChange(selector.getSelectorTag(), !isSelected);
            }
        }
    }
}
複製代碼

將本來具體的行爲都移到了接口中,而SelectorGroup只和抽象的接口互動,不和具體行爲互動,這樣的SelectorGroup具備彈性。

如今只要像這樣就能夠分別實現單選和多選:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //多選
        SelectorGroup multipleGroup = new SelectorGroup();
        multipleGroup.setChoiceMode(SelectorGroup.MODE_MULTIPLE_CHOICE);
        multipleGroup.setStateListener(new MultipleChoiceListener());
        ((Selector) findViewById(R.id.selector_10)).setGroup("multiple", multipleGroup);
        ((Selector) findViewById(R.id.selector_20)).setGroup("multiple", multipleGroup);
        ((Selector) findViewById(R.id.selector_30)).setGroup("multiple", multipleGroup);
        //單選
        SelectorGroup singleGroup = new SelectorGroup();
        singleGroup.setChoiceMode(SelectorGroup.MODE_SINGLE_CHOICE);
        singleGroup.setStateListener(new SingleChoiceListener());
        ((Selector) findViewById(R.id.single10)).setGroup("single", singleGroup);
        ((Selector) findViewById(R.id.single20)).setGroup("single", singleGroup);
        ((Selector) findViewById(R.id.single30)).setGroup("single", singleGroup);
    }
}
複製代碼

activity_main.xml中佈局了6個Selector,其中三個用於單選,三個用於多選。

菜單選

這一次新需求是多選和單選的組合:菜單選。這種模式將選項分紅若干組,組內單選,組間多選。看下使用策略模式重構後的SelectorGroup是如何輕鬆應對的:

private class OderChoiceMode implements SelectorGroup.ChoiceAction {

    @Override
    public void onChoose(Selector selector, SelectorGroup selectorGroup, SelectorGroup.StateListener stateListener) {
        //'取消同組的上次選中按鈕'
        cancelPreSelector(selector, selectorGroup);
        //'選中當前點擊按鈕'
        selector.setSelected(true);
    }

    //'取消同組上次選中按鈕,同組的按鈕具備相同的groupTag'
    private void cancelPreSelector(Selector selector, SelectorGroup selectorGroup) {
        Selector preSelector = selectorGroup.getPreSelector(selector.getGroupTag());
        if (preSelector!=null) {
            preSelector.setSelected(false);
        }
    }
}
複製代碼

而後就能夠像這樣動態的爲SelectorGroup擴展菜單選行爲了:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        //'菜單選'
        SelectorGroup orderGroup = new SelectorGroup();
        orderGroup.setStateListener(new OrderChoiceListener());
        orderGroup.setChoiceMode(new OderChoiceMode());
        //'爲同組按鈕設置相同的groupTag'
        ((Selector) findViewById(R.id.selector_starters_duck)).setGroup("starters", orderGroup);
        ((Selector) findViewById(R.id.selector_starters_pork)).setGroup("starters", orderGroup);
        ((Selector) findViewById(R.id.selector_starters_springRoll)).setGroup("starters", orderGroup);
        ((Selector) findViewById(R.id.selector_main_pizza)).setGroup("main", orderGroup);
        ((Selector) findViewById(R.id.selector_main_pasta)).setGroup("main", orderGroup);
        ((Selector) findViewById(R.id.selector_soup_mushroom)).setGroup("soup", orderGroup);
        ((Selector) findViewById(R.id.selector_soup_scampi)).setGroup("soup", orderGroup);
    }
}
複製代碼

效果以下:

order-choice.gif

其中單選按鈕經過繼承Selector重寫onSwitchSelected(),定義了選中效果爲愛心動畫。

總結

至此,選項按鈕這個repository已經將兩種設計模式運用於實戰。

  1. 運用了模版方法模式將變化的按鈕佈局和點擊效果和按鈕自己隔離。

  2. 運用了策略模式將變化的選中行爲和選中組隔離。

在經歷屢次需求變動的忽然襲擊後,遍體鱗傷的咱們須要找出自救的方法:

實現需求前,經過分析需求識別出「會變的」和「不變的」邏輯,增長一層抽象將「會變的」邏輯封裝起來,以實現隔離和分層,將「不變的」邏輯和抽象的互動代碼在上層類中固定下來。需求發生變化時,經過在下層實現抽象以多態的方式來應對。這樣的代碼具備彈性,就能以「不變的」上層邏輯應對變化的需求

talk is cheap, show me the code

實例代碼省略了一些非關鍵的細節,完整代碼在這裏

推薦閱讀

相關文章
相關標籤/搜索