行为型-模板方法(Template Method)

4/4/2022 设计模式

摘要

JDK:1.8.0_202

# 一:场景问题

# 1.1 登录控制

现在有这么一个基于Web的企业级应用系统,需要实现这两种控制权限,直接使用不同的登录页面来区分它们。他们分别是普通用户登录前台控制,工作人员登录后台控制。

说明:普通用户和工作人员在数据库里面是存储在不同表里面的;当然也是不同的模块来维护普通用户的数据和工作人员的数据;后台登录密码加密

# 1.2 不用模式的解决方案

由于普通用户登录和工作人员登录是不同的模块,有不同的页面,不同的逻辑处理,不同的数据存储,因此,在实现上完全当成两个独立的小模块去完成了。这里把它们的逻辑处理部分分别实现出来。

1. 普通用户登录的逻辑处理部分:

/**
 * 普通用户登录控制的逻辑处理
 */
public class NormalLogin {

    /**
     * 判断登录数据是否正确,也就是是否能登录成功
     *
     * @param lm 封装登录数据的Model
     * @return true表示登录成功,false表示登录失败
     */
    public boolean login(LoginModel lm) {
        // 1:从数据库获取登录人员的信息,就是根据用户编号去获取人员的数据
        UserModel um = this.findUserByUserId(lm.getUserId());
        // 2:判断从前台传递过来的登录数据,和数据库中已有的数据是否匹配
        // 先判断用户是否存在,如果um为null,说明用户肯定不存在
        // 但是不为null,用户不一定存在,因为数据层可能返回new UserModel();
        // 因此还需要做进一步的判断
        if (um != null) {
            //如果用户存在,检查用户编号和密码是否匹配
            return um.getUserId().equals(lm.getUserId()) && um.getPwd().equals(lm.getPwd());
        }
        return false;
    }

    /**
     * 根据用户编号获取用户的详细信息
     *
     * @param userId 用户编号
     * @return 对应的用户的详细信息
     */
    private UserModel findUserByUserId(String userId) {
        // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象
        UserModel um = new UserModel();
        um.setUserId(userId);
        um.setName("test");
        um.setPwd("test");
        um.setUuid("User0001");
        return um;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

对应的LoginModel:

/**
 * 描述登录人员登录时填写的信息的数据模型
 */
public class LoginModel {
    private String userId, pwd;

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

对应的UserModel:

/**
 * 描述用户信息的数据模型
 */
public class UserModel {
    private String uuid, userId, pwd, name;

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

2. 工作人员登录的逻辑处理部分:

/**
 * 工作人员登录控制的逻辑处理
 */
public class WorkerLogin {
    /**
     * 判断登录数据是否正确,也就是是否能登录成功
     *
     * @param lm 封装登录数据的Model
     * @return true表示登录成功,false表示登录失败
     */
    public boolean login(LoginModel lm) {
        // 1:根据工作人员编号去获取工作人员的数据
        WorkerModel wm = findWorkerByWorkerId(lm.getWorkerId());
        // 2:判断从前台传递过来的用户名和加密后的密码数据,
        // 和数据库中已有的数据是否匹配
        // 先判断工作人员是否存在,如果wm为null,说明工作人员肯定不存在
        // 但是不为null,工作人员不一定存在,
        // 因为数据层可能返回new WorkerModel();因此还需要做进一步的判断
        if (wm != null) {
            //3:把从前台传来的密码数据,使用相应的加密算法进行加密运算
            String encryptPwd = this.encryptPwd(lm.getPwd());
            //如果工作人员存在,检查工作人员编号和密码是否匹配
            return wm.getWorkerId().equals(lm.getWorkerId()) && wm.getPwd().equals(encryptPwd);
        }
        return false;
    }

    /**
     * 对密码数据进行加密
     *
     * @param pwd 密码数据
     * @return 加密后的密码数据
     */
    private String encryptPwd(String pwd) {
        // 这里对密码进行加密,省略了
        return pwd;
    }

    /**
     * 根据工作人员编号获取工作人员的详细信息
     *
     * @param workerId 工作人员编号
     * @return 对应的工作人员的详细信息
     */
    private WorkerModel findWorkerByWorkerId(String workerId) {
        // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象
        WorkerModel wm = new WorkerModel();
        wm.setWorkerId(workerId);
        wm.setName("Worker1");
        wm.setPwd("worker1");
        wm.setUuid("Worker0001");
        return wm;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

对应的LoginModel:

/**
 * 描述登录人员登录时填写的信息的数据模型
 */
public class LoginModel {
    private String workerId, pwd;

    public String getWorkerId() {
        return workerId;
    }

    public void setWorkerId(String workerId) {
        this.workerId = workerId;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

对应的WorkerModel:

/**
 * 描述工作人员信息的数据模型
 */
public class WorkerModel {
    private String uuid, workerId, pwd, name;

    public String getUuid() {
        return uuid;
    }

    public void setUuid(String uuid) {
        this.uuid = uuid;
    }

    public String getWorkerId() {
        return workerId;
    }

    public void setWorkerId(String workerId) {
        this.workerId = workerId;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

# 1.3 有何问题

看了上面的实现示例,是不是很简单。但是,仔细看看,总会觉得有点问题,两种登录的实现太相似了,现在是完全分开,当作两个独立的模块来实现的,如果今后要扩展功能,比如要添加 "控制同一个编号同时只能登录一次" 的功能,那么两个模块都需要修改,是很麻烦的。而且,现在的实现中,也有很多相似的地方,显得很重复。另外,具体的实现和判断的步骤混合在一起,不利于今后变换功能,比如要变换加密算法等。

总之,上面的实现,有两个很明显的问题:一是重复或相似代码太多;二是扩展起来很不方便

那么该怎么解决呢?该如何实现才能让系统既灵活又能简洁的实现需求功能呢?

# 二:解决方案

用来解决上述问题的一个合理的解决方案就是模板方法模式。

模板方法:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

# 2.1 解决思路

仔细分析上面的问题,重复或相似代码太多、扩展不方便,出现这些问题的原因在哪里?**主要就是两个实现是完全分开、相互独立的,没有从整体上进行控制。如果把两个模块合起来看,就会发现,那些重复或相似的代码就应该被抽取出来,做成公共的功能,而不同的登录控制就可以去扩展这些公共的功能。这样一来,扩展的时候,如果出现有相同的功能,那就直接扩展公共功能就可以了。

使用模板方法模式,就可以很好的来实现上面的思路。分析上面两个登录控制模块,会发现它们在实现上,有着大致相同的步骤,只是在每步具体的实现上,略微有些不同,因此,可以把这些运算步骤看作是算法的骨架,把具体的不同的步骤实现,延迟到子类去实现,这样就可以通过子类来提供不同的功能实现了。

经过分析总结,登录控制大致的逻辑判断步骤如下:

根据登录人员的编号去获取相应的数据;

获取对登录人员填写的密码数据进行加密后的数据,如果不需要加密,那就是直接返回登录人员填写的密码数据;

判断登录人员填写的数据和从数据库中获取的数据是否匹配;

在这三个步骤里面,第一个和第三个步骤是必不可少的,而第二个步骤是可选的。那么就可以定义一个父类,在里面定义一个方法来定义这个算法骨架,这个方法就是模板方法,然后把父类无法确定的实现,延迟到具体的子类来实现就可以了。

通过这样的方式,如果要修改加密的算法,那就在模板的子类里面重新覆盖实现加密的方法就好了,完全不需要去改变父类的算法结构,就可以重新定义这些特定的步骤。

# 2.2 模式结构和说明

模板方法模式的结构:

classDiagram AbstractClass <|-- ConcreteClass class AbstractClass{ <<abstract>> +doPrimitiveOperation1()* void +doPrimitiveOperation2()* void +templateMethod() void } class ConcreteClass{ +doPrimitiveOperation1() void +doPrimitiveOperation2() void }
  • AbstractClass:抽象类。用来定义算法骨架和原语操作,具体的子类通过重定义这些原语操作来实现一个算法的各个步骤。在这个类里面,还可以提供算法中通用的实现。
  • ConcreteClass:具体实现类。用来实现算法骨架中的某些步骤,完成跟特定子类相关的功能。

# 2.3 示例代码

抽象类:

/**
 * 定义模板方法、原语操作等的抽象类
 */
public abstract class AbstractClass {

    /**
     * 原语操作1,所谓原语操作就是抽象的操作,必须要由子类提供实现
     */
    public abstract void doPrimitiveOperation1();

    /**
     * 原语操作2
     */
    public abstract void doPrimitiveOperation2();

    /**
     * 模板方法,定义算法骨架
     */
    public final void templateMethod() {
        doPrimitiveOperation1();
        doPrimitiveOperation2();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

具体实现类:

/**
 * 具体实现类,实现原语操作
 */
public class ConcreteClass extends AbstractClass {
    public void doPrimitiveOperation1() {
        //具体的实现
    }

    public void doPrimitiveOperation2() {
        //具体的实现
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 2.4 重写示例

要使用模板方法模式来实现前面的示例,按照模板方法模式的定义和结构,需要定义出一个抽象的父类,在这个父类里面定义模板方法,这个模板方法应该实现进行登录控制的整体的算法步骤。当然公共的功能,就放到这个父类去实现,而这个父类无法决定的功能,就延迟到子类去实现。

这样一来,两种登录控制就做为这个父类的子类,分别实现自己需要的功能。此时系统的结构如图所示:

classDiagram LoginTemplate <|-- WorkerLogin LoginTemplate <|-- NormalLogin class LoginTemplate{ <<abstract>> +login(LoginModel) bool +findLoginUser(String)* LoginModel +encryptPwd(String)* String +match(LoginModel, LoginModel) boolean } class WorkerLogin{ +findLoginUser(String) LoginModel +encryptPwd(String) String } class NormalLogin{ +findLoginUser(String) LoginModel }

为了把原来的两种登录控制统一起来,首先需要把封装登录控制所需要的数据模型统一起来,不再区分是用户编号还是工作人员编号,而是统一称为登录人员的编号,还有把其它用不上的数据去掉,这样就直接使用一个数据模型就可以了。当然,如果各个子类实现需要其它的数据,还可以自行扩展。

/**
 * 封装进行登录控制所需要的数据
 */
public class LoginModel {

    /**
     * 登录人员的编号,通用的,可能是用户编号,也可能是工作人员编号
     */
    private String loginId;
    /**
     * 登录的密码
     */
    private String pwd;

    public String getLoginId() {
        return loginId;
    }

    public void setLoginId(String loginId) {
        this.loginId = loginId;
    }

    public String getPwd() {
        return pwd;
    }

    public void setPwd(String pwd) {
        this.pwd = pwd;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

定义公共的登录控制算法:

/**
 * 登录控制的模板
 */
public abstract class LoginTemplate {
    /**
     * 判断登录数据是否正确,也就是是否能登录成功
     *
     * @param lm 封装登录数据的Model
     * @return true表示登录成功,false表示登录失败
     */
    public final boolean login(LoginModel lm) {
        // 1:根据登录人员的编号去获取相应的数据
        LoginModel dbLm = this.findLoginUser(lm.getLoginId());
        if (dbLm != null) {
            // 2:对密码进行加密
            String encryptPwd = this.encryptPwd(lm.getPwd());
            // 把加密后的密码设置回到登录数据模型里面
            lm.setPwd(encryptPwd);
            // 3:判断是否匹配
            return this.match(lm, dbLm);
        }
        return false;
    }

    /**
     * 根据登录编号来查找和获取存储中相应的数据
     *
     * @param loginId 登录编号
     * @return 登录编号在存储中相对应的数据
     */
    public abstract LoginModel findLoginUser(String loginId);

    /**
     * 对密码数据进行加密
     *
     * @param pwd 密码数据
     * @return 加密后的密码数据
     */
    public String encryptPwd(String pwd) {
        return pwd;
    }

    /**
     * 判断用户填写的登录数据和存储中对应的数据是否匹配得上
     *
     * @param lm   用户填写的登录数据
     * @param dbLm 在存储中对应的数据
     * @return true表示匹配成功,false表示匹配失败
     */
    public boolean match(LoginModel lm, LoginModel dbLm) {
        return lm.getLoginId().equals(dbLm.getLoginId()) && lm.getPwd().equals(dbLm.getPwd());
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

实现新的普通用户登录控制的逻辑处理:

/**
 * 普通用户登录控制的逻辑处理
 */
public class NormalLogin extends LoginTemplate {
    @Override
    public LoginModel findLoginUser(String loginId) {
        // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象
        LoginModel lm = new LoginModel();
        lm.setLoginId(loginId);
        lm.setPwd("testpwd");
        return lm;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

实现新的工作人员登录控制的逻辑处理:

/**
 * 工作人员登录控制的逻辑处理
 */
public class WorkerLogin extends LoginTemplate {

    @Override
    public LoginModel findLoginUser(String loginId) {
        // 这里省略具体的处理,仅做示意,返回一个有默认数据的对象
        LoginModel lm = new LoginModel();
        lm.setLoginId(loginId);
        lm.setPwd("workerpwd");
        return lm;
    }

    @Override
    public String encryptPwd(String pwd) {
        // 覆盖父类的方法,提供真正的加密实现
        // 这里对密码进行加密,比如使用:MD5、3DES等等,省略了
        System.out.println("使用MD5进行密码加密");
        return pwd;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

客户端:

public class Client {
    public static void main(String[] args) {
        // 准备登录人的信息
        LoginModel lm = new LoginModel();
        lm.setLoginId("admin");
        lm.setPwd("workerpwd");

        // 准备用来进行判断的对象
        LoginTemplate lt = new WorkerLogin();
        LoginTemplate lt2 = new NormalLogin();

        // 进行登录测试
        boolean flag = lt.login(lm);
        System.out.println("可以登录工作平台=" + flag);

        boolean flag2 = lt2.login(lm);
        System.out.println("可以进行普通人员登录=" + flag2);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

运行结果:

# 三:模式讲解

# 3.1 认识模板方法模式

1. 模式的功能

模板方法的功能在于固定算法骨架,而让具体算法实现可扩展。

这在实际应用中非常广泛,尤其是在设计框架级功能的时候非常有用。**框架定义好了算法的步骤,在合适的点让开发人员进行扩展,实现具体的算法。**比如在DAO实现中,设计通用的增删改查功能,这个后面会给大家示例。

模板方法还额外提供了一个好处,就是可以控制子类的扩展。因为在父类里面定义好了算法的步骤,只是在某几个固定的点才会调用到被子类实现的方法,因此也就只允许在这几个点来扩展功能,这些个可以被子类覆盖以扩展功能的方法通常被称为 "钩子" 方法

2. 为何不是接口

有的朋友可能会问一个问题,不是说在Java中应该尽量面向接口编程吗,为何模板方法的模板是采用的抽象方法呢?

要回答这个问题,要首先搞清楚抽象类和接口的关系:

接口是一种特殊的抽象类,所有接口中的属性自动是常量,也就是public final static的,而所有接口中的方法必须是抽象的;

抽象类,简单点说是用abstract修饰的类。这里要特别注意的是抽象类和抽象方法的关系,记住两句话:抽象类不一定包含抽象方法;有抽象方法的类一定是抽象类

抽象类和接口相比较,最大的特点就在于抽象类里面是可以有具体的实现方法的,而接口中所有的方法都是没有具体的实现的

因此,虽然Java编程中倡导大家“面向接口编程”,并不是说就不再使用抽象类了,那么什么时候使用抽象类呢?

通常在**"既要约束子类的行为,又要为子类提供公共功能"**的时候使用抽象类;

按照这个原则来思考模板方法模式的实现,模板方法模式需要固定定义算法的骨架,这个骨架应该只有一份,算是一个公共的行为,但是里面具体的步骤的实现又可能是各不相同的,恰好符合选择抽象类的原则。

把模板实现成为抽象类,为所有的子类提供了公共的功能,就是定义了具体的算法骨架;同时在模板里面把需要由子类扩展的具体步骤的算法定义成为抽象方法,要求子类去实现这些方法,这就约束了子类的行为。

因此综合考虑,用抽象类来实现模板是一个很好的选择。

3. 变与不变

**程序设计的一个很重要的思考点就是“变与不变”,**也就是分析程序中哪些功能是可变的,哪些功能是不变的,然后把不变的部分抽象出来,进行公共的实现,把变化的部分分离出去,用接口来封装隔离,或者是用抽象类来约束子类行为。

模板方法模式很好的体现了这一点。模板类实现的就是不变的方法和算法的骨架,而需要变化的地方,都通过抽象方法,把具体实现延迟到子类去了,而且还通过父类的定义来约束了子类的行为,从而使系统能有更好的复用性和扩展性。

4. 好莱坞法则

什么是好莱坞法则呢?简单点说,就是 "不要找我们,我们会联系你"

模板方法模式很好的体现了这一点,做为父类的模板会在需要的时候,调用子类相应的方法,也就是由父类来找子类,而不是让子类来找父类

这其实也是一种反向的控制结构,按照通常的思路,是子类找父类才对,也就是应该是子类来调用父类的方法,因为父类根本就不知道子类,而子类是知道父类的,但是在模板方法模式里面,是父类来找子类,所以是一种反向的控制结构。

那么,在Java里面能实现这样功能的理论依据在哪里呢?

理论依据就在于 Java的动态绑定采用的是 "后期绑定" 技术,对于出现子类覆盖父类方法的情况,在编译时是看数据类型,运行时看实际的对象类型(new操作符后跟的构造方法是哪个类的),一句话:new谁就调用谁的方法。

因此在使用模板方法模式的时候,虽然用的数据类型是模板类型,但是在创建类实例的时候是创建的具体的子类的实例,因此调用的时候,会被动态绑定到子类的方法上去,从而实现反向控制。其实在写父类的时候,它调用的方法是父类自己的抽象方法,只是在运行的时候被动态绑定到了子类的方法上。

# 3.2 优缺点

1. 实现代码复用

模板方法模式是一种实现代码复用的很好的手段。通过把子类的公共功能提炼和抽取,把公共部分放到模板里面去实现。

2. 算法骨架不容易升级

模板方法模式最基本的功能就是通过模板的制定,把算法骨架完全固定下来。事实上模板和子类是非常耦合的,如果要对模板中的算法骨架进行变更,可能就会要求所有相关的子类进行相应的变化。所以抽取算法骨架的时候要特别小心,尽量确保是不会变化的部分才放到模板中。

# 3.3 思考模板方法模式

1. 模板方法模式的本质

模板方法模式的本质:固定算法骨架

模板方法模式主要是通过制定模板,把算法步骤固定下来,至于谁来实现,模板可以自己提供实现,也可以由子类去实现,还可以通过回调机制让其它类来实现。

通过固定算法骨架,来约束子类的行为,并在特定的扩展点,来让子类进行功能扩展,从而让程序既有很好的复用性,又有较好的扩展性。

2. 对设计原则的体现

模板方法很好的体现了开闭原则和里氏替换原则。

首先从设计上,先分离变与不变,然后把不变的部分抽取出来,定义到父类里面,比如算法骨架,比如一些公共的、固定的实现等等。这些不变的部分被封闭起来,尽量不去修改它了,要扩展新的功能,那就使用子类来扩展,通过子类来实现可变化的步骤,对于这种新增功能的做法是开放的。

其次,能够实现统一的算法骨架,通过切换不同的具体实现来切换不同的功能,一个根本原因就是里氏替换原则,遵循这个原则,保证所有的子类实现的是同一个算法模板,并能在使用模板的地方,根据需要,切换不同的具体实现。

3. 何时选用模板方法模式

建议在如下情况中,选用模板方法模式:

需要固定定义算法骨架,实现一个算法的不变的部分,并把可变的行为留给子类来实现的情况;

各个子类中具有公共行为,应该抽取出来,集中在一个公共类中去实现,从而避免代码重复;

需要控制子类扩展的情况。模板方法模式会在特定的点来调用子类的方法,这样只允许在这些点进行扩展;

# 3.4 相关模式

1. 模板方法模式和工厂方法模式

这两个模式可以配合使用。

模板方法模式可以通过工厂方法来获取需要调用的对象。

2. 模板方法模式和策略模式

这两个模式的功能有些相似,但是是有区别的。

从表面上看,两个模式都能实现算法的封装,但是模板方法封装的是算法的骨架,这个算法骨架是不变的,变化的是算法中某些步骤的具体实现;而策略模式是把某个步骤的具体实现算法封装起来,所有封装的算法对象是等价的,可以相互替换。

因此,可以在模板方法中使用策略模式,就是把那些变化的算法步骤通过使用策略模式来实现,但是具体选取哪个策略还是要由外部来确定,而整体的算法步骤,也就是算法骨架就由模板方法来定义了

# 四:JDK

# 五:参考文献

最后更新: 4/5/2022, 1:08:48 AM