乐趣区

关于后端:聊一聊模板方法模式

对立抽取,制订标准;

一、概述

模板办法模式,又叫模板模式,属于 23 种设计模式中的 行为型模式 。在抽象类中公开定义了执行的办法,子类能够按需重写其办法,然而要以抽象类中定义的形式调用办法。总结起来就是: 定义一个操作的算法构造,而将一些步骤提早到子类中。在不扭转算法构造的状况下, 子类能重定义该算法的特定步骤。

上面是模板模式的 UML 图,抽象类(AbstractClass)定义公共的步骤和办法,顺次调用理论的模板办法,当然每个办法能够是形象办法(需交给子类实现),也能够是提供默认的办法。具体的类(ConcreteClass)能够重写所有的办法,然而不能扭转抽象类中定义的整体构造。

二、入门案例

置信大家都吃过蛋糕,当初市面上的蛋糕堪称是形形色色,你能想到的造型商家能给你整进去,你想不到的,他们也能整进去。不过无论造型如何变动,不变的有两种货色:“奶油”和“面包”。其余的资料随便搭配,就凑成了各式各样的蛋糕。

基于这个场景,咱们来写一个案例,进一步理解下模板模式;创立三个类:Cake(蛋糕)、StrawberryCake(草莓蛋糕)、CherryCake(樱桃蛋糕)。最初创立一个 Client 类,实现这个制作蛋糕的调用过程。

package com.wsrf.template;

/**
 * @author 往事如风
 * @version 1.0
 * @date 2023/5/4 16:12
 * @description:抽象类:蛋糕
 */
public abstract class Cake {

    /**
     * 制作
     */
    public void make() {System.out.println("开始筹备资料。");
        bread();
        cream();
        fruit();
        System.out.println("通过一系列的操作。");
        System.out.println("制作实现。");
    }

    /**
     * 筹备面包
     */
    public void bread() {System.out.println("筹备资料:面包");
    }

    /**
     * 筹备奶油
     */
    public void cream() {System.out.println("筹备资料:奶油");
    }

    /**
     * 筹备水果
     */
    protected abstract void fruit();}
package com.wsrf.template;

/**
 * @author 往事如风
 * @version 1.0
 * @date 2023/5/4 16:13
 * @description:具体类:草莓蛋糕
 */
public class StrawberryCake extends Cake{
    @Override
    protected void fruit() {System.out.println("筹备资料:草莓");
    }
}
package com.wsrf.template;

/**
 * @author 往事如风
 * @version 1.0
 * @date 2023/5/4 16:14
 * @description:具体类:樱桃蛋糕
 */
public class CherryCake extends Cake{
    @Override
    protected void fruit() {System.out.println("筹备资料:樱桃");
    }
}
package com.wsrf.template;

/**
 * @author 往事如风
 * @version 1.0
 * @date 2023/5/4 16:21
 * @description
 */
public class Client {public static void main(String[] args) {Cake c1 = new CherryCake();
        c1.make();
        System.out.println("-------------------------------------");
        Cake c2 = new StrawberryCake();
        c2.make();}
}
/**
输入后果:开始筹备资料。筹备资料:面包
筹备资料:奶油
筹备资料:樱桃
通过一系列的操作。制作实现。-------------------------------------
开始筹备资料。筹备资料:面包
筹备资料:奶油
筹备资料:草莓
通过一系列的操作。制作实现。*/

Cake 类中定义了制作蛋糕的整个步骤,也就是 make 办法;而后抽取了专用的办法,bread 办法和 cream 办法;最初定义一个形象办法 fruit,这个办法须要交给具体的子类 StrawberryCakeCherryCake去实现,从而定制差异化的“蛋糕”。

三、使用场景

通过下面的“蛋糕”案例,在平时开发中咱们能够具体分析一下业务需要,首先在父类中定义需要须要实现的步骤,而后将能够专用的办法抽取到父类中,将个性化的办法放到具体的子类中去实现;这样能够很好的造就“抽象化”的思维模式,这是拉开差距的第一步。

最近在开发中,遇到这样的一个业务场景:须要给不同的管理人员计算各种不同的津贴,如区域总监有区域治理津贴、佣金、造就育成津贴等等。通过剖析,每种不必类型的津贴,都是须要 金额 x 比例 x 系数,比例每种津贴都有不同的计算形式,系数也是。所以,大抵的想法就是:金额 x 比例 x 系数这个计算形式设置为对立的办法,系数和比例让具体的津贴子类去实现。所以大抵的伪代码如下;

首先,我定义了一个抽象类AbstractManageAllowanceCalService,用于定义对立的计算方法,并预留了获取比例和获取系数的形象办法。

/**
 * @author 往事如风
 * @version 1.0
 * @date 2023/5/4 17:12
 * @description:津贴计算父类
 */
@Slf4j
public abstract class AbstractManageAllowanceCalService {

    /**
     * 计算津贴
     * @param amount
     * @return
     */
    public BigDecimal calAmount(BigDecimal amount) {if (Objects.isNull(amount)) {return BigDecimal.ZERO;}
        BigDecimal ratio = getRatio();
        BigDecimal coefficient = getCoefficient();
        log.info("金额:{},系数:{},比例:{}", amount, coefficient, ratio);
        return amount.multiply(ratio).multiply(coefficient);
    }

    /**
     * 获取比例
     * @return
     */
    protected abstract BigDecimal getRatio();

    /**
     * 获取系数
     * @return
     */
    protected abstract BigDecimal getCoefficient();}

而后,定义两个具体的子类,用于计算区域治理津贴和佣金。

/**
 * @author 往事如风
 * @version 1.0
 * @date 2023/5/4 17:17
 * @description: 区域治理津贴计算
 */
@Service
public class AreaBusinessAllowanceCalService extends AbstractManageAllowanceCalService{
    /**
     * 区域治理津贴比例
     * @return
     */
    @Override
    protected BigDecimal getRatio() {return new BigDecimal(0.5).setScale(1, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 区域治理津贴系数
     * @return
     */
    @Override
    protected BigDecimal getCoefficient() {return new BigDecimal(0.92).setScale(2, BigDecimal.ROUND_HALF_UP);
    }
}
/**
 * @author 往事如风
 * @version 1.0
 * @date 2023/5/4 17:19
 * @description:佣金计算
 */
@Service
public class SalaryCalService extends AbstractManageAllowanceCalService{
    /**
     * 佣金比例
     * @return
     */
    @Override
    protected BigDecimal getRatio() {return new BigDecimal(0.45).setScale(2, BigDecimal.ROUND_HALF_UP);
    }

    /**
     * 佣金系数
     * @return
     */
    @Override
    protected BigDecimal getCoefficient() {return new BigDecimal(0.88).setScale(2, BigDecimal.ROUND_HALF_UP);
    }
}

最初,定义一个 controller 类,用于接口调用,提供计算能力;接管两个参数,金额和计算津贴类型。

/**
 * @author 往事如风
 * @version 1.0
 * @date 2023/5/4 17:21
 * @description
 */
@RestController
@RequestMapping("/cal")
public class CalController implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @PostMapping("/amount")
    public Result<BigDecimal> calAmount(BigDecimal amount, String calType) {
        AbstractManageAllowanceCalService service = null;
        if ("AREA".equals(calType)) {
            // 区域治理津贴
             service = (AbstractManageAllowanceCalService) applicationContext.getBean("areaBusinessAllowanceCalService");
        } else if ("SALARY".equals(calType)) {
            // 佣金
            service = (AbstractManageAllowanceCalService) applicationContext.getBean("salaryCalService");
        }
        if (Objects.nonNull(service)) {return Result.success(service.calAmount(amount));
        }
        return Result.fail();}

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {CalController.applicationContext = applicationContext;}
}

在这个 controller 类中,我通过剖析“类型”这个参数,来判断须要调用哪个 service 去实现具体的计算逻辑。这里用了 if-else 的形式去实现;其实也能够用到另一个设计模式——策略模式,这样写进去的代码就会比拟优雅,这里就不对策略模式开展赘述了。

四、源码中使用

4.1、JDK 源码中的模板模式

在 JDK 中其实也有很多中央使用到了模版模式,这里咱挑一个讲。并发包下的 AbstractQueuedSynchronizer 类,就是一个抽象类,也就是咱们先前的文章中提到过的 AQS。

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {public final void acquire(int arg) {if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();}

    public final boolean release(int arg) {if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

    protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();
    }

    protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();
    }
}        

其中,tryAcquire 和 tryRelease 这两个形式间接抛了异样,用 protected 关键词润饰,须要由子类去实现。而后再 acquire 和 release 办法中别离去调用这两办法。也就是 acquire 办法定义了一个对立的构造,差异化的 tryAcquire 办法须要具体的子类去实现性能,实现了模版模式。

4.2、Spring 源码中的模板模式

说到源码,Spring 是一个绕不开的话题,那就来学习下 Spring 中的模版模式。其中,有一个类 DefaultBeanDefinitionDocumentReader,它是BeanDefinitionDocumentReader 的实现类,是提取 spring 配置文件中的 bean 信息,并转化为 BeanDefinition。

public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocumentReader {protected void doRegisterBeanDefinitions(Element root) {
        BeanDefinitionParserDelegate parent = this.delegate;
        this.delegate = this.createDelegate(this.getReaderContext(), root, parent);
        //...

        this.preProcessXml(root);
        this.parseBeanDefinitions(root, this.delegate);
        this.postProcessXml(root);
        this.delegate = parent;
    }

    protected void preProcessXml(Element root) { }

    protected void postProcessXml(Element root) {}}    

这里我截图了其中的一段代码,次要是 doRegisterBeanDefinitions 这个办法,从跟节点 root 登程,root 下的每个 bean 注册定义。

该办法中还调用了 preProcessXml 和 postProcessXml 这两个办法,然而在 DefaultBeanDefinitionDocumentReader 类中,这两个办法是未实现的,须要其子类去实现具体的逻辑。所以,这里也是一个很典型的模板模式的使用。

五、总结

模板办法模式其实是一个比较简单的设计模式,它有如下长处:1、封装不变的逻辑,扩大差异化的逻辑;2、抽取公共代码,进步代码的复用性;3、父类管制行为,子类实现细节。

其毛病就是不同的实现都须要一个子类去保护,会导致子类的个数一直减少,造成零碎更加宏大。

用一句话总结:将专用的办法抽取到父类,在父类中预留可变的办法,最初子类去实现可变的办法。

模板模式更多的是考查咱们对于专用办法的提取;对于编程也是这样,更多的是一种思维能力,不能只局限于代码,要把格局关上。

六、参考源码

编程文档:https://gitee.com/cicadasmile/butte-java-note

利用仓库:https://gitee.com/cicadasmile/butte-flyer-parent
退出移动版