This is the fourth part of design Patterns.

“Design Pattern Best Practice # 1 — Use Strategic Mode joyfully”

“Design Pattern Best Practice # 2 — Use Pipes happily”

“Design Patterns Best Practice # 3 — Use Proxy Patterns happily”

What is a template pattern

Template patterns, also known as Template method patterns, define the process of an operation and defer certain steps of the process to subclasses for implementation, so that subclasses can redefine certain steps of the operation without changing the process of the operation. For example, cooking, the operation process is generally “prepare food” -> “put oil” -> “stir-fry” -> “seasoning” -> “plate”, but may be for different dishes to put different types of oil, different dishes seasoning may be different.

When to use the template pattern

If the process of an operation is complex and can be divided into multiple steps, and the process steps are the same for different operation implementation classes, only some specific steps need to be customized. In this case, the template mode can be used. If an operation is not complex (that is, only one step), or if the same process does not exist, then the policy pattern should be used. This also illustrates the difference between the template pattern and the policy pattern: the policy pattern focuses on multiple policies (breadth), while the template pattern focuses on the same policy (same process), but has multiple steps, and specific steps can be customized (depth).

Use the template pattern happily

background

In our platform’s dynamic form configuration process, each new form item needs to be converted to the corresponding Schema and saved in DB according to the component type of the form item (such as single-line text box, drop-down selection box) and the various configurations currently entered. At first, the code logic for the transformation looks something like this:

Public class FormItemConverter {/** * convert input configuration to FormItem ** @param config front-end configuration * @return FormItem */ public FormItem convert(FormItemConfig config) { FormItem formItem = new FormItem(); // the public formItem property formitem.settitle (config.gettitle ()); formItem.setCode(config.getCode()); formItem.setComponent(config.getComponent()); FormComponentProps = new FormComponentProps(); FormComponentProps(); formItem.setComponentProps(props); If (config.isreadonly ()) {props. SetReadOnly (true); } FormItemTypeEnum type = config.getType(); / / the drop-down selection box of special property handling the 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 (" please enter "+ config.getTitle()); rules.add(requiredRule); } / / text input box only if some rules (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 (" Please enter at least "+ minLength +" words "); rules.add(minRule); } Integer maxLength = config.getMaxLength(); if (maxLength ! = null && maxLength > 0) { FormItemRule maxRule = new FormItemRule(); maxRule.setMax(maxLength); MaxRule. SetMessage (" please enter at most "+ maxLength +" words "); rules.add(maxRule); }} / /... Other constraint rules return formItem; }}Copy the code

Obviously, this code violates the open closed principle (open for extensions, closed for changes) : if you need to add a new form item (with special component properties) at this point, you will inevitably change the convert method to do the special handling for the new form item. Looking at the code above, the action of turning the configuration into a form item satisfies the following process:

  1. Create the form items and set the common form item properties, and then work on the special properties of the different form items
  2. Create component properties, work on the generic component properties, and then work on the special properties for different components
  3. Create constraint rules, process the general constraint rules, and then process the feature constraint rules for different form items

Isn’t this exactly the use scenario that conforms to the template pattern (the operation process is fixed, and the special steps can be customized)? With that in mind, let me share my current “best practices” for implementing template patterns based on Spring (if you have better practices, please feel free to comment on them)

plan

Define the template

That is, first define the operation process of the form item transformation, namely the following convert method (using the final modifier to ensure that subclasses cannot modify the operation process) :

Public abstract class FormItemConverter {/** * subclass FormItemTypeEnum getType(); Public final FormItem convert(FormItemConfig config) public final FormItem convert(FormItemConfig config) public final FormItem convert(FormItemConfig config) { FormItem item = createItem(config); AfterItemCreate (item, config); afterItemCreate(item, config); afterItemCreate(item, config); FormComponentProps props = createComponentProps(config); item.setComponentProps(props); AfterPropsCreate (props, config); afterPropsCreate(props, config); afterPropsCreate(props, config); List<FormItemRule> rules = createRules(config); item.setRules(rules); AfterRulesCreate (rule, config); afterRulesCreate(rule, config); afterRulesCreate(rule, 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; } /** * After the form item is created, if the subclass needs special treatment, */ protected void afterItemCreate(FormItem item, FormItemConfig config) {} /** * Create component properties and set common component properties */ private FormComponentProps createComponentProps(FormItemConfig config) {FormComponentProps = new FormComponentProps(); if (config.isReadOnly()) { props.setReadOnly(true); } if (StringUtils.isNotBlank(config.getPlaceholder())) { props.setPlaceholder(config.getPlaceholder()); } return props; } /** * After the component property is created, subclasses that need special treatment, */ 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 (" please enter "+ config.getTitle()); rules.add(requiredRule); } return rules; } /** * after the constraint rule is created, if the subclass needs special treatment, */ protected void afterRulesCreate(List<FormItemRule> rules, FormItemConfig config) {}}Copy the code

Template implementation

Customizing special steps for different form items:

/ / @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"); }}} /** ** Extends 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 (" Please enter at least "+ minLength +" words "); rules.add(minRule); } Integer maxLength = config.getMaxLength(); if (maxLength ! = null && maxLength > 0) { FormItemRule maxRule = new FormItemRule(); maxRule.setMax(maxLength); MaxRule. SetMessage (" please enter at most "+ maxLength +" words "); rules.add(maxRule); }}} /** * Componentpublic Class TextInputConverter extends CommonTextConverter {@Override public FormItemTypeEnum getType() { return FormItemTypeEnum.TEXT_INPUT; }} /** * Extends FormItemConverter */ @ComponentPublic Class TextAreaConvertor extends FormItemConverter {@Override public FormItemTypeEnum getType() { return FormItemTypeEnum.TEXT_AREA; }}Copy the code

Simple manufacturing factory

@Componentpublic class FormItemConverterFactory { private static final EnumMap<FormItemTypeEnum, FormItemConverter> CONVERTER_MAP = new EnumMap<>(FormItemTypeEnum.class); Public FormItemConverter getConverter(FormItemTypeEnum) 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); }}}Copy the code

Put into use

@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(" There is no converter: "+ ItemConfig.getType ()); } return converter.convert(itemConfig); }}Copy the code

The Factory is only responsible for obtaining the Converter, each Converter is only responsible for the conversion function of the corresponding form items, and the Manager is only responsible for the logical arrangement, so as to achieve the “low coupling and high cohesion” in the function.

Imagine an extension

A new form item, the NUMBER_PICKER, is added with special constraints: a minimum value and a maximum value, minNumer and maxNumber, respectively, when entered into FormItemConfig.

@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 (" input number cannot be smaller than "+ minNumber); rules.add(minNumRule); } Integer maxNumber = config.getMaxNumber(); If (maxNumber! = null) { FormItemRule maxNumRule = new FormItemRule(); maxNumRule.setMaximum(maxNumber); Maxnumrule-setmessage (" input number cannot be greater than "+ maxNumber); rules.add(maxNumRule); }}}Copy the code

At this point, we just need to add the corresponding enumeration and implement the corresponding FormItemConverter. We don’t need to change any logical code. Because Spring automatically handles the relationship between NUMBER_PICKER and NumberPickerConverter when it starts — perfectly in line with the “open closed principle.”