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

唐子玄發表於2019-05-20

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

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

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

自定義單選按鈕

回顧下Selector + SelectorGroup的效果:

selector.gif

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

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

實現關鍵邏輯如下:

  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中的行為無法被替換。

每次行為擴充套件都重新寫一個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

例項程式碼省略了一些非關鍵的細節,完整程式碼在這裡

推薦閱讀

相關文章