0%

十四、模板模式和适配器模式

模板模式

定义

模板模式通常又叫模板方法模式(Template Method Pattern)是指定义一个算法的骨 架,并允许子类为一个或者多个步骤提供实现。模板方法使得子类可以在不改变算法结 构的情况下,重新定义算法的某些步骤,属于行为性设计模式。

模板方法适用于以下应用场景:

  1. 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现。
  2. 各子类中公共的行为被提取出来并集中到一个公共的父类中,从而避免代码重复。

我们还是以课程创建流程为例:发布预习资料–>制作课件 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
    54
    public 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
    28
    public class JavaCourse extends NetworkCourse {
    private boolean needHomeworkFlag;
    public JavaCourse(boolean needHomeworkFlag) {
    this.needHomeworkFlag = needHomeworkFlag;
    }
    @Override
    protected boolean needHomework() {
    return needHomeworkFlag;
    }
    @Override
    protected void checkHomework() {
    System.out.println("检查Java作业");
    }
    @Override
    protected void feedback() {
    System.out.println("Java反馈收集");
    }
    }

    public class PythonCourse extends NetworkCourse {
    /**
    * python没有课后作业
    */
    @Override
    protected void feedback() {
    System.out.println("python反馈收集");
    }
    }

76568891

利用模板模式重构 JDBC 操作业务场景

创建一个模板类JdbcTemplate,封装所有的JDBC操作。以查询为例,每次查询的表不 同,返回的数据结构也就不一样。我们针对不同的数据,都要封装成不同的实体对象。 而每个实体封装的逻辑都是不一样的,但封装前和封装后的处理流程是不变的,因此, 我们可以使用模板方法模式来设计这样的业务场景。

  • 先创建约束 ORM 逻辑的接口 RowMapper:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public 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
    57
    public 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
    @Data
    @Accessors(chain = true)
    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
    17
    public 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执行逻辑,然后把几个方法交给子类定制化完成,源码如下:
78988674

模板模式的优缺点

优点:

  1. 利用模板方法将相同处理逻辑的代码放到抽象父类中,可以提高代码的复用性。
  2. 将不同的代码不同的子类中,通过对子类的扩展增加新的行为,提高代码的扩展性。
  3. 把不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台, 符合开闭原则。

缺点:

  1. 类数目的增加,每一个抽象类都需要一个子类来实现,这样导致类的个数增加。
  2. 类数量的增加,间接地增加了系统实现的复杂度。
  3. 继承关系自身缺点,如果父类添加新的抽象方法,所有子类都要改一遍。

模板方法模式比较简单,只要勤加练习, 多结合业务场景思考问题,就能够把模板方法模式运用好。

适配器模式

适配器模式的应用场景

适配器模式(Adapter Pattern)是指将一个类的接口转换成客户期望的另一个接口,使 原本的接口不兼容的类可以一起工作,属于结构型设计模式。
适配器适用于以下几种业务场景:

  1. 已经存在的类,它的方法和需求不匹配(方法结果相同或相似)的情况。
  2. 适配器模式不是软件设计阶段考虑的设计模式,是随着软件维护,由于不同产品、不 同厂家造成功能类似而接口不相同情况下的解决方案。有点亡羊补牢的感觉。

在中国民用电都是 220V 交流电,但我们手机使用的锂电池使用的 5V 直流电。因此,我 们给手机充电时就需要使用电源适配器来进行转换。下面我们有代码来还原这个生活场 景,创建 AC220 类,表示 220V 交流电:

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
/**
* 现有支持AC220的接口
* 被适配接口
* @author 陈添明
* @date 2019/4/14
*/
public interface IAC220 {
int outputAC220V();
}

public class AC220 implements IAC220 {
@Override
public int outputAC220V(){
int output = 220;
System.out.println("输出交流电" + output + "V");
return output;
}
}

/**
* 目标接口
* 新需求要求支持DC5V的接口
* 但是DC5V可以通过AC220V转换得到
* @author 陈添明
* @date 2019/4/14
*/
public interface IDC5 {
int outputDC5V();
}

/**
* 适配器实现目标接口,同时持有被适配对象
* @author 陈添明
* @date 2019/4/14
*/
public class DC5VAdapter implements IDC5 {
IAC220 iac220;
public DC5VAdapter(IAC220 iac220) {
this.iac220 = iac220;
}
@Override
public int outputDC5V() {
int i = iac220.outputAC220V();
int r = i / 44;
System.out.println("输出直流" + r + "V");
return r;
}
}

59584429

重构第三登录自由适配的业务场景

下面我们来一个实际的业务场景,利用适配模式来解决实际问题。年纪稍微大一点的小 伙伴一定经历过这样一个过程。我们很早以前开发的老系统应该都有登录接口,但是随 着业务的发展和社会的进步,单纯地依赖用户名密码登录显然不能满足用户需求了。现 在,我们大部分系统都已经支持多种登录方式,如 QQ 登录、微信登录、手机登录、微 博登录等等,同时保留用户名密码的登录方式。虽然登录形式丰富了,但是登录后的处理逻辑可以不必改,同样是将登录状态保存到 session,遵循开闭原则。其他新增的登录方式,可以复用之前登录的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class SiginService {
/**
* 注册方法
*
* @param username * @param password * @return
*/
public ResultMsg regist(String username, String password) {
return new ResultMsg(200, "注册成功", new Member());
}
/**
* 登录的方法
*
* @param username * @param password * @return
*/
public ResultMsg login(String username, String password) {
return null;
}
}

为了遵循开闭原则,老系统的代码我们不会去修改。

  • 现在需要支持第三方登录 – 新的目标接口

    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
    */
    @Override
    public ResultMsg loginForQQ(String id) {
    //1、openId 是全局唯一,我们可以把它当做是一个用户名(加长)
    // 2、密码默认为 QQ_EMPTY
    // 3、注册(在原有系统里面创建一个用户)
    //4、调用原来的登录方法
    ResultMsg resultMsg = siginService.login("12345", "1111");
    return resultMsg;
    }
    /**
    * 微信登录
    *
    * @param id
    */
    @Override
    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
    83
    public 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
    */
    @Override
    public boolean support(Object adapter) {
    return adapter instanceof LoginForQQAdapter;
    }
    /**
    * 登录接口
    * @param params
    * @return
    */
    @Override
    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
    */
    @Override
    public ResultMsg loginForQQ(String id) {
    return processLogin(LoginForQQAdapter.class, id);
    }
    /**
    * 微信登录
    *
    * @param id
    */
    @Override
    public ResultMsg loginForWechat(String id) {
    return processLogin(LoginForWechatAdapter.class, id);
    }
    @SneakyThrows
    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;
    }
    }

69005337

适配器模式在源码中的体现

Spring中适配器模式也应用得非常广泛,例如:SpringAOP中的AdvisorAdapter类, 它有三个实现类 MethodBeforeAdviceAdapterAfterReturningAdviceAdapterThrowsAdviceAdapter,先来看顶层接口AdvisorAdapter的源代码:
62911191

再看MethodBeforeAdviceAdapter类:
62951178

Spring 会根据不同的AOP配置来确定使用对应的Advice,跟策略模式不同的一个方法可以同时拥有多个Advice

下面再来看一个 SpringMVC中的HandlerAdapter类,它也有多个子类,类图如下:
63011975

适配器模式的优缺点

优点:

  1. 能提高类的透明性和复用,现有的类复用但不需要改变。
  2. 目标类和适配器类解耦,提高程序的扩展性。
  3. 在很多业务场景中符合开闭原则。

缺点:

  1. 适配器编写过程需要全面考虑,可能会增加系统的复杂性。
  2. 增加代码阅读难度,降低代码可读性,过多使用适配器会使系统代码变得凌乱。

源码:https://github.com/chentianming11/design-pattern
template和adapter包!