美文网首页Android开发Android开发经验谈Android技术知识
这样重构代码就能和产品经理成为好朋友——策略模式实战

这样重构代码就能和产品经理成为好朋友——策略模式实战

作者: 唐子玄 | 来源:发表于2019-05-19 22:45 被阅读5次

    变化是永恒的,产品需求稳定不变是不可能的,和产品经理互怼是没有用的,但有一个方向是可以努力的:让代码更有弹性,以不变应万变。

    继上一次发版前突然变更单选按钮样式之后,又新增了两个和选项按钮有关的需求。它们分别是多选和菜单选。多选类似于原生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 ;
        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(SelectorGroup selectorGroup) {
            this.selectorGroup = selectorGroup;
            selectorGroup.addSelector(this);
            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 Set<Selector> selectors = new HashSet<>();
    
        public void addSelector(Selector selector) {
            selectors.add(selector);
        }
    
        public void onSelectorClick(Selector selector) {
            cancelPreSelector(selector);
        }
    
        //遍历所有按钮,将之前选中的按钮设置为未选中
        private void cancelPreSelector(Selector selector) {
            for (Selector s : selectors) {
                if (!s.equals(selector) && s.isSelected()) {
                    s.setSelected(false);
                }
            }
        }
    }
    

    剥离行为

    选中按钮后的行为被写死在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(selectors, selector, onStateChangeListener);
            }
        }
        
        //选中后的行为被抽象成接口
        public interface ChoiceAction {
            void onChoose(Set<Selector> selectors, Selector selector, 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(ChoiceAction choiceMode) {
            this.choiceMode = 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(Set<Selector> selectors, Selector selector, StateListener stateListener) {
                //将自己选中
                selector.setSelected(true);
                //将除了自己外的其他按钮设置为未选中
                cancelPreSelector(selector, selectors);
            }
        }
        
        //多选行为
        private class MultipleAction implements ChoiceAction {
            @Override
            public void onChoose(Set<Selector> selectors, Selector selector, StateListener stateListener) {
                //反转自己的选中状态
                boolean isSelected = selector.isSelected();
                selector.setSelected(!isSelected);
            }
        }
    

    将原本具体的行为都移到了接口中,而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);
            ((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个Selector,其中三个用于单选,三个用于多余。

    菜单选

    这一次新需求是多选和单选的组合:菜单选。这种模式将选项分成若干组,组内单选,组间多选。看下使用策略模式重构后的SelectorGroup是如何轻松应对的:

    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);
                        }
                    }
                }
            }
    
            //获取标签前缀
            private String getTagPrefix(String tag) {
                //约定tag由两个部分组成,中间用下划线分割:前缀_标签名
                int index = tag.indexOf("_");
                return tag.substring(0, index);
            }
        }
    

    SelectorGroup.ChoiceAction中重新定义按钮选中时的行为:同组互斥选中,不同组可以多选。这就需要一种标识组的方法,本文采用了给同组按钮设置相同前缀的做法:

    <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这样的前缀。

    然后就可以像这样动态的为SelectorGroup扩展菜单选行为了:

    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(),定义了选中效果为爱心动画。

    总结

    至此,选项按钮这个repository已经将两种设计模式运用于实战。

    1. 运用了模版方法模式将变化的按钮布局和点击效果和按钮本身隔离。

    2. 运用了策略模式将变化的选中行为和选中组隔离。

    在经历多次需求变更的突然袭击后,遍体鳞伤的我们需要找出自救的方法:

    实现需求前,通过分析需求识别出“会变的”和“不变的”逻辑,增加一层抽象将“会变的”逻辑封装起来,以实现隔离和分层,将“不变的”逻辑和抽象的互动代码在上层类中固定下来。需求发生变化时,通过在下层实现抽象以多态的方式来应对。这样的代码具有弹性,就能以“不变的”上层逻辑应对变化的需求

    talk is cheap, show me the code

    实例代码省略了一些非关键的细节,完整代码在这里

    相关文章

      网友评论

        本文标题:这样重构代码就能和产品经理成为好朋友——策略模式实战

        本文链接:https://www.haomeiwen.com/subject/eohczqtx.html