概述
模板方法是在面向对象编程中经常会使用到的一个方法,它巧妙的使用了类多态这一特性。我第一次在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 方法,它的实现或者说算法可以分解为以下几个步骤:
- 判断记录是否已经在数据库中存在(findById),
- 如果不存在则插入新记录(insert),
- 否则更新已存在的记录(update)。
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),通过在子类中实现这些方法来提供具体的行为:
- 在基类方法中定义算法的结构,把算法的某些步骤推迟到子类实现。
- 子类通过重写这些步骤来实现具体的行为,而不是去修改算法的结构。
适用场景
- 对于算法中不变的部分只在基类中实现一次,变化的部分由子类去实现。
- 在重构代码的时候如果发现多个类之间有共同的行为(代码)时,应该为这些类创建抽象基类并(在模板方法中)实现共同的行为,以避免出现重复性的代码。
实际应用
在做 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。