您现在的位置是:首页 >其他 >设计模式之【模板方法模式】,模板方法和函数式回调,哪个才是趋势?网站首页其他

设计模式之【模板方法模式】,模板方法和函数式回调,哪个才是趋势?

秃了也弱了。 2024-06-17 10:26:10
简介设计模式之【模板方法模式】,模板方法和函数式回调,哪个才是趋势?

一、什么是模板方法模式

模板方法模式(Template Method Pattern)又叫模板模式,是指定义一个操作中的算法的框架,而将一些步骤延迟到子类中。使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤,属于行为型设计模式。

模板方法模式实际上是封装了一个固定流程,该流程由几个步骤组成,具体步骤可以由子类进行不同实现,从而让固定的流程产生不同的结果。它非常简单,其实就是类的继承机制,但它却是一个应用非常广泛的模式。目标方法模式的本质是抽象封装流程,具体进行实现。

例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等,其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。

1、主要角色

在这里插入图片描述

模板方法(Template Method)模式包含以下主要角色:

  • 抽象类(Abstract Class):负责给出一个算法的轮廓和骨架。它由一个模板方法和若干个基本方法构成。
  • 具体子类(Concrete Class):实现抽象类中所定义的抽象方法和钩子方法,它们是一个顶级逻辑的组成步骤。

抽象类包含模板方法和基本方法,模板方法定义了算法的骨架,按某种顺序调用其包含的基本方法。基本方法是实现算法各个步骤的方法,是模板方法的组成部分。基本方法又可以分为三种:

  • 抽象方法(Abstract Method) :一个抽象方法由抽象类声明、由其具体子类实现。
  • 具体方法(Concrete Method) :一个具体方法由一个抽象类或具体类声明并实现,其子类可以进行覆盖也可以直接继承。
  • 钩子方法(Hook Method) :在抽象类中已经实现,包括用于判断的逻辑方法和需要子类重写的空方法两种。一般钩子方法是用于判断的逻辑方法,这类方法名一般为isXxx,返回值类型为boolean类型或int类型。

2、应用场景

1、一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
2、各子类中公共的行为被提取出来并集中到一个公共的父类中,从而避免代码重复。
3、需要通过子类来决定父类算法中某个步骤是否执行,实现子类对父类的反向控制。

3、优缺点

优点:

  • 提高代码复用性,将相同部分的代码放在抽象的父类中,而将不同的代码放入不同的子类中。
  • 实现了控制反转,通过一个父类调用其子类的操作,通过对子类的具体实现扩展不同的行为,实现了反向控制 ,并符合“开闭原则”。

缺点:

  • 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象。
  • 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
  • 继承关系自身的缺点,如果父类添加新的抽象方法,所有子类都要改一遍。

4、注意事项及细节

  • 模板方法模式的基本思想是:算法只存在于一个地方,也就是在父类中,容易修改。需要修改算法时,只需要修改父类的模板方法或者已经实现的某些步骤,子类就会继承这些修改。
  • 实现了最大化代码复用。父类的模板方法和已实现的某些步骤会被子类继承而直接使用。
  • 既统一了算法,也提供了很大的灵活性。父类的模板方法确保了算法的结构保持不变,同时由子类提供部分步骤的实现。
  • 该模式的不足之处:每一个不同的实现都需要一个子类实现,导致类的个数增加,使得系统更加庞大。
  • 一般模板方法都加上final关键字,防止子类重写模板方法。
  • 模板方法模式使用场景:当要完成某个过程,该过程要执行一系列步骤,这一系列的步骤基本相同,但其个别步骤在实现时可能不同,通常考虑用模板方法模式来处理。

二、实例

1、炒菜案例

炒菜的步骤是固定的,分为倒油、热油、倒蔬菜、倒调料品、翻炒等步骤。现通过模板方法模式来用代码模拟。类图如下:
在这里插入图片描述

// 抽象模板类
public abstract class AbstractClass {
	public final void cookProcess() {
		//第一步:倒油
		this.pourOil();
		//第二步:热油
		this.heatOil();
		//第三步:倒蔬菜
		this.pourVegetable();
		//第四步:倒调味料
		this.pourSauce();
		//第五步:翻炒
		this.fry();
	}
	// 第一步都是一样的,倒油
	public void pourOil() {
		System.out.println("倒油");
	}
	//第二步:热油是一样的,所以直接实现
	public void heatOil() {
		System.out.println("热油");
	}
	//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
	public abstract void pourVegetable();
	//第四步:倒调味料是不一样
	public abstract void pourSauce();
	//第五步:翻炒是一样的,所以直接实现
	public void fry(){
		System.out.println("炒啊炒啊炒到熟啊");
	}
}
// 炒包菜
public class ConcreteClass_BaoCai extends AbstractClass {
	@Override
	public void pourVegetable() {
		System.out.println("下锅的蔬菜是包菜");
	}
	@Override
	public void pourSauce() {
		System.out.println("下锅的酱料是辣椒");
	}
}
// 炒菜心
public class ConcreteClass_CaiXin extends AbstractClass {
	@Override
	public void pourVegetable() {
		System.out.println("下锅的蔬菜是菜心");
	}
	@Override
	public void pourSauce() {
		System.out.println("下锅的酱料是蒜蓉");
	}
}
// 测试类
public class Client {
	public static void main(String[] args) {
		//炒手撕包菜
		ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
		baoCai.cookProcess();
		//炒蒜蓉菜心
		ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
		caiXin.cookProcess();
	}
}

注意:为防止恶意操作,一般模板方法都加上 final 关键词。

(1)模板方法模式的钩子方法

在模板方法模式的父类,我们可以定义一个方法,它默认不做任何事,子类可以视情况要不要覆盖它,该方法称为“钩子”。

钩子方法的主要目的是用来干预执行流程,使得我们控制行为流程更加灵活,更符合实际业务的需求。钩子方法一般为适合条件分支语句的返回值(如boolean、int等),我们可以根据自己的业务场景来决定是否需要使用钩子方法。

我们还是以炒菜为例,在模板抽象类中加一步:焯水。并不是炒所有的菜都需要焯水的:

// 抽象模板类
public abstract class AbstractClass {
	public final void cookProcess() {
		if(this.needBlanching()) {
			// 按需要判断是否要焯水
			this.blanching();
		}
		//第一步:倒油
		this.pourOil();
		//第二步:热油
		this.heatOil();
		//第三步:倒蔬菜
		this.pourVegetable();
		//第四步:倒调味料
		this.pourSauce();
		//第五步:翻炒
		this.fry();
	}
	public boolean needBlanching() {
		return false;
	}
	public void blanching() {
		System.out.println("焯水");
	}
	// 第一步都是一样的,倒油
	public void pourOil() {
		System.out.println("倒油");
	}
	//第二步:热油是一样的,所以直接实现
	public void heatOil() {
		System.out.println("热油");
	}
	//第三步:倒蔬菜是不一样的(一个下包菜,一个是下菜心)
	public abstract void pourVegetable();
	//第四步:倒调味料是不一样
	public abstract void pourSauce();
	//第五步:翻炒是一样的,所以直接实现
	public void fry(){
		System.out.println("炒啊炒啊炒到熟啊");
	}
}
// 炒包菜
public class ConcreteClass_BaoCai extends AbstractClass {
	@Override
	public boolean needBlanching() {
		System.out.println("下锅的蔬菜是包菜,需要焯水");
		return true;
	}
	@Override
	public void pourVegetable() {
		System.out.println("下锅的蔬菜是包菜");
	}
	@Override
	public void pourSauce() {
		System.out.println("下锅的酱料是辣椒");
	}
}
// 炒菜心
public class ConcreteClass_CaiXin extends AbstractClass {
	@Override
	public void pourVegetable() {
		System.out.println("下锅的蔬菜是菜心");
	}
	@Override
	public void pourSauce() {
		System.out.println("下锅的酱料是蒜蓉");
	}
}
// 测试类
public class Client {
	public static void main(String[] args) {
		//炒手撕包菜
		ConcreteClass_BaoCai baoCai = new ConcreteClass_BaoCai();
		baoCai.cookProcess();
		//炒蒜蓉菜心
		ConcreteClass_CaiXin caiXin = new ConcreteClass_CaiXin();
		caiXin.cookProcess();
	}
}

2、重构JDBC案例

创建一个模板类JdbcTemplate,封装所有的JDBC操作。

public abstract class JdbcTemplate {
    private DataSource dataSource;

    public JdbcTemplate2(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public final List<?> executeQuery(String sql, Object[] values){
        try {
            //1、获取连接
            Connection conn = this.getConnection();
            //2、创建语句集
            PreparedStatement pstm = this.createPrepareStatement(conn,sql);
            //3、执行语句集
            ResultSet rs = this.executeQuery(pstm,values);
            //4、处理结果集
            List<?> result = this.parseResultSet(rs);
            //5、关闭结果集
            rs.close();
            //6、关闭语句集
            pstm.close();
            //7、关闭连接
            conn.close();
            return result;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    private List<?> parseResultSet(ResultSet rs) throws Exception {
        List<Object> result = new ArrayList<Object>();
        int rowNum = 0;
        while (rs.next()){
            result.add(this.mapRow(rs,rowNum++));
        }
        return result;
    }
	// 抽象方法
    public abstract Object mapRow(ResultSet rs,int rowNum) throws Exception;


    private ResultSet executeQuery(PreparedStatement pstm, Object[] values) throws SQLException {
        for (int i = 0; i < values.length; i++) {
            pstm.setObject(i,values[i]);
        }
        return pstm.executeQuery();
    }

    private PreparedStatement createPrepareStatement(Connection conn, String sql) throws SQLException {
        return conn.prepareStatement(sql);
    }

    private Connection getConnection() throws SQLException {
        return this.dataSource.getConnection();
    }
}

public class MemberDao extends JdbcTemplate {
    public MemberDao2(DataSource dataSource) {
        super(dataSource);
    }

    public Object mapRow(ResultSet rs, int rowNum) throws Exception {
        Member member = new Member();
        //字段过多,原型模式
        member.setUsername(rs.getString("username"));
        member.setPassword(rs.getString("password"));
        member.setAge(rs.getInt("age"));
        member.setAddr(rs.getString("addr"));
        return member;
    }

    public List<?> selectAll(){
        String sql = "select * from t_member";
        return super.executeQuery(sql ,null);
    }
}
public class Test {
    public static void main(String[] args) {
        MemberDao memberDao = new MemberDao(new DataSource());
        List<?> result = memberDao.selectAll();
    }
}

我们使用模板方法封装jdbc,将数据映射方法在子类重写,极大地提高了代码复用率。

但是,这种方案真的就是最优方案吗?

三、模板方法模式与Callback回调模式

模板模式常用在框架开发中,通过提供功能扩展点,让框架用户在不修改框架源码的情况下,基于扩展点定制化框架的功能。除此之外,模板模式还可以起到代码复用的作用。

复用和扩展是模板模式的两大作用,实际上,还有另外一个技术概念,也能起到跟模板模式相同的作用,那就是回调(Callback)

1、回调基本原理

相对于普通的函数调用来说,回调是一种双向调用关系。A 类事先注册某个函数 F 到 B 类,A 类在调用 B 类的 P 函数的时候,B 类反过来调用 A 类注册给它的 F 函数。这里的 F 函数就是“回调函数”。A 调用 B,B 反过来又调用 A,这种调用机制就叫作“回调”。

// A 类将回调函数传递给 B 类
public interface ICallback {
  void methodToCallback();
}
public class BClass {
  public void process(ICallback callback) {
    //...
    callback.methodToCallback();
    //...
  }
}
public class AClass {
  public static void main(String[] args) {
    BClass b = new BClass();
    b.process(new ICallback() { //回调对象
      @Override
      public void methodToCallback() {
        System.out.println("Call back me.");
      }
    });
  }
}

回调不仅可以应用在代码设计上,在更高层次的架构设计上也比较常用。比如,通过三方支付系统来实现支付功能,用户在发起支付请求之后,一般不会一直阻塞到支付结果返回,而是注册回调接口(类似回调函数,一般是一个回调用的 URL)给三方支付系统,等三方支付系统执行完成之后,将结果通过回调接口返回给用户。

回调可以分为同步回调和异步回调(或者延迟回调)。同步回调指在函数返回之前执行回调函数;异步回调指的是在函数返回之后执行回调函数。上面的代码实际上是同步回调的实现方式,在 process() 函数返回之前,执行完回调函数 methodToCallback()。而上面支付的例子是异步回调的实现方式,发起支付之后不需要等待回调接口被调用就直接返回。从应用场景上来看,同步回调看起来更像模板模式,异步回调看起来更像观察者模式。

2、案例一:回调方式重构JDBC

上面我们使用模板方法模式使用JDBC,我们在此基础上进一步使用组合、回调方式进行重构。

定义JdbcTemplate 工具类:

public class JdbcTemplate {
    private DataSource dataSource;

    public JdbcTemplate(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public final List<?> executeQuery(String sql,RowMapper<?> rowMapper,Object[] values){
        try {
            //1、获取连接
            Connection conn = this.getConnection();
            //2、创建语句集
            PreparedStatement pstm = this.createPrepareStatement(conn,sql);
            //3、执行语句集
            ResultSet rs = this.executeQuery(pstm,values);
            //4、处理结果集
            List<?> result = this.parseResultSet(rs,rowMapper);
            //5、关闭结果集
            rs.close();
            //6、关闭语句集
            pstm.close();
            //7、关闭连接
            conn.close();
            return result;
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;
    }

    private List<?> parseResultSet(ResultSet rs, RowMapper<?> rowMapper) throws Exception {
        List<Object> result = new ArrayList<Object>();
        int rowNum = 0;
        while (rs.next()){
            result.add(rowMapper.mapRow(rs,rowNum++));
        }
        return result;
    }


    private ResultSet executeQuery(PreparedStatement pstm, Object[] values) throws SQLException {
        for (int i = 0; i < values.length; i++) {
            pstm.setObject(i,values[i]);
        }
        return pstm.executeQuery();
    }

    private PreparedStatement createPrepareStatement(Connection conn, String sql) throws SQLException {
        return conn.prepareStatement(sql);
    }

    private Connection getConnection() throws SQLException {
        return this.dataSource.getConnection();
    }
}

public class MemberDao {
    private JdbcTemplate jdbcTemplate = new JdbcTemplate(null);

    public List<?> selectAll(){
        String sql = "select * from t_member";
        return jdbcTemplate.executeQuery(sql, new RowMapper<Member>() {
            public Member mapRow(ResultSet rs, int rowNum) throws Exception {
                Member member = new Member();
                //字段过多,原型模式
                member.setUsername(rs.getString("username"));
                member.setPassword(rs.getString("password"));
                member.setAge(rs.getInt("age"));
                member.setAddr(rs.getString("addr"));
                return member;
            }
        },null);
    }
}

我们发现,取消了原来的继承机制,使用组合+方法回调机制,似乎代码更灵活。

3、案例二:注册监听事件

在客户端开发中,我们经常给控件注册事件监听器,比如下面这段代码,就是在 Android 应用开发中,给 Button 控件的点击事件注册监听器。

Button button = (Button)findViewById(R.id.button);
button.setOnClickListener(new OnClickListener() {
  @Override
  public void onClick(View v) {
    System.out.println("I am clicked.");
  }
});

从代码结构上来看,事件监听器很像回调,即传递一个包含回调函数(onClick())的对象给另一个函数。从应用场景上来看,它又很像观察者模式,即事先注册观察者(OnClickListener),当用户点击按钮的时候,发送点击事件给观察者,并且执行相应的 onClick() 函数。

4、模板方法模式 VS 回调

从应用场景上来看,同步回调跟模板模式几乎一致。它们都是在一个大的算法骨架中,自由替换其中的某个步骤,起到代码复用和扩展的目的。而异步回调跟模板模式有较大差别,更像是观察者模式。

从代码实现上来看,回调和模板模式完全不同。回调基于组合关系来实现,把一个对象传递给另一个对象,是一种对象之间的关系;模板模式基于继承关系来实现,子类重写父类的抽象方法,是一种类之间的关系。

组合优于继承。实际上,这里也不例外。在代码实现上,回调相对于模板模式会更加灵活,主要体现在下面几点:

  • 像 Java 这种只支持单继承的语言,基于模板模式编写的子类,已经继承了一个父类,不再具有继承的能力。
  • 回调可以使用匿名类来创建回调对象,可以不用事先定义类;而模板模式针对不同的实现都要定义不同的子类。
  • 如果某个类中定义了多个模板方法,每个方法都有对应的抽象方法,那即便我们只用到其中的一个模板方法,子类也必须实现所有的抽象方法。而回调就更加灵活,我们只需要往用到的模板方法中注入回调对象即可。

四、源码中的目标方法模式

1、InputStream类

InputStream类就使用了模板方法模式。在InputStream类中定义了多个 read() 方法,如下:

public abstract class InputStream implements Closeable {
	//抽象方法,要求子类必须重写
	public abstract int read() throws IOException;
	
	public int read(byte b[]) throws IOException {
		return read(b, 0, b.length);
	}
	public int read(byte b[], int off, int len) throws IOException {
		if (b == null) {
			throw new NullPointerException();
		} else if (off < 0 || len < 0 || len > b.length - off) {
			throw new IndexOutOfBoundsException();
		} else if (len == 0) {
			return 0;
		}
		int c = read(); //调用了无参的read方法,该方法是每次读取一个字节数据
		if (c == -1) {
			return -1;
		}
		b[off] = (byte)c;
		int i = 1;
		try {
			for (; i < len ; i++) {
				c = read();
				if (c == -1) {
					break;
				}
				b[off + i] = (byte)c;
			}
		} catch (IOException ee) {
		}
		return i;
	}
}

从上面代码可以看到,无参的 read() 方法是抽象方法,要求子类必须实现。而 read(byte b[])方法调用了 read(byte b[], int off, int len) 方法,所以在此处重点看的方法是带三个参数的方法。

在InputStream父类中已经定义好了读取一个字节数组数据的方法是每次读取一个字节,并将其存储到数组的第一个索引位置,读取len个字节数据。具体如何读取一个字节数据,由子类实现。

2、AbstractList

我们看一下AbstractList的部分源码:

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
	public abstract E get(int index);
}

get方法是一个抽象方法,它的逻辑交由子类来进行实现,ArrayList就是AbstractList的子类。

同理,有AbstractList就有AbstractSet和AbstractMap,源码也用到了模板方法模式。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。