6

设计模式最佳套路4 —— 愉快地使用模板模式

 3 years ago
source link: https://my.oschina.net/u/4662964/blog/5016665
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
设计模式最佳套路4 —— 愉快地使用模板模式

9f65dbf6-0439-49a4-9281-7e10d9795da0.gif

什么是模板模式



模板模式(Template Pattern)  又叫模板方法模式,其定义了操作的流程,并将流程中的某些步骤延迟到子类中进行实现,使得子类在不改变操作流程的前提下,即可重新定义该操作的某些特定步骤。例如做菜,操作流程一般为 “准备菜”->“放油”->“炒菜”->“调味”->“装盘”,但可能对于不同的菜要放不同类型的油,不同的菜调味方式也可能不一样。

何时使用模板模式



当一个操作的流程较为复杂,可分为多个步骤,且对于不同的操作实现类,流程步骤相同,只有部分特定步骤才需要自定义,此时可以考虑使用模板模式。如果一个操作不复杂(即只有一个步骤),或者不存在相同的流程,那么应该使用策略模式。从这也可看出模板模式和策略模式的区别:策略模式关注的是多种策略(广度),而模板模式只关注同种策略(相同流程),但是具备多个步骤,且特定步骤可自定义(深度)。 ca032ee1-a8f2-42bc-badc-c55e1217d17b.gif

愉快地使用模板模式

背景

我们平台的动态表单在配置表单项的过程中,每新增一个表单项,都要根据表单项的组件类型(例如 单行文本框、下拉选择框)和当前输入的各种配置来转换好对应的 Schema 并保存在 DB 中。一开始,转换的代码逻辑大概是这样的:
public class FormItemConverter {    /**     * 将输入的配置转变为表单项     *     * @param config 前端输入的配置     * @return 表单项     */    public FormItem convert(FormItemConfig config) {        FormItem formItem = new FormItem();        // 公共的表单项属性        formItem.setTitle(config.getTitle());        formItem.setCode(config.getCode());        formItem.setComponent(config.getComponent());        // 创建表单组件的属性        FormComponentProps props = new FormComponentProps();        formItem.setComponentProps(props);        // 公共的组件属性        if (config.isReadOnly()) {            props.setReadOnly(true);        }        FormItemTypeEnum type = config.getType();        // 下拉选择框的特殊属性处理        if (type == ComponentTypeEnum.DROPDOWN_SELECT) {            props.setAutoWidth(false);            if (config.isMultiple()) {                props.setMode("multiple");            }        }        // 模糊搜索框的特殊属性处理        if (type == ComponentTypeEnum.FUZZY_SEARCH) {            formItem.setFuzzySearch(true);            props.setAutoWidth(false);        }        // ...  其他组件的特殊处理        // 创建约束规则        List<FormItemRule> rules = new ArrayList<>(2);        formItem.setRules(rules);        // 每个表单项都可有的约束规则        if (config.isRequired()) {            FormItemRule requiredRule = new FormItemRule();            requiredRule.setRequired(true);            requiredRule.setMessage("请输入" + config.getTitle());            rules.add(requiredRule);        }        // 文本输入框才有的规则        if (type == ComponentTypeEnum.TEXT_INPUT || type == ComponentTypeEnum.TEXT_AREA) {            Integer minLength = config.getMinLength();            if (minLength != null && minLength > 0) {                FormItemRule minRule = new FormItemRule();                minRule.setMin(minLength);                minRule.setMessage("请至少输入 " + minLength + " 个字");                rules.add(minRule);            }            Integer maxLength = config.getMaxLength();            if (maxLength != null && maxLength > 0) {                FormItemRule maxRule = new FormItemRule();                maxRule.setMax(maxLength);                maxRule.setMessage("请最多输入 " + maxLength + " 个字");                rules.add(maxRule);            }        }        // ... 其他约束规则        return formItem;    }}

很明显,这份代码违反了 开闭原则(对扩展开放,对修改关闭):如果此时需要添加一种新的表单项(包含特殊的组件属性),那么不可避免的要修改 convert 方法来进行新表单项的特殊处理。观察上面的代码,将配置转变为表单项 这个操作,满足以下流程:

  1. 创建表单项,并设置通用的表单项属性,然后再对不同表单项的特殊属性进行处理
  2. 创建组件属性,处理通用的组件属性,然后再对不同组件的特殊属性进行处理
  3. 创建约束规则,处理通用的约束规则,然后再对不同表单项的特性约束规则进行处理
这不正是符合模板模式的使用场景(操作流程固定,特殊步骤可自定义处理)吗?基于上面这个场景,下面我就分享一下我目前基于 Spring 实现模板模式的 “最佳套路”(如果你有更好的套路,欢迎赐教和讨论哦)~

6ec496db-bfba-4e53-a705-174e0e1765fc.gif

方案

  • 定义出模板

即首先定义出表单项转换的操作流程,即如下的 convert 方法(使用 final 修饰,确保子类不可修改操作流程):
public abstract class FormItemConverter {    /**     * 子类可处理的表单项类型     */    public abstract FormItemTypeEnum getType();    /**     * 将输入的配置转变为表单项的操作流程     *     * @param config 前端输入的配置     * @return 表单项     */    public final FormItem convert(FormItemConfig config) {        FormItem item = createItem(config);        // 表单项创建完成之后,子类如果需要特殊处理,可覆写该方法        afterItemCreate(item, config);        FormComponentProps props = createComponentProps(config);        item.setComponentProps(props);        // 组件属性创建完成之后,子类如果需要特殊处理,可覆写该方法        afterPropsCreate(props, config);        List<FormItemRule> rules = createRules(config);        item.setRules(rules);        // 约束规则创建完成之后,子类如果需要特殊处理,可覆写该方法        afterRulesCreate(rules, config);        return item;    }    /**     * 共用逻辑:创建表单项、设置通用的表单项属性     */    private FormItem createItem(FormItemConfig config) {        FormItem formItem = new FormItem();        formItem.setCode(config.getCode());        formItem.setTitle(config.getTitle());        formItem.setComponent(config.getComponent());        return formItem;    }    /**     * 表单项创建完成之后,子类如果需要特殊处理,可覆写该方法     */    protected void afterItemCreate(FormItem item, FormItemConfig config) { }    /**     * 共用逻辑:创建组件属性、设置通用的组件属性     */    private FormComponentProps createComponentProps(FormItemConfig config) {        FormComponentProps props = new FormComponentProps();        if (config.isReadOnly()) {            props.setReadOnly(true);        }        if (StringUtils.isNotBlank(config.getPlaceholder())) {            props.setPlaceholder(config.getPlaceholder());        }        return props;    }    /**     * 组件属性创建完成之后,子类如果需要特殊处理,可覆写该方法     */    protected void afterPropsCreate(FormComponentProps props, FormItemConfig config) { }    /**     * 共用逻辑:创建约束规则、设置通用的约束规则     */    private List<FormItemRule> createRules(FormItemConfig config) {        List<FormItemRule> rules = new ArrayList<>(4);        if (config.isRequired()) {            FormItemRule requiredRule = new FormItemRule();            requiredRule.setRequired(true);            requiredRule.setMessage("请输入" + config.getTitle());            rules.add(requiredRule);        }        return rules;    }    /**     * 约束规则创建完成之后,子类如果需要特殊处理,可覆写该方法     */    protected void afterRulesCreate(List<FormItemRule> rules, FormItemConfig config) { }}
  • 模板的实现

针对不同的表单项,对特殊步骤进行自定义处理:
/** * 下拉选择框的转换器 */@Componentpublic class DropdownSelectConverter extends FormItemConverter {    @Override    public FormItemTypeEnum getType() {        return FormItemTypeEnum.DROPDOWN_SELECT;    }    @Override    protected void afterPropsCreate(FormComponentProps props, FormItemConfig config) {        props.setAutoWidth(false);        if (config.isMultiple()) {            props.setMode("multiple");        }    }}/** * 模糊搜索框的转换器 */@Componentpublic class FuzzySearchConverter extends FormItemConverter {    @Override    public FormItemTypeEnum getType() {        return FormItemTypeEnum.FUZZY_SEARCH;    }    @Override    protected void afterItemCreate(FormItem item, FormItemConfig config) {        item.setFuzzySearch(true);    }    @Override    protected void afterPropsCreate(FormComponentProps props, FormItemConfig config) {        props.setAutoWidth(false);    }}/** * 通用文本类转换器 */public abstract class CommonTextConverter extends FormItemConverter {    @Override    protected void afterRulesCreate(List<FormItemRule> rules, FormItemConfig config) {        Integer minLength = config.getMinLength();        if (minLength != null && minLength > 0) {            FormItemRule minRule = new FormItemRule();            minRule.setMin(minLength);            minRule.setMessage("请至少输入 " + minLength + " 个字");            rules.add(minRule);        }        Integer maxLength = config.getMaxLength();        if (maxLength != null && maxLength > 0) {            FormItemRule maxRule = new FormItemRule();            maxRule.setMax(maxLength);            maxRule.setMessage("请最多输入 " + maxLength + " 个字");            rules.add(maxRule);        }    }}/** * 单行文本框的转换器 */@Componentpublic class TextInputConverter extends CommonTextConverter {    @Override    public FormItemTypeEnum getType() {        return FormItemTypeEnum.TEXT_INPUT;    }}/** * 多行文本框的转换器 */@Componentpublic class TextAreaConvertor extends FormItemConverter {    @Override    public FormItemTypeEnum getType() {        return FormItemTypeEnum.TEXT_AREA;    }}
  • 制作简单工厂

@Componentpublic class FormItemConverterFactory {    private static final     EnumMap<FormItemTypeEnum, FormItemConverter> CONVERTER_MAP = new EnumMap<>(FormItemTypeEnum.class);    /**     * 根据表单项类型获得对应的转换器     *     * @param type 表单项类型     * @return 表单项转换器     */    public FormItemConverter getConverter(FormItemTypeEnum type) {        return CONVERTER_MAP.get(type);    }    @Autowired    public void setConverters(List<FormItemConverter> converters) {        for (final FormItemConverter converter : converters) {            CONVERTER_MAP.put(converter.getType(), converter);        }    }}
  • 投入使用

@Componentpublic class FormItemManagerImpl implements FormItemManager {    @Autowired    private FormItemConverterFactory converterFactory;    @Override    public List<FormItem> convertFormItems(JSONArray inputConfigs) {        return IntStream.range(0, inputConfigs.size())                        .mapToObj(inputConfigs::getJSONObject)                        .map(this::convertFormItem)                        .collect(Collectors.toList());    }    private FormItem convertFormItem(JSONObject inputConfig) {        FormItemConfig itemConfig = inputConfig.toJavaObject(FormItemConfig.class);        FormItemConverter converter = converterFactory.getConverter(itemConfig.getType());        if (converter == null) {            throw new IllegalArgumentException("不存在转换器:" + itemConfig.getType());        }        return converter.convert(itemConfig);    }}

Factory 只负责获取 Converter,每个 Converter 只负责对应表单项的转换功能,Manager 只负责逻辑编排,从而达到功能上的 “低耦合高内聚”。

54698d25-710c-402c-9766-9dcdaba4d1e5.jpg

  • 设想一次扩展

此时要加入一种新的表单项 —— 数字选择器(NUMBER_PICKER),它有着特殊的约束条件:最小值和最大值,输入到 FormItemConfig 时分别为 minNumer 和 maxNumber。
@Componentpublic class NumberPickerConverter extends FormItemConverter {    @Override    public FormItemTypeEnum getType() {        return FormItemTypeEnum.NUMBER_PICKER;    }    @Override    protected void afterRulesCreate(List<FormItemRule> rules, FormItemConfig config) {        Integer minNumber = config.getMinNumber();        // 处理最小值        if (minNumber != null) {            FormItemRule minNumRule = new FormItemRule();            minNumRule.setMinimum(minNumber);            minNumRule.setMessage("输入数字不能小于 " + minNumber);            rules.add(minNumRule);        }        Integer maxNumber = config.getMaxNumber();        // 处理最大值        if (maxNumber != null) {            FormItemRule maxNumRule = new FormItemRule();            maxNumRule.setMaximum(maxNumber);            maxNumRule.setMessage("输入数字不能大于 " + maxNumber);            rules.add(maxNumRule);        }    }}
此时,我们只需要添加对应的枚举和实现对应的 FormItemConverter,并不需要修改任何逻辑代码,因为 Spring 启动时会自动帮我们处理好 NUMBER_PICKER 和 NumberPickerConverter 的关联关系 —— 完美符合 “开闭原则”。 45f8d392-6de1-493e-843f-638ba84f1e81.gif
淘系技术部-全域营销团队-诚招英才

战斗在阿里电商的核心地带,负责连接供需两端,支持电商营销领域的各类产品、平台和解决方案,其中包括聚划算、百亿补贴、天猫U先、天猫小黑盒、天猫新品孵化、品牌号等重量级业务。我们深度参与双11、618、99划算节等年度大促,不断挑战技术的极限! 团队成员背景多样,有深耕电商精研技术的老司机,也有朝气蓬勃的小萌新,更有可颜可甜的小姐姐,期待具有好奇心和思考力的你的加入!

【招聘岗位】Java 工程师 、数据工程师

如果您有兴趣可将简历发至 [email protected]

或者添加作者微信 wx_zhou_mi 进行详细咨询,欢迎来撩~



✿    拓展阅读

作者|之叶

编辑|橙子君

出品|阿里巴巴新零售淘系技术

40bd0d3f-1091-44d6-bf84-8c06a340cd45.jpge5a340a4-32b1-410c-b2fb-7c64ad02e3cd.png

本文分享自微信公众号 - 淘系技术(AlibabaMTT)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK