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

变化是永恒的,产品需求稳定不变是不可能的,和产品经理互怼是没有用的,但有一个方向是可以努力的:让代码更有弹性,以不变应万变。
继上一次发版前突然变更单选按钮样式之后,又新增了两个和选项按钮有关的需求。它们分别是多选和菜单选。多选类似于原生CheckBox,而菜单选是多选和单选的组合,类似于西餐点菜,西餐菜单将食物分为前菜、主食、汤,每种只能选择 1 个(即同组内单选,多组间多选)。
上一篇中的自定义单选按钮Selector + SelectorGroup完美 hold 住按钮样式的变化,这一次能否从容应对新增需求?
自定义单选按钮
回顾下Selector + SelectorGroup的效果:

640?wx_fmt=png


其中每一个选项就是Selector,它们的状态被SelectorGroup管理。
这组自定义控件突破了原生单选按钮的布局限制,选项的相对位置可以用 xml 定义(原生控件只能是垂直或水平铺开),而且还可以方便地更换按钮样式以及定义选中效果(上图中选中后有透明度动画)
实现关键逻辑如下:

单个按钮是一个抽象容器控件,它可以被点击并借助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,只需要新建它的子类。

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

public class SelectorGroup {
//持有所有按钮
private Set 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 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中重新定义按钮选中时的行为:同组互斥选中,不同组可以多选。这就需要一种标识组的方法,本文采用了给同组按钮设置相同前缀的做法:
starters_pork starters_duck starters_springRoll main_pizza main_pastasoup_mushroom soup_scampi 
复制代码前菜、主食、汤分别采用了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);
}

}
复制代码效果如下:

640?wx_fmt=png

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

总结

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

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

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

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

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

talk is cheap, show me the code

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

https://github.com/wisdomtl/Selector

                        喜欢 就关注吧,欢迎投稿!

640?wx_fmt=jpeg


作者:唐子玄
链接:https://juejin.im/post/5ce0e55ae51d451075366eeb
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


【6层】一字型框架办公楼(含建筑结构图、计算书) 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码
深度学习是机器学习的一个子领域,它基于人工神经网络的研究,特别是利用多层次的神经网络来进行学习和模式识别。深度学习模型能够学习数据的高层次特征,这些特征对于图像和语音识别、自然语言处理、医学图像分析等应用至关重要。以下是深度学习的一些关键概念和组成部分: 1. **神经网络(Neural Networks)**:深度学习的基础是人工神经网络,它是由多个层组成的网络结构,包括输入层、隐藏层和输出层。每个层由多个神经元组成,神经元之间通过权重连接。 2. **前馈神经网络(Feedforward Neural Networks)**:这是最常见的神经网络类型,信息从输入层流向隐藏层,最终到达输出层。 3. **卷积神经网络(Convolutional Neural Networks, CNNs)**:这种网络特别适合处理具有网格结构的数据,如图像。它们使用卷积层来提取图像的特征。 4. **循环神经网络(Recurrent Neural Networks, RNNs)**:这种网络能够处理序列数据,如时间序列或自然语言,因为它们具有记忆功能,能够捕捉数据中的时间依赖性。 5. **长短期记忆网络(Long Short-Term Memory, LSTM)**:LSTM 是一种特殊的 RNN,它能够学习长期依赖关系,非常适合复杂的序列预测任务。 6. **生成对抗网络(Generative Adversarial Networks, GANs)**:由两个网络组成,一个生成器和一个判别器,它们相互竞争,生成器生成数据,判别器评估数据的真实性。 7. **深度学习框架**:如 TensorFlow、Keras、PyTorch 等,这些框架提供了构建、训练和部署深度学习模型的工具和库。 8. **激活函数(Activation Functions)**:如 ReLU、Sigmoid、Tanh 等,它们在神经网络中用于添加非线性,使得网络能够学习复杂的函数。 9. **损失函数(Loss Functions)**:用于评估模型的预测与真实值之间的差异,常见的损失函数包括均方误差(MSE)、交叉熵(Cross-Entropy)等。 10. **优化算法(Optimization Algorithms)**:如梯度下降(Gradient Descent)、随机梯度下降(SGD)、Adam 等,用于更新网络权重,以最小化损失函数。 11. **正则化(Regularization)**:技术如 Dropout、L1/L2 正则化等,用于防止模型过拟合。 12. **迁移学习(Transfer Learning)**:利用在一个任务上训练好的模型来提高另一个相关任务的性能。 深度学习在许多领域都取得了显著的成就,但它也面临着一些挑战,如对大量数据的依赖、模型的解释性差、计算资源消耗大等。研究人员正在不断探索新的方法来解决这些问题。
1、资源项目源码均已通过严格测试验证,保证能够正常运行;、 2项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码。 1、资源项目源码均已通过严格测试验证,保证能够正常运行; 2、项目问题、技术讨论,可以给博主私信或留言,博主看到后会第一时间与您进行沟通; 3、本项目比较适合计算机领域相关的毕业设计课题、课程作业等使用,尤其对于人工智能、计算机科学与技术等相关专业,更为适合; 4、下载使用后,可先查看README.md或论文文件(如有),本项目仅用作交流学习参考,请切勿用于商业用途。 5、资源来自互联网采集,如有侵权,私聊博主删除。 6、可私信博主看论文后选择购买源代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值