這樣重構(gòu)代碼就能和產(chǎn)品經(jīng)理成為好朋友——策略模式實(shí)戰(zhàn)

變化是永恒的施禾,產(chǎn)品需求穩(wěn)定不變是不可能的,和產(chǎn)品經(jīng)理互懟是沒有用的屎媳,但有一個(gè)方向是可以努力的:讓代碼更有彈性,以不變應(yīng)萬變论巍。

繼上一次發(fā)版前突然變更單選按鈕樣式之后烛谊,又新增了兩個(gè)和選項(xiàng)按鈕有關(guān)的需求。它們分別是多選和菜單選环壤。多選類似于原生CheckBox,而菜單選是多選和單選的組合钞诡,類似于西餐點(diǎn)菜郑现,西餐菜單將食物分為前菜、主食荧降、湯接箫,每種只能選擇 1 個(gè)(即同組內(nèi)單選,多組間多選)朵诫。

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

自定義單選按鈕

回顧下Selector + SelectorGroup的效果:

selector.gif

其中每一個(gè)選項(xiàng)就是Selector剪返,它們的狀態(tài)被SelectorGroup管理废累。

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

實(shí)現(xiàn)關(guān)鍵邏輯如下:

  1. 單個(gè)按鈕是一個(gè)抽象容器控件邑滨,它可以被點(diǎn)擊并借助View.setSelected()記憶按鈕選中狀態(tài)。按鈕內(nèi)元素布局由其子類填充钱反。
public abstract class Selector extends FrameLayout implements View.OnClickListener {
    //按鈕唯一標(biāo)示符
    private String tag ;
    private SelectorGroup selectorGroup;

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

    private void initView(Context context, AttributeSet attrs) {
        //構(gòu)建視圖(延遲到子類進(jìn)行)
        View view = onCreateView();
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        this.addView(view, params);
        this.setOnClickListener(this);
    }
    
    //構(gòu)建視圖(在子類中自定義視圖)
    protected abstract View onCreateView();
    
    //將按鈕添加到組
    public Selector setGroup(SelectorGroup selectorGroup) {
        this.selectorGroup = selectorGroup;
        selectorGroup.addSelector(this);
        return this;
    }
    
    @Override
    public void setSelected(boolean selected) {
        //設(shè)置按鈕選中狀態(tài)
        boolean isPreSelected = isSelected();
        super.setSelected(selected);
        if (isPreSelected != selected) {
            onSwitchSelected(selected);
        }
    }
    
    //按鈕選中狀態(tài)變更(在子類中自定義變更效果)
    protected abstract void onSwitchSelected(boolean isSelect);
    
    @Override
    public void onClick(View v) {
        //通知選中組掖看,當(dāng)前按鈕被選中
        if (selectorGroup != null) {
            selectorGroup.onSelectorClick(this);
        }
    }
}

Selector通過模版方法模式,將構(gòu)建按鈕視圖和按鈕選中效果延遲到子類構(gòu)建面哥。所以當(dāng)按鈕內(nèi)部元素布局發(fā)生改變時(shí)不需要修改Selector哎壳,只需要新建它的子類。

  1. 單選組持有所有按鈕尚卫,當(dāng)按鈕被點(diǎn)擊時(shí)归榕,選中組遍歷其余按鈕并取消選中狀態(tài),以此來實(shí)現(xiàn)單選效果
public class SelectorGroup {
    //持有所有按鈕
    private Set<Selector> selectors = new HashSet<>();

    public void addSelector(Selector selector) {
        selectors.add(selector);
    }

    public void onSelectorClick(Selector selector) {
        cancelPreSelector(selector);
    }

    //遍歷所有按鈕吱涉,將之前選中的按鈕設(shè)置為未選中
    private void cancelPreSelector(Selector selector) {
        for (Selector s : selectors) {
            if (!s.equals(selector) && s.isSelected()) {
                s.setSelected(false);
            }
        }
    }
}

剝離行為

選中按鈕后的行為被寫死在SelectorGroup.onSelectorClick()中蹲坷,這使得SelectorGroup中的行為無法被替換驶乾。

每次行為擴(kuò)展都重新寫一個(gè)SelectorGroup怎么樣?不行循签!因?yàn)?code>Selector是和SelectorGroup耦合的级乐,這意味著Selector的代碼也要跟著改動(dòng),這不符合開閉原則县匠。

SelectorGroup中除了會(huì)變的“選中行為”之外风科,也有不會(huì)變的成分,比如“持有所有的按鈕”乞旦。是不是可以增加一層抽象將變化的行為封裝起來贼穆,使得SelectorGroup與變化隔離?

接口是封裝行為的最佳選擇兰粉,可以運(yùn)用策略模式將選中行為封裝起來

策略模式的詳細(xì)介紹可以點(diǎn)擊這里故痊。

這樣就可以在外部構(gòu)建具體的選中行為,再將其注入到SelectorGroup中玖姑,以實(shí)現(xiàn)動(dòng)態(tài)修改行為:

public class SelectorGroup {
    private ChoiceAction choiceMode;

    //注入具體選中行為
    public void setChoiceMode(ChoiceAction choiceMode) {
        this.choiceMode = choiceMode;
    }
    
    //當(dāng)按鈕被點(diǎn)擊時(shí)應(yīng)用選中行為
    void onSelectorClick(Selector selector) {
        if (choiceMode != null) {
            choiceMode.onChoose(selectors, selector, onStateChangeListener);
        }
    }
    
    //選中后的行為被抽象成接口
    public interface ChoiceAction {
        void onChoose(Set<Selector> selectors, Selector selector, StateListener stateListener);
    }
}

將具體行為替換成接口后就好像是在原本嚴(yán)嚴(yán)實(shí)實(shí)的SelectorGroup中挖了一個(gè)洞愕秫,只要符合這個(gè)洞形狀的東西都可以塞進(jìn)來。這樣就很靈活了焰络。

如果每次使用SelectorGroup戴甩,都需要重新自定義選中行為也很費(fèi)力,所以在其中添加了最常用的單選和多選行為:

public class SelectorGroup {
    public static final int MODE_SINGLE_CHOICE = 1;
    public static final int MODE_MULTIPLE_CHOICE = 2;
    private ChoiceAction choiceMode;

    //通過這個(gè)方法設(shè)置自定義行為
    public void setChoiceMode(ChoiceAction choiceMode) {
        this.choiceMode = choiceMode;
    }
    
    //通過這個(gè)方法設(shè)置默認(rèn)行為
    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(Set<Selector> selectors, Selector selector, StateListener stateListener) {
            //將自己選中
            selector.setSelected(true);
            //將除了自己外的其他按鈕設(shè)置為未選中
            cancelPreSelector(selector, selectors);
        }
    }
    
    //多選行為
    private class MultipleAction implements ChoiceAction {
        @Override
        public void onChoose(Set<Selector> selectors, Selector selector, StateListener stateListener) {
            //反轉(zhuǎn)自己的選中狀態(tài)
            boolean isSelected = selector.isSelected();
            selector.setSelected(!isSelected);
        }
    }

將原本具體的行為都移到了接口中闪彼,而SelectorGroup只和抽象的接口互動(dòng)甜孤,不和具體行為互動(dòng),這樣的代碼具有彈性畏腕。

現(xiàn)在只要像這樣就可以分別實(shí)現(xiàn)單選和多選:

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);
        ((Selector) findViewById(R.id.selector_10)).setGroup(multipleGroup);
        ((Selector) findViewById(R.id.selector_20)).setGroup(multipleGroup);
        ((Selector) findViewById(R.id.selector_30)).setGroup(multipleGroup);
        //單選
        SelectorGroup singleGroup = new SelectorGroup();
        singleGroup.setStateListener(new SingleChoiceListener());
        ((Selector) findViewById(R.id.single10)).setGroup(singleGroup);
        ((Selector) findViewById(R.id.single20)).setGroup(singleGroup);
        ((Selector) findViewById(R.id.single30)).setGroup(singleGroup);
    }
}

activity_main.xml中布局了6個(gè)Selector缴川,其中三個(gè)用于單選,三個(gè)用于多余描馅。

菜單選

這一次新需求是多選和單選的組合:菜單選二跋。這種模式將選項(xiàng)分成若干組,組內(nèi)單選流昏,組間多選扎即。看下使用策略模式重構(gòu)后的SelectorGroup是如何輕松應(yīng)對(duì)的:

class OrderChoiceMode implements SelectorGroup.ChoiceAction {
        @Override
        public void onChoose(Set<Selector> selectors, Selector selector, SelectorGroup.StateListener stateListener) {
            //同組互斥選中
            String tagPrefix = getTagPrefix(selector.getSelectorTag());
            cancelPreSelectorBySameTag(selectors, tagPrefix, stateListener);
            selector.setSelected(true);
        }

        //在同一組中取消之前的選擇(要求同一組按鈕的tag具有相同的前綴)
        private void cancelPreSelectorBySameTag(Set<Selector> selectors, String tagPrefix, SelectorGroup.StateListener stateListener) {
            for (Selector selector : selectors) {
                String prefix = getTagPrefix(selector.getSelectorTag());
                if (prefix.equals(tagPrefix) && selector.isSelected()) {
                    selector.setSelected(false);
                    if (stateListener != null) {
                        stateListener.onStateChange(selector.getSelectorTag(), false);
                    }
                }
            }
        }

        //獲取標(biāo)簽前綴
        private String getTagPrefix(String tag) {
            //約定tag由兩個(gè)部分組成况凉,中間用下劃線分割:前綴_標(biāo)簽名
            int index = tag.indexOf("_");
            return tag.substring(0, index);
        }
    }

SelectorGroup.ChoiceAction中重新定義按鈕選中時(shí)的行為:同組互斥選中谚鄙,不同組可以多選。這就需要一種標(biāo)識(shí)組的方法刁绒,本文采用了給同組按鈕設(shè)置相同前綴的做法:

<resources>
    <string name="tag_starters_pork">starters_pork</string>
    <string name="tag_starters_duck">starters_duck</string>
    <string name="tag_starters_springRoll">starters_springRoll</string>
    <string name="tag_main_pizza">main_pizza</string>
    <string name="tag_main_pasta">main_pasta</string>
    <string name="tag_soup_mushroom">soup_mushroom</string>
    <string name="tag_soup_scampi">soup_scampi</string>
</resources>

前菜闷营、主食、湯分別采用了starters、main傻盟、soup這樣的前綴速蕊。

然后就可以像這樣動(dòng)態(tài)的為SelectorGroup擴(kuò)展菜單選行為了:

public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        //order-choice
        SelectorGroup orderGroup = new SelectorGroup();
        orderGroup.setChoiceMode(new OrderChoiceMode());
        ((Selector) findViewById(R.id.selector_starters_duck)).setGroup(orderGroup);
        ((Selector) findViewById(R.id.selector_starters_pork)).setGroup(orderGroup);
        ((Selector) findViewById(R.id.selector_starters_springRoll)).setGroup(orderGroup);
        ((Selector) findViewById(R.id.selector_main_pizza)).setGroup(orderGroup);
        ((Selector) findViewById(R.id.selector_main_pasta)).setGroup(orderGroup);
        ((Selector) findViewById(R.id.selector_soup_mushroom)).setGroup(orderGroup);
        ((Selector) findViewById(R.id.selector_soup_scampi)).setGroup(orderGroup);
    }
}

效果如下:


order-choice.gif

其中單選按鈕通過繼承Selector重寫onSwitchSelected(),定義了選中效果為愛心動(dòng)畫娘赴。

總結(jié)

至此规哲,選項(xiàng)按鈕這個(gè)repository已經(jīng)將兩種設(shè)計(jì)模式運(yùn)用于實(shí)戰(zhàn)。

  1. 運(yùn)用了模版方法模式將變化的按鈕布局和點(diǎn)擊效果和按鈕本身隔離诽表。

  2. 運(yùn)用了策略模式將變化的選中行為和選中組隔離唉锌。

在經(jīng)歷多次需求變更的突然襲擊后,遍體鱗傷的我們需要找出自救的方法:

實(shí)現(xiàn)需求前竿奏,通過分析需求識(shí)別出“會(huì)變的”和“不變的”邏輯袄简,增加一層抽象將“會(huì)變的”邏輯封裝起來,以實(shí)現(xiàn)隔離和分層泛啸,將“不變的”邏輯和抽象的互動(dòng)代碼在上層類中固定下來绿语。需求發(fā)生變化時(shí),通過在下層實(shí)現(xiàn)抽象以多態(tài)的方式來應(yīng)對(duì)候址。這樣的代碼具有彈性吕粹,就能以“不變的”上層邏輯應(yīng)對(duì)變化的需求

talk is cheap, show me the code

實(shí)例代碼省略了一些非關(guān)鍵的細(xì)節(jié)宗雇,完整代碼在這里

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末昂芜,一起剝皮案震驚了整個(gè)濱河市莹规,隨后出現(xiàn)的幾起案子赔蒲,更是在濱河造成了極大的恐慌,老刑警劉巖良漱,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件舞虱,死亡現(xiàn)場離奇詭異,居然都是意外死亡母市,警方通過查閱死者的電腦和手機(jī)矾兜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來患久,“玉大人椅寺,你說我怎么就攤上這事〗В” “怎么了返帕?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長篙挽。 經(jīng)常有香客問我荆萤,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任链韭,我火速辦了婚禮偏竟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘敞峭。我一直安慰自己踊谋,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布儡陨。 她就那樣靜靜地躺著褪子,像睡著了一般。 火紅的嫁衣襯著肌膚如雪骗村。 梳的紋絲不亂的頭發(fā)上嫌褪,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音胚股,去河邊找鬼笼痛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛琅拌,可吹牛的內(nèi)容都是我干的缨伊。 我是一名探鬼主播,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼进宝,長吁一口氣:“原來是場噩夢啊……” “哼刻坊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起党晋,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬榮一對(duì)情侶失蹤谭胚,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后未玻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體灾而,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年扳剿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了旁趟。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡庇绽,死狀恐怖锡搜,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情瞧掺,我是刑警寧澤耕餐,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站夸盟,受9級(jí)特大地震影響蛾方,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一桩砰、第九天 我趴在偏房一處隱蔽的房頂上張望拓春。 院中可真熱鬧,春花似錦亚隅、人聲如沸硼莽。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懂鸵。三九已至,卻和暖如春行疏,著一層夾襖步出監(jiān)牢的瞬間匆光,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來泰國打工酿联, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留终息,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓贞让,卻偏偏與公主長得像周崭,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子喳张,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 專業(yè)考題類型管理運(yùn)行工作負(fù)責(zé)人一般作業(yè)考題內(nèi)容選項(xiàng)A選項(xiàng)B選項(xiàng)C選項(xiàng)D選項(xiàng)E選項(xiàng)F正確答案 變電單選GYSZ本規(guī)程...
    小白兔去釣魚閱讀 9,014評(píng)論 0 13
  • 這個(gè)序可能會(huì)有些長 先作個(gè)自我介紹,我是一名交互設(shè)計(jì)師柴墩,90后忙厌。我并不怎么喜歡編輯文章或?qū)扅c(diǎn)什么凫岖,就是因?yàn)閼薪龋?..
    IxDKN閱讀 11,068評(píng)論 16 160
  • 業(yè)務(wù)場景 興高采烈地前去一周一次的需求大會(huì)。為了更加精準(zhǔn)的推送哥放,需要采集用戶信息歼指,于是乎產(chǎn)品設(shè)計(jì)了如下界面: 沒想...
    唐子玄閱讀 5,609評(píng)論 14 44
  • 1、窗體 1甥雕、常用屬性 (1)Name屬性:用來獲取或設(shè)置窗體的名稱踩身,在應(yīng)用程序中可通過Name屬性來引用窗體。 ...
    Moment__格調(diào)閱讀 4,562評(píng)論 0 11
  • 老天不給你困難社露,你又如何看透人心挟阻;老天不給你失敗,你又如何發(fā)現(xiàn)身邊的人是真是假;老天不給你孤獨(dú)附鸽,你又如何反思自释哑础;...
    羅掌柜real閱讀 209評(píng)論 0 0