资讯专栏INFORMATION COLUMN

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

李增田 / 2145人阅读

摘要:这种模式将选项分成若干组,组内单选,组间多选。总结至此,选项按钮这个已经将两种设计模式运用于实战。运用了策略模式将变化的选中行为和选中组隔离。这样的代码具有弹性,就能以不变的上层逻辑应对变化的需求。

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

继上一次发版前突然变更单选按钮样式之后,又新增了两个和选项按钮有关的需求。它们分别是多选和菜单选。多选类似于原生CheckBox,而菜单选是多选和单选的组合,类似于西餐点菜,西餐菜单将食物分为前菜、主食、汤,每种只能选择 1 个(即同组内单选,多组间多选)。

上一篇中的自定义单选按钮Selector + SelectorGroup完美 hold 住按钮样式的变化,这一次能否从容应对新增需求?

自定义单选按钮

回顾下Selector + SelectorGroup的效果:

其中每一个选项就是Selector,它们的状态被SelectorGroup管理。

这组自定义控件突破了原生单选按钮的布局限制,选项的相对位置可以用 xml 定义(原生控件只能是垂直或水平铺开),而且还可以方便地更换按钮样式以及定义选中效果(上图中选中后有透明度动画)

实现关键逻辑如下:

    单个按钮是一个抽象容器控件,它可以被点击并借助View.setSelected()记忆按钮选中状态。按钮内元素布局由其子类填充。

</>复制代码

  1. public abstract class Selector extends FrameLayout implements View.OnClickListener {

  2. //按钮唯一标示符
  3. private String tag ;
  4. private SelectorGroup selectorGroup;
  5. public Selector(Context context) {
  6. super(context);
  7. initView(context, null);
  8. }
  9. private void initView(Context context, AttributeSet attrs) {
  10. //构建视图(延迟到子类进行)
  11. View view = onCreateView();
  12. LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
  13. this.addView(view, params);
  14. this.setOnClickListener(this);
  15. }
  16. //构建视图(在子类中自定义视图)
  17. protected abstract View onCreateView();
  18. //将按钮添加到组
  19. public Selector setGroup(SelectorGroup selectorGroup) {
  20. this.selectorGroup = selectorGroup;
  21. selectorGroup.addSelector(this);
  22. return this;
  23. }
  24. @Override
  25. public void setSelected(boolean selected) {
  26. //设置按钮选中状态
  27. boolean isPreSelected = isSelected();
  28. super.setSelected(selected);
  29. if (isPreSelected != selected) {
  30. onSwitchSelected(selected);
  31. }
  32. }
  33. //按钮选中状态变更(在子类中自定义变更效果)
  34. protected abstract void onSwitchSelected(boolean isSelect);
  35. @Override
  36. public void onClick(View v) {
  37. //通知选中组,当前按钮被选中
  38. if (selectorGroup != null) {
  39. selectorGroup.onSelectorClick(this);
  40. }
  41. }
  42. }

Selector通过模版方法模式,将构建按钮视图和按钮选中效果延迟到子类构建。所以当按钮内部元素布局发生改变时不需要修改Selector,只需要新建它的子类。

    单选组持有所有按钮,当按钮被点击时,选中组遍历其余按钮并取消选中状态,以此来实现单选效果

</>复制代码

  1. public class SelectorGroup {

  2. //持有所有按钮
  3. private Set selectors = new HashSet<>();
  4. public void addSelector(Selector selector) {
  5. selectors.add(selector);
  6. }
  7. public void onSelectorClick(Selector selector) {
  8. cancelPreSelector(selector);
  9. }
  10. //遍历所有按钮,将之前选中的按钮设置为未选中
  11. private void cancelPreSelector(Selector selector) {
  12. for (Selector s : selectors) {
  13. if (!s.equals(selector) && s.isSelected()) {
  14. s.setSelected(false);
  15. }
  16. }
  17. }
  18. }

剥离行为

选中按钮后的行为被写死在SelectorGroup.onSelectorClick()中,这使得SelectorGroup中的行为无法被替换。

每次行为扩展都重新写一个SelectorGroup怎么样?不行!因为Selector是和SelectorGroup耦合的,这意味着Selector的代码也要跟着改动,这不符合开闭原则。

SelectorGroup中除了会变的“选中行为”之外,也有不会变的成分,比如“持有所有的按钮”。是不是可以增加一层抽象将变化的行为封装起来,使得SelectorGroup与变化隔离?

</>复制代码

  1. 接口是封装行为的最佳选择,可以运用策略模式将选中行为封装起来

策略模式的详细介绍可以点击这里。

这样就可以在外部构建具体的选中行为,再将其注入到SelectorGroup中,以实现动态修改行为:

</>复制代码

  1. public class SelectorGroup {

  2. private ChoiceAction choiceMode;
  3. //注入具体选中行为
  4. public void setChoiceMode(ChoiceAction choiceMode) {
  5. this.choiceMode = choiceMode;
  6. }
  7. //当按钮被点击时应用选中行为
  8. void onSelectorClick(Selector selector) {
  9. if (choiceMode != null) {
  10. choiceMode.onChoose(selectors, selector, onStateChangeListener);
  11. }
  12. }
  13. //选中后的行为被抽象成接口
  14. public interface ChoiceAction {
  15. void onChoose(Set selectors, Selector selector, StateListener stateListener);
  16. }
  17. }

将具体行为替换成接口后就好像是在原本严严实实的SelectorGroup中挖了一个洞,只要符合这个洞形状的东西都可以塞进来。这样就很灵活了。

如果每次使用SelectorGroup,都需要重新自定义选中行为也很费力,所以在其中添加了最常用的单选和多选行为:

</>复制代码

  1. public class SelectorGroup {

  2. public static final int MODE_SINGLE_CHOICE = 1;
  3. public static final int MODE_MULTIPLE_CHOICE = 2;
  4. private ChoiceAction choiceMode;
  5. //通过这个方法设置自定义行为
  6. public void setChoiceMode(ChoiceAction choiceMode) {
  7. this.choiceMode = choiceMode;
  8. }
  9. //通过这个方法设置默认行为
  10. public void setChoiceMode(int mode) {
  11. switch (mode) {
  12. case MODE_MULTIPLE_CHOICE:
  13. choiceMode = new MultipleAction();
  14. break;
  15. case MODE_SINGLE_CHOICE:
  16. choiceMode = new SingleAction();
  17. break;
  18. }
  19. }
  20. //单选行为
  21. private class SingleAction implements ChoiceAction {
  22. @Override
  23. public void onChoose(Set selectors, Selector selector, StateListener stateListener) {
  24. //将自己选中
  25. selector.setSelected(true);
  26. //将除了自己外的其他按钮设置为未选中
  27. cancelPreSelector(selector, selectors);
  28. }
  29. }
  30. //多选行为
  31. private class MultipleAction implements ChoiceAction {
  32. @Override
  33. public void onChoose(Set selectors, Selector selector, StateListener stateListener) {
  34. //反转自己的选中状态
  35. boolean isSelected = selector.isSelected();
  36. selector.setSelected(!isSelected);
  37. }
  38. }

将原本具体的行为都移到了接口中,而SelectorGroup只和抽象的接口互动,不和具体行为互动,这样的代码具有弹性。

现在只要像这样就可以分别实现单选和多选:

</>复制代码

  1. public class MainActivity extends AppCompatActivity {

  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. //多选
  7. SelectorGroup multipleGroup = new SelectorGroup();
  8. multipleGroup.setChoiceMode(SelectorGroup.MODE_MULTIPLE_CHOICE);
  9. ((Selector) findViewById(R.id.selector_10)).setGroup(multipleGroup);
  10. ((Selector) findViewById(R.id.selector_20)).setGroup(multipleGroup);
  11. ((Selector) findViewById(R.id.selector_30)).setGroup(multipleGroup);
  12. //单选
  13. SelectorGroup singleGroup = new SelectorGroup();
  14. singleGroup.setStateListener(new SingleChoiceListener());
  15. ((Selector) findViewById(R.id.single10)).setGroup(singleGroup);
  16. ((Selector) findViewById(R.id.single20)).setGroup(singleGroup);
  17. ((Selector) findViewById(R.id.single30)).setGroup(singleGroup);
  18. }
  19. }

activity_main.xml中布局了6个Selector,其中三个用于单选,三个用于多余。

菜单选

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

</>复制代码

  1. class OrderChoiceMode implements SelectorGroup.ChoiceAction {

  2. @Override
  3. public void onChoose(Set selectors, Selector selector, SelectorGroup.StateListener stateListener) {
  4. //同组互斥选中
  5. String tagPrefix = getTagPrefix(selector.getSelectorTag());
  6. cancelPreSelectorBySameTag(selectors, tagPrefix, stateListener);
  7. selector.setSelected(true);
  8. }
  9. //在同一组中取消之前的选择(要求同一组按钮的tag具有相同的前缀)
  10. private void cancelPreSelectorBySameTag(Set selectors, String tagPrefix, SelectorGroup.StateListener stateListener) {
  11. for (Selector selector : selectors) {
  12. String prefix = getTagPrefix(selector.getSelectorTag());
  13. if (prefix.equals(tagPrefix) && selector.isSelected()) {
  14. selector.setSelected(false);
  15. if (stateListener != null) {
  16. stateListener.onStateChange(selector.getSelectorTag(), false);
  17. }
  18. }
  19. }
  20. }
  21. //获取标签前缀
  22. private String getTagPrefix(String tag) {
  23. //约定tag由两个部分组成,中间用下划线分割:前缀_标签名
  24. int index = tag.indexOf("_");
  25. return tag.substring(0, index);
  26. }
  27. }

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

</>复制代码

  1. starters_pork
  2. starters_duck
  3. starters_springRoll
  4. main_pizza
  5. main_pasta
  6. soup_mushroom
  7. soup_scampi

前菜、主食、汤分别采用了starters、main、soup这样的前缀。

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

</>复制代码

  1. public class MainActivity extends AppCompatActivity {

  2. @Override
  3. protected void onCreate(Bundle savedInstanceState) {
  4. super.onCreate(savedInstanceState);
  5. setContentView(R.layout.activity_main);
  6. //order-choice
  7. SelectorGroup orderGroup = new SelectorGroup();
  8. orderGroup.setChoiceMode(new OrderChoiceMode());
  9. ((Selector) findViewById(R.id.selector_starters_duck)).setGroup(orderGroup);
  10. ((Selector) findViewById(R.id.selector_starters_pork)).setGroup(orderGroup);
  11. ((Selector) findViewById(R.id.selector_starters_springRoll)).setGroup(orderGroup);
  12. ((Selector) findViewById(R.id.selector_main_pizza)).setGroup(orderGroup);
  13. ((Selector) findViewById(R.id.selector_main_pasta)).setGroup(orderGroup);
  14. ((Selector) findViewById(R.id.selector_soup_mushroom)).setGroup(orderGroup);
  15. ((Selector) findViewById(R.id.selector_soup_scampi)).setGroup(orderGroup);
  16. }
  17. }

效果如下:

其中单选按钮通过继承Selector重写onSwitchSelected(),定义了选中效果为爱心动画。

总结

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

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

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

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

</>复制代码

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

talk is cheap, show me the code

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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/6689.html

相关文章

  • 微软测试工程师史亮:新的产品,新的挑战

    摘要:年加入微软中国有限公司,任职软件开发测试工程师,负责微软在线业务与商业智能产品的测试工作。目前,史亮正从事下一代产品的研发工作。在他们的热心帮助下,我获得了去北京面试微软测试开发工程师,简称的机会。 非商业转载请注明作译者、出处,并保留本文的原始链接:http://www.ituring.com.cn/article/114546 史亮,东南大学计算机软件与理论专业博士,研究...

    saucxs 评论0 收藏0
  • 阿里高级体验设计专家朱斌:如何通过设计管理用户注意力?

    摘要:本届工作坊,我们邀请到了拥有年资深设计经验的高级体验设计专家朱斌,他将作为产品场讲师为我们分享如何有效的管理用户注意力的话题。美国设计同行对创新的追求和包容是最能体现设计魅力的地方。 导读:7月6-7日,由msup主办的第43届MPD工作坊将于北京召开。MPD工作坊是一个围绕岗位角色发展的实践课堂,按照软件研发中心的岗位职能划分,以产品经理、团队经理、 架构师、开发经理、测试经理作为五...

    xeblog 评论0 收藏0
  • 如果想成为一名顶尖的前端,这份书单你一定要收藏!

    摘要:其中负载均衡那一节,基本上是参考的权威指南负载均衡的内容。开发指南读了一半,就是看这本书理解了的事件循环。哈哈创京东一本骗钱的书。 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯IVWEB团队 发表于云+社区专栏作者:link 2014年一月以来,自己接触web前端开发已经两年多了,记录一下自己前端学习路上看过的,以及道听途说的一些书,基本上按照由浅入深来介绍...

    callmewhy 评论0 收藏0
  • 如果想成为一名顶尖的前端,这份书单你一定要收藏!

    摘要:其中负载均衡那一节,基本上是参考的权威指南负载均衡的内容。开发指南读了一半,就是看这本书理解了的事件循环。哈哈创京东一本骗钱的书。 欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由腾讯IVWEB团队 发表于云+社区专栏作者:link 2014年一月以来,自己接触web前端开发已经两年多了,记录一下自己前端学习路上看过的,以及道听途说的一些书,基本上按照由浅入深来介绍...

    Scliang 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<