天天看点

设计模式 - 模板方法(Template Method)

概述

       模板方法是在面向对象编程中经常会使用到的一个方法,它巧妙的使用了类多态这一特性。我第一次在GoF的书中看到这个模式的时候有一种很熟悉的感觉,因为在我过去编写的代码中已经使用过类似的方式来组织代码,下面通过一个系统演进的例子来介绍模板方法。

示例代码

       假设有一套校园信息管理系统,其中一个功能就是把学生信息录入到数据库里,为了更好的说明问题,在这里我们抛开具体的业务细节,仅考虑持久层的实现,那么简化后的代码是这样:

/**
 * 实体基类,对应数据库的一行记录.
 */
public abstract class EntityBase {
    public int id = -1;
}

/**
 * 学生信息.
 */
public class Student extends EntityBase {
    // 姓名、年龄等其它字段
}

/**
 * 负责数据库操作.
 */
public class Repository {
    public void save(EntityBase entity) {
        if (findById(entity.id) == null) {
            insert(entity);
        } else {
            update(entity);
        }
    }

    public EntityBase findById(int id) {
        return null;
    }

    public void insert(EntityBase entity) { }

    public void update(EntityBase entity) { }

}

public class Tester {

    public static void main(String[] args) throws Exception {
        Student student = new Student();
        Repository repo = new Repository();
        repo.save(student);
    }
}
           

目前的设计有一个问题,findById insert update 等方法的实现依赖于具体的数据库,其中一个原因是不同厂商的数据库对SQL语法的支持是有差异的,单一的 Repository类无法适配不同数据库,为此我们需要改进一下代码,把Repository类抽象成接口,然后针对不同的数据库编写具体的实现代码,在运行时根据配置信息选择具体的数据库实现。

/**
 * 定义对数据库的操作.
 */
public interface Repository {
    void save(EntityBase entity);

    EntityBase findById(int id);

    void insert(EntityBase entity);

    void update(EntityBase entity);
}

public class MySQLRepository implements Repository {
    public void save(EntityBase entity) {
        if (findById(entity.id) == null) {
            insert(entity);
        } else {
            update(entity);
        }
    }

    public EntityBase findById(int id) {
        return null;
    }

    public void insert(EntityBase entity) { }

    public void update(EntityBase entity) { }
}

public class OracleRepository implements Repository {
    public void save(EntityBase entity) {
        if (findById(entity.id) == null) {
            insert(entity);
        } else {
            update(entity);
        }
    }

    public EntityBase findById(int id) {
        return null;
    }

    public void insert(EntityBase entity) { }

    public void update(EntityBase entity) { }
}

public class Tester {

    public static void main(String[] args) throws Exception {
//        String className = "MySQLRepository";
        String className = "OracleRepository";
        Student student = new Student();
        Repository repo = (Repository) Class.forName(className).newInstance();
        repo.save(student);
    }
}
           

改进后的代码看起来还不错,以后如果需要支持其它数据库,只需要实现一个新的 Repository 类就可以了,但是还存在一点问题,无论是MySQL 还是 Oracle,它们对 save 方法的实现都是一样的,重复性的代码不但让程序看起来不够紧凑,而且会使得后期代码的维护和重构工作变得复杂。

考察一下 save 方法,它的实现或者说算法可以分解为以下几个步骤:

  1. 判断记录是否已经在数据库中存在(findById),
  2. 如果不存在则插入新记录(insert),
  3. 否则更新已存在的记录(update)。

对于任何 Repository 类来说这个算法都是一样的,变化的只是具体实现的细节,这正是 模板方法的适用场景,为此可以使用该设计模式进一步改进代码:

public abstract class AbstractRepository {

    public void save(EntityBase entity) {
        if (findById(entity.id) == null) {
            insert(entity);
        } else {
            update(entity);
        }
    }

    public abstract EntityBase findById(int id);

    public abstract void insert(EntityBase entity);

    public abstract void update(EntityBase entity);
}

public class MySQLRepository extends AbstractRepository {

    public EntityBase findById(int id) {
        return null;
    }

    public void insert(EntityBase entity) { }

    public void update(EntityBase entity) { }
}

public class OracleRepository extends AbstractRepository {

    public EntityBase findById(int id) {
        return null;
    }

    public void insert(EntityBase entity) { }

    public void update(EntityBase entity) { }
}
           

改进后的版本消除了重复性的代码,同时提供了良好的扩展性。

模板方法

       模板方法使用抽象方法(findById insert update)在基类中定义算法(save),通过在子类中实现这些方法来提供具体的行为:

  1.  在基类方法中定义算法的结构,把算法的某些步骤推迟到子类实现。
  2. 子类通过重写这些步骤来实现具体的行为,而不是去修改算法的结构。
设计模式 - 模板方法(Template Method)

适用场景

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

  1. 对于算法中不变的部分只在基类中实现一次,变化的部分由子类去实现。
  2. 在重构代码的时候如果发现多个类之间有共同的行为(代码)时,应该为这些类创建抽象基类并(在模板方法中)实现共同的行为,以避免出现重复性的代码。

实际应用

    在做 Java web 开发时可以通过编写 Servlet 类来处理 HTTP 请求,这个 Servlet 类需要派生自 javax.servlet.http.HttpServle,服务器(tomcat / jboss)在接收到 HTTP 请求后通过一系列的调用,最终会把请求路由到 HttpServlet 的 service 方法, service 方法则根据一定的规则(算法)把请求分发给 doGet / doPost / doPut 等方法,自定义Servlet 类通过重写这些 doXXX 方法来实现具体的业务逻辑。

protected void service(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        String method = req.getMethod();

        if (method.equals(METHOD_GET)) {
            long lastModified = getLastModified(req);
            if (lastModified == -1) {
                doGet(req, resp);
            } else {
                // ...
            }
        } else if (method.equals(METHOD_HEAD)) {
            doHead(req, resp);
        } else if (method.equals(METHOD_POST)) {
            doPost(req, resp);
        } else if (method.equals(METHOD_PUT)) {
            doPut(req, resp);
        } else if (method.equals(METHOD_DELETE)) {
            doDelete(req, resp);
        } else if (method.equals(METHOD_OPTIONS)) {
            doOptions(req,resp);
        } else if (method.equals(METHOD_TRACE)) {
            doTrace(req,resp);
        } else {
            // ...
        }
    }
           

在这里service 方法相当 templateMethod, 而 doXXX 则是 primitiveMethod。

继续阅读