模板模式
定义
模板模式通常又叫模板方法模式(Template Method Pattern)是指定义一个算法的骨 架,并允许子类为一个或者多个步骤提供实现。模板方法使得子类可以在不改变算法结 构的情况下,重新定义算法的某些步骤,属于行为性设计模式。
模板方法适用于以下应用场景:
- 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
- 各子类中公共的行为被提取出来并集中到一个公共的父类中,从而避免代码重复。
我们还是以课程创建流程为例:发布预习资料–>制作课件 PPT–>在线直播 –>提交课堂笔记–>提交源码–>布置作业–>检查作业。
首先我们来创建
NetworkCourse
抽象类: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
54public abstract class NetworkCourse {
/**
* 创建课程 -- 定义算法骨架
*/
public final void createCourse() {
//1、发布预习资料
this.postPreResource();
//2、制作 PPT 课件
this.createPPT();
//3、在线直播
this.liveVideo();
//4、提交课件、课堂笔记
this.postNote();
//5、提交源码
this.postSource();
//6、布置作业,有些课是没有作业,有些课是有作业的
// 如果有作业的话,检查作业,如果没作业,完成了
if (needHomework()) {
checkHomework();
}
// 7. 收集课后反馈
this.feedback();
}
final void postPreResource() {
System.out.println("发布预习资料");
}
final void createPPT() {
System.out.println("制作 PPT 课件");
}
final void liveVideo() {
System.out.println("在线直播");
}
final void postNote() {
System.out.println("提交课件、课堂笔记");
}
final void postSource() {
System.out.println("提交源码");
}
/**
* 钩子方法:实现流程的微调
* 是否有作业
* @return
*/
protected boolean needHomework() {
return true;
}
/**
* 默认空实现
*/
protected void checkHomework() {}
protected abstract void feedback();
}设计钩子方法的主要目的是用来干预执行流程,使得我们控制行为流程更加灵活,更符合实际业 务的需求。钩子方法的返回值一般为适合条件分支语句的返回值(如 boolean、int 等)。
具体的course类
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
28public class JavaCourse extends NetworkCourse {
private boolean needHomeworkFlag;
public JavaCourse(boolean needHomeworkFlag) {
this.needHomeworkFlag = needHomeworkFlag;
}
protected boolean needHomework() {
return needHomeworkFlag;
}
protected void checkHomework() {
System.out.println("检查Java作业");
}
protected void feedback() {
System.out.println("Java反馈收集");
}
}
public class PythonCourse extends NetworkCourse {
/**
* python没有课后作业
*/
protected void feedback() {
System.out.println("python反馈收集");
}
}
利用模板模式重构 JDBC 操作业务场景
创建一个模板类JdbcTemplate
,封装所有的JDBC
操作。以查询为例,每次查询的表不 同,返回的数据结构也就不一样。我们针对不同的数据,都要封装成不同的实体对象。 而每个实体封装的逻辑都是不一样的,但封装前和封装后的处理流程是不变的,因此, 我们可以使用模板方法模式来设计这样的业务场景。
先创建约束 ORM 逻辑的接口 RowMapper:
1
2
3
4
5
6
7
8
9
10public interface RowMapper<T> {
/**
* 行映射处理
* @param rs
* @param rowNum
* @return
* @throws Exception
*/
T mapRow(ResultSet rs, int rowNum) throws Exception;
}再创建封装了所有处理流程的抽象类 JdbcTemplate:
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
54
55
56
57public abstract class JdbcTemplate {
private DataSource dataSource;
public JdbcTemplate(DataSource dataSource) {
this.dataSource = dataSource;
}
public <T> List<T> executeQuery(String sql, RowMapper<T> rowMapper, Object[] params) {
try {
//1、获取连接
Connection conn = this.getConnection();
//2、创建语句集
PreparedStatement pstm = this.createPrepareStatement(conn, sql);
//3、执行语句集
ResultSet rs = this.executeQuery(pstm, params);
//4、处理结果集
List<T> result = this.paresResultSet(rs, rowMapper);
//5、关闭结果集
this.closeResultSet(rs);
//6、关闭语句集
this.closeStatement(pstm);
//7、关闭连接
this.closeConnection(conn);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private final void closeConnection(Connection conn) throws SQLException {
conn.close();
}
private final void closeStatement(PreparedStatement pstm) throws SQLException {
pstm.close();
}
private final void closeResultSet(ResultSet rs) throws SQLException {
rs.close();
}
protected <T> List<T> paresResultSet(ResultSet rs, RowMapper<T> rowMapper) throws Exception {
List<T> result = new ArrayList<>();
int rowNum = 1;
while (rs.next()) {
result.add(rowMapper.mapRow(rs, rowNum++));
}
return result;
}
private final ResultSet executeQuery(PreparedStatement pstm, Object[] params) throws SQLException {
for (int i = 0; i < params.length; i++) {
pstm.setObject(i, params[i]);
}
return pstm.executeQuery();
}
private final PreparedStatement createPrepareStatement(Connection conn, String sql) throws SQLException {
return conn.prepareStatement(sql);
}
private final Connection getConnection() throws SQLException {
return this.dataSource.getConnection();
}
}创建实体对象 Member 类:
1
2
3
4
5
6
7
8
true) (chain =
public class Member {
private String username;
private String password;
private String nickName;
private int age;
}创建数据库操作类 MemberDao:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class MemberDao extends JdbcTemplate {
public MemberDao(DataSource dataSource) {
super(dataSource);
}
public List<Member> selectAll() {
String sql = "select * from t_member";
List<Member> members = executeQuery(sql, (rs, rowNum) -> {
Member member = new Member()
.setUsername(rs.getString("username"))
.setAge(rs.getInt("age"))
.setNickName(rs.getString("nick_name"))
.setPassword(rs.getString("password"));
return member;
}, null);
return members;
}
}
模板模式在源码中的体现
有一个每天都在用的HttpServlet
,有三个方法service()
和doGet()
、doPost()
方法,都是模板方法的抽象实现。
在MyBatis
框架也有一些经典的应用,我们来一下BaseExecutor
类,它是一个基础的SQL
执行类,实现了大部分的SQL
执行逻辑,然后把几个方法交给子类定制化完成,源码如下:
模板模式的优缺点
优点:
- 利用模板方法将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性。
- 将不同的代码不同的子类中,通过对子类的扩展增加新的行为,提高代码的扩展性。
- 把不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台, 符合开闭原则。
缺点:
- 类数目的增加,每一个抽象类都需要一个子类来实现,这样导致类的个数增加。
- 类数量的增加,间接地增加了系统实现的复杂度。
- 继承关系自身缺点,如果父类添加新的抽象方法,所有子类都要改一遍。
模板方法模式比较简单,只要勤加练习, 多结合业务场景思考问题,就能够把模板方法模式运用好。
适配器模式
适配器模式的应用场景
适配器模式(Adapter Pattern)是指将一个类的接口转换成客户期望的另一个接口,使 原本的接口不兼容的类可以一起工作,属于结构型设计模式。
适配器适用于以下几种业务场景:
- 已经存在的类,它的方法和需求不匹配(方法结果相同或相似)的情况。
- 适配器模式不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品、不 同厂家造成功能类似而接口不相同情况下的解决方案。有点亡羊补牢的感觉。
在中国民用电都是 220V 交流电,但我们手机使用的锂电池使用的 5V 直流电。因此,我 们给手机充电时就需要使用电源适配器来进行转换。下面我们有代码来还原这个生活场 景,创建 AC220 类,表示 220V 交流电:
1 | /** |
重构第三登录自由适配的业务场景
下面我们来一个实际的业务场景,利用适配模式来解决实际问题。年纪稍微大一点的小 伙伴一定经历过这样一个过程。我们很早以前开发的老系统应该都有登录接口,但是随 着业务的发展和社会的进步,单纯地依赖用户名密码登录显然不能满足用户需求了。现 在,我们大部分系统都已经支持多种登录方式,如 QQ 登录、微信登录、手机登录、微 博登录等等,同时保留用户名密码的登录方式。虽然登录形式丰富了,但是登录后的处理逻辑可以不必改,同样是将登录状态保存到 session,遵循开闭原则。其他新增的登录方式,可以复用之前登录的逻辑
1 | public class SiginService { |
为了遵循开闭原则,老系统的代码我们不会去修改。
现在需要支持第三方登录 – 新的目标接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* 第三方登录接口 -- 目标接口
*
* @author 陈添明
* @date 2019/4/14
*/
public interface IPassportForThird {
/**
* QQ登录
* @param id
* @return
*/
ResultMsg loginForQQ(String id);
/**
* 微信登录
*/
ResultMsg loginForWechat(String id);
}第三方登录适配器 - 实现兼容 PassportForThirdAdapter
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/**
* 第三方登录适配器
* 实现目标接口,持有被适配对象的引用
*
* @author 陈添明
* @date 2019/4/14
*/
public class PassportForThirdAdapter implements IPassportForThird {
SiginService siginService;
public PassportForThirdAdapter(SiginService siginService) {
this.siginService = siginService;
}
/**
* QQ登录
*
* @param id
* @return
*/
public ResultMsg loginForQQ(String id) {
//1、openId 是全局唯一,我们可以把它当做是一个用户名(加长)
// 2、密码默认为 QQ_EMPTY
// 3、注册(在原有系统里面创建一个用户)
//4、调用原来的登录方法
ResultMsg resultMsg = siginService.login("12345", "1111");
return resultMsg;
}
/**
* 微信登录
*
* @param id
*/
public ResultMsg loginForWechat(String id) {
/**
* 一堆微信登录的逻辑
*/
ResultMsg resultMsg = siginService.login("12345", "1111");
return resultMsg;
}
}每种登录方式都有各自的逻辑,考虑单一性原则。将不同的适配逻辑分离,同时使用策略模式选择不同的适配器执行处理。
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83public abstract class LoginAdapter {
protected SiginService siginService;
public LoginAdapter(SiginService siginService) {
this.siginService = siginService;
}
/**
* 兼容校验
*
* @param adapter
* @return
*/
public abstract boolean support(Object adapter);
/**
* 登录接口
* @param params
* @return
*/
public abstract ResultMsg login(Object... params);
}
public class LoginForQQAdapter extends LoginAdapter {
public LoginForQQAdapter(SiginService siginService) {
super(siginService);
}
/**
* 兼容校验
* @param adapter
* @return
*/
public boolean support(Object adapter) {
return adapter instanceof LoginForQQAdapter;
}
/**
* 登录接口
* @param params
* @return
*/
public ResultMsg login(Object... params) {
//1、openId 是全局唯一,我们可以把它当做是一个用户名(加长)
// 2、密码默认为 QQ_EMPTY
// 3、注册(在原有系统里面创建一个用户)
//4、调用原来的登录方法
ResultMsg resultMsg = siginService.login("12345", "1111");
return resultMsg;
}
}
public class PassportForThirdService implements IPassportForThird {
private SiginService siginService;
public PassportForThirdService(SiginService siginService) {
this.siginService = siginService;
}
/**
* QQ登录
*
* @param id
* @return
*/
public ResultMsg loginForQQ(String id) {
return processLogin(LoginForQQAdapter.class, id);
}
/**
* 微信登录
*
* @param id
*/
public ResultMsg loginForWechat(String id) {
return processLogin(LoginForWechatAdapter.class, id);
}
private ResultMsg processLogin(Class<? extends LoginAdapter> clz, Object... params) {
Constructor<? extends LoginAdapter> constructor = clz.getConstructor(SiginService.class);
LoginAdapter loginAdapter = constructor.newInstance(siginService);
if (loginAdapter.support(loginAdapter)) {
return loginAdapter.login(params);
}
return null;
}
}
适配器模式在源码中的体现
Spring
中适配器模式也应用得非常广泛,例如:SpringAOP
中的AdvisorAdapter
类, 它有三个实现类 MethodBeforeAdviceAdapter
、AfterReturningAdviceAdapter
和 ThrowsAdviceAdapter
,先来看顶层接口AdvisorAdapter
的源代码:
再看MethodBeforeAdviceAdapter
类:
Spring 会根据不同的AOP
配置来确定使用对应的Advice
,跟策略模式不同的一个方法可以同时拥有多个Advice
。
下面再来看一个 SpringMVC
中的HandlerAdapter
类,它也有多个子类,类图如下:
适配器模式的优缺点
优点:
- 能提高类的透明性和复用,现有的类复用但不需要改变。
- 目标类和适配器类解耦,提高程序的扩展性。
- 在很多业务场景中符合开闭原则。
缺点:
- 适配器编写过程需要全面考虑,可能会增加系统的复杂性。
- 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。
源码:https://github.com/chentianming11/design-pattern
template和adapter包!