聊一聊模板方法模式

Posted 知了一笑

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了聊一聊模板方法模式相关的知识,希望对你有一定的参考价值。

模板方法模式,又叫模板模式,属于23种设计模式中的行为型模式。在抽象类中公开定义了执行的方法,子类可以按需重写其方法,但是要以抽象类中定义的方式调用方法。

统一抽取,制定规范;

一、概述

模板方法模式,又叫模板模式,属于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、父类控制行为,子类实现细节。

其缺点就是不同的实现都需要一个子类去维护,会导致子类的个数不断增加,造成系统更加庞大。

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

模板模式更多的是考察我们对于公用方法的提取;对于编程也是这样,更多的是一种思维能力,不能只局限于代码,要把格局打开。

设计模式聊一聊模板方法设计模式

1.关于模板方法设计模式

 有一个问题
 就是说,这个发短信的代码里面,都有一段通用的计算逻辑,是完全相同的代码
 但是相同的一段代码,给通过复制粘贴的方式,放到了不同的类里去
 一旦说,那段通用的计算逻辑,要修改,就涉及到多个类都要去修改那个代码
 如果你一旦忘了修改某个类中的那段代码,后果不堪设想
 而且到了后期,几乎没人记得清楚,那段通用逻辑代码放在了多少个类中,如果要排查,需要将很多类重新  读一遍代码
这就是垃圾代码,扩展性,维护性,很烂

2.一个helloworld级别的demo

public class TemplateMethodPatterDemo 

	public static void main(String[] args) 
		DiscountCalculator calculator1 = new DiscountCalculator1();
		calculator1.calculate();
		
		DiscountCalculator calculator2 = new DiscountCalculator2();
		calculator2.calculate();
		
		DiscountCalculator calculator3 = new DiscountCalculator3();
		calculator3.calculate();
	
	
	public interface DiscountCalculator 
		
		void calculate();
		
	
	
	/**
	 * 模板方法实现的精华所在
	 * @author zhonghuashishan
	 *
	 */
	public static abstract class AbstractDiscountCalculator implements DiscountCalculator 
		
		public void calculate() 
			// 完成通用的计算逻辑
			commonCalculate();
			// 完成特殊的计算逻辑
			specificCalculate();
		
		
		private void commonCalculate() 
			System.out.println("通用的计算逻辑,修改了一下");
		
		
		protected abstract void specificCalculate();
		
	
	
	public static class DiscountCalculator1 extends AbstractDiscountCalculator 
		
		public void specificCalculate() 
			System.out.println("优惠计算器1的特殊计算逻辑");  
		
		
	
	
	public static class DiscountCalculator2 extends AbstractDiscountCalculator 
			
		public void specificCalculate() 
			System.out.println("优惠计算器2的特殊计算逻辑");  
		
		
	
	
	public static class DiscountCalculator3 extends AbstractDiscountCalculator 
		
		public void specificCalculate() 
			System.out.println("优惠计算器3的特殊计算逻辑");  
		
		
	
	

以上是关于聊一聊模板方法模式的主要内容,如果未能解决你的问题,请参考以下文章

设计模式聊一聊模板方法设计模式

设计模式聊一聊模板方法设计模式

设计模式聊一聊模板方法设计模式

具体聊一聊简单工厂模式工厂方法模式和抽象工厂模式

单例模式(下)---聊一聊单例模式的几种写法

单例模式(下)---聊一聊单例模式的几种写法