模板方法模式:用做菜的故事理解算法骨架复用

87 阅读9分钟

一、故事解说:外婆的菜谱如何统一做菜流程

假设你外婆有一本祖传菜谱:

  1. 固定流程:所有菜的烹饪流程都是:

    • 准备食材 → 处理食材 → 下锅烹饪 → 调味 → 装盘;
  2. 变化步骤

    • 红烧肉:准备五花肉,切块焯水,红烧,加糖和酱油,装盘撒葱花;
    • 清蒸鱼:准备鲈鱼,处理内脏,清蒸,加蒸鱼豉油,装盘撒香菜;
  3. 核心逻辑:外婆定义了做菜的通用流程(模板),具体步骤由不同菜品实现,但流程顺序固定不变。

模板方法模式核心:定义一个算法的骨架(如做菜流程),将一些步骤延迟到子类实现,让子类在不改变算法结构的情况下,重新定义某些步骤,就像外婆的菜谱一样。

二、模板方法模式核心结构(做菜案例)

java

// 1. 抽象类:定义做菜的模板方法和基本步骤
abstract class CookingRecipe {
    // 模板方法:定义做菜的整体流程
    public final void cook() {
        prepareIngredients();
        processIngredients();
        cookInPot();
        season();
       装盘();
    }
    
    // 必须由子类实现的抽象方法
    protected abstract void prepareIngredients();
    protected abstract void processIngredients();
    
    // 具体方法:通用实现
    protected void cookInPot() {
        System.out.println("下锅烹饪");
    }
    
    // 钩子方法:子类可选择性重写
    protected void season() {
        System.out.println("加盐和味精");
    }
    
    // 具体方法:默认实现
    private void 装盘() {
        System.out.println("装盘上桌");
    }
}

// 2. 具体子类:红烧肉
class BraisedPork extends CookingRecipe {
    @Override
    protected void prepareIngredients() {
        System.out.println("准备五花肉");
    }
    
    @Override
    protected void processIngredients() {
        System.out.println("五花肉切块,焯水去血沫");
    }
    
    @Override
    protected void cookInPot() {
        System.out.println("锅中倒油,放冰糖炒糖色,下肉翻炒,加酱油和水,小火慢炖1小时");
    }
    
    @Override
    protected void season() {
        System.out.println("加老抽、料酒、八角、桂皮调味");
    }
}

// 3. 具体子类:清蒸鱼
class SteamedFish extends CookingRecipe {
    @Override
    protected void prepareIngredients() {
        System.out.println("准备鲈鱼");
    }
    
    @Override
    protected void processIngredients() {
        System.out.println("处理鱼内脏,在鱼身上划几刀");
    }
    
    @Override
    protected void cookInPot() {
        System.out.println("锅中烧水,水开后放鱼蒸8分钟");
    }
    
    @Override
    protected void season() {
        System.out.println("倒掉蒸鱼的水,撒葱丝,淋热油,浇蒸鱼豉油");
    }
}

// 4. 客户端:使用模板方法
public class Chef {
    public static void main(String[] args) {
        System.out.println("开始做红烧肉...");
        CookingRecipe porkRecipe = new BraisedPork();
        porkRecipe.cook();
        
        System.out.println("\n开始做清蒸鱼...");
        CookingRecipe fishRecipe = new SteamedFish();
        fishRecipe.cook();
        
        // 输出:
        // 开始做红烧肉...
        // 准备五花肉
        // 五花肉切块,焯水去血沫
        // 锅中倒油,放冰糖炒糖色,下肉翻炒,加酱油和水,小火慢炖1小时
        // 加老抽、料酒、八角、桂皮调味
        // 装盘上桌
        // 
        // 开始做清蒸鱼...
        // 准备鲈鱼
        // 处理鱼内脏,在鱼身上划几刀
        // 锅中烧水,水开后放鱼蒸8分钟
        // 倒掉蒸鱼的水,撒葱丝,淋热油,浇蒸鱼豉油
        // 装盘上桌
    }
}

三、Android 常用模板方法模式案例与实现

案例 1:Activity 生命周期管理(基类 + 子类)

java

// 1. 抽象基类:BaseActivity
public abstract class BaseActivity extends AppCompatActivity {
    // 模板方法:Activity生命周期流程
    @Override
    protected final void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        // 固定流程:设置布局 → 初始化视图 → 初始化数据 → 设置监听器
        setContentView(getLayoutId());
        initViews();
        initData();
        setupListeners();
    }
    
    // 必须由子类实现的抽象方法
    protected abstract int getLayoutId();
    protected abstract void initViews();
    protected abstract void initData();
    
    // 钩子方法:子类可选择性重写
    protected void setupListeners() {
        // 默认空实现,子类需要时重写
    }
    
    // 其他生命周期方法...
}

// 2. 具体子类:MainActivity
public class MainActivity extends BaseActivity {
    private TextView textView;
    
    @Override
    protected int getLayoutId() {
        return R.layout.activity_main;
    }
    
    @Override
    protected void initViews() {
        textView = findViewById(R.id.textView);
    }
    
    @Override
    protected void initData() {
        textView.setText("欢迎来到主界面");
    }
    
    @Override
    protected void setupListeners() {
        textView.setOnClickListener(v -> {
            // 处理点击事件
        });
    }
}

// 3. 具体子类:LoginActivity
public class LoginActivity extends BaseActivity {
    private EditText usernameEditText;
    private EditText passwordEditText;
    private Button loginButton;
    
    @Override
    protected int getLayoutId() {
        return R.layout.activity_login;
    }
    
    @Override
    protected void initViews() {
        usernameEditText = findViewById(R.id.username);
        passwordEditText = findViewById(R.id.password);
        loginButton = findViewById(R.id.loginButton);
    }
    
    @Override
    protected void initData() {
        // 初始化登录数据
    }
    
    @Override
    protected void setupListeners() {
        loginButton.setOnClickListener(v -> {
            // 处理登录逻辑
        });
    }
}

优点

  • 统一流程:所有 Activity 遵循相同的初始化流程,减少代码重复;

  • 提高可维护性:修改通用流程只需在基类中修改,无需修改所有子类;

  • 强制规范:通过抽象方法确保子类实现必要的初始化步骤。

缺点

  • 灵活性降低:子类必须遵循基类定义的流程,难以改变整体结构;
  • 过度抽象风险:基类可能变得过于庞大,包含过多方法;
  • 子类依赖基类:子类行为依赖基类实现,基类修改可能影响所有子类。

案例 2:网络请求框架的基类(定义请求流程)

java

// 1. 抽象基类:定义网络请求的模板方法
public abstract class NetworkRequest {
    // 模板方法:定义完整的网络请求流程
    public final void execute() {
        preRequest();
        Response response = sendRequest();
        postRequest(response);
        processResponse(response);
    }
    
    // 前置处理:钩子方法,子类可选择性重写
    protected void preRequest() {
        System.out.println("准备请求:添加通用请求头");
    }
    
    // 必须由子类实现的抽象方法
    protected abstract Response sendRequest();
    
    // 后置处理:钩子方法
    protected void postRequest(Response response) {
        System.out.println("请求完成:记录请求耗时");
    }
    
    // 处理响应:抽象方法
    protected abstract void processResponse(Response response);
}

// 2. 具体子类:GET请求
public class GetRequest extends NetworkRequest {
    private String url;
    
    public GetRequest(String url) {
        this.url = url;
    }
    
    @Override
    protected Response sendRequest() {
        System.out.println("发送GET请求到:" + url);
        // 实际发送GET请求的代码
        return new Response(200, "{"data":"response data"}");
    }
    
    @Override
    protected void processResponse(Response response) {
        if (response.getStatusCode() == 200) {
            System.out.println("处理成功响应:" + response.getBody());
        } else {
            System.out.println("处理失败响应:" + response.getStatusCode());
        }
    }
}

// 3. 具体子类:POST请求
public class PostRequest extends NetworkRequest {
    private String url;
    private String body;
    
    public PostRequest(String url, String body) {
        this.url = url;
        this.body = body;
    }
    
    @Override
    protected void preRequest() {
        super.preRequest();
        System.out.println("额外处理:设置POST请求体");
    }
    
    @Override
    protected Response sendRequest() {
        System.out.println("发送POST请求到:" + url + ",内容:" + body);
        // 实际发送POST请求的代码
        return new Response(201, "{"message":"created"}");
    }
    
    @Override
    protected void processResponse(Response response) {
        if (response.getStatusCode() == 201) {
            System.out.println("资源创建成功:" + response.getBody());
        } else {
            System.out.println("处理失败响应:" + response.getStatusCode());
        }
    }
}

// 4. 客户端:使用网络请求模板
public class Client {
    public static void main(String[] args) {
        System.out.println("执行GET请求...");
        NetworkRequest getRequest = new GetRequest("https://api.example.com/data");
        getRequest.execute();
        
        System.out.println("\n执行POST请求...");
        NetworkRequest postRequest = new PostRequest("https://api.example.com/upload", "{"key":"value"}");
        postRequest.execute();
        
        // 输出:
        // 执行GET请求...
        // 准备请求:添加通用请求头
        // 发送GET请求到:https://api.example.com/data
        // 请求完成:记录请求耗时
        // 处理成功响应:{"data":"response data"}
        // 
        // 执行POST请求...
        // 准备请求:添加通用请求头
        // 额外处理:设置POST请求体
        // 发送POST请求到:https://api.example.com/upload,内容:{"key":"value"}
        // 请求完成:记录请求耗时
        // 资源创建成功:{"message":"created"}
    }
}

优点

  • 流程标准化:所有网络请求遵循相同流程,便于统一处理日志、错误等;

  • 代码复用:通用逻辑(如添加请求头、记录耗时)只需在基类实现一次;

  • 易于扩展:新增请求类型(如 PUT、DELETE)只需继承基类,实现必要方法。

缺点

  • 子类受限:子类必须遵循基类定义的流程,难以改变整体结构;
  • 调试困难:多层继承和方法调用可能使调试变得复杂;
  • 基类膨胀:基类可能包含越来越多的钩子方法,导致类变得庞大。

案例 3:数据库操作基类(定义 CRUD 流程)

java

// 1. 抽象基类:定义数据库操作的模板方法
public abstract class BaseDao<T> {
    protected SQLiteDatabase db;
    
    public BaseDao(SQLiteDatabase db) {
        this.db = db;
    }
    
    // 模板方法:定义插入数据的流程
    public final long insert(T entity) {
        beginTransaction();
        long rowId = doInsert(entity);
        endTransaction();
        afterInsert(rowId);
        return rowId;
    }
    
    // 模板方法:定义查询数据的流程
    public final List<T> query(String selection, String[] selectionArgs) {
        beginTransaction();
        List<T> result = doQuery(selection, selectionArgs);
        endTransaction();
        afterQuery(result);
        return result;
    }
    
    // 事务开始:具体方法
    private void beginTransaction() {
        db.beginTransaction();
        System.out.println("开始数据库事务");
    }
    
    // 事务结束:具体方法
    private void endTransaction() {
        db.setTransactionSuccessful();
        db.endTransaction();
        System.out.println("结束数据库事务");
    }
    
    // 必须由子类实现的抽象方法
    protected abstract long doInsert(T entity);
    protected abstract List<T> doQuery(String selection, String[] selectionArgs);
    
    // 钩子方法:插入后处理
    protected void afterInsert(long rowId) {
        System.out.println("插入完成,行ID: " + rowId);
    }
    
    // 钩子方法:查询后处理
    protected void afterQuery(List<T> result) {
        System.out.println("查询完成,结果数量: " + (result != null ? result.size() : 0));
    }
}

// 2. 具体子类:用户数据访问对象
public class UserDao extends BaseDao<User> {
    private static final String TABLE_NAME = "users";
    
    public UserDao(SQLiteDatabase db) {
        super(db);
    }
    
    @Override
    protected long doInsert(User user) {
        ContentValues values = new ContentValues();
        values.put("name", user.getName());
        values.put("age", user.getAge());
        return db.insert(TABLE_NAME, null, values);
    }
    
    @Override
    protected List<User> doQuery(String selection, String[] selectionArgs) {
        List<User> users = new ArrayList<>();
        Cursor cursor = db.query(TABLE_NAME, null, selection, selectionArgs, null, null, null);
        
        if (cursor != null) {
            while (cursor.moveToNext()) {
                String name = cursor.getString(cursor.getColumnIndex("name"));
                int age = cursor.getInt(cursor.getColumnIndex("age"));
                users.add(new User(name, age));
            }
            cursor.close();
        }
        
        return users;
    }
    
    @Override
    protected void afterInsert(long rowId) {
        super.afterInsert(rowId);
        System.out.println("用户数据已插入,ID: " + rowId);
    }
}

// 3. 客户端:使用数据库操作模板
public class DatabaseClient {
    public static void main(String[] args) {
        // 假设已创建SQLiteDatabase实例
        SQLiteDatabase db = null; // 实际中初始化数据库
        UserDao userDao = new UserDao(db);
        
        // 插入用户
        User user = new User("张三", 25);
        long rowId = userDao.insert(user);
        
        // 查询用户
        List<User> users = userDao.query("age > ?", new String[]{"20"});
        
        // 输出:
        // 开始数据库事务
        // 结束数据库事务
        // 插入完成,行ID: 1
        // 用户数据已插入,ID: 1
        // 开始数据库事务
        // 结束数据库事务
        // 查询完成,结果数量: 1
    }
}

优点

  • 事务一致性:所有数据库操作都遵循相同的事务管理流程,确保数据一致性;

  • 减少重复代码:事务管理、日志记录等通用逻辑只需在基类实现一次;

  • 便于维护:修改数据库操作流程(如添加缓存)只需在基类修改。

缺点

  • 灵活性受限:子类必须遵循基类定义的流程,难以实现特殊需求;
  • 继承链脆弱:基类修改可能影响所有子类,需要谨慎设计;
  • 过度抽象:对于简单的数据库操作,使用模板方法可能增加不必要的复杂度。

四、模板方法模式的适用场景与总结

适用场景

  1. 流程固定:多个子类有共同的算法流程,但部分步骤实现不同(如 Activity 生命周期、网络请求流程);
  2. 代码复用:避免在多个子类中重复实现相同的代码(如事务管理、日志记录);
  3. 框架设计:框架定义通用流程,让开发者实现特定步骤(如 Android 的 AsyncTask);
  4. 强制规范:确保子类遵循统一的执行流程(如初始化顺序)。

核心优点

  • 代码复用:将通用代码放在基类,减少子类重复;
  • 统一流程:确保所有子类遵循相同的算法结构;
  • 易于维护:修改算法流程只需在基类修改,无需修改所有子类;
  • 符合开闭原则:新增子类只需实现必要步骤,无需修改基类。

核心缺点

  • 灵活性降低:子类必须遵循基类定义的流程,难以改变整体结构;
  • 类数量增加:每个不同实现都需要一个子类,可能导致类数量过多;
  • 调试困难:多层继承和方法调用可能使调试变得复杂;
  • 基类膨胀:基类可能包含越来越多的钩子方法,导致类变得庞大。

Android 中的最佳实践

  • 基类设计:在框架层使用模板方法定义通用流程(如 Activity 基类、网络请求基类);

  • 钩子方法使用:合理使用钩子方法,让子类可选择性重写;

  • 避免过度抽象:只在确实需要统一流程的地方使用模板方法,避免滥用;

  • 结合其他模式:与工厂模式结合创建子类实例,与策略模式结合替代复杂的条件判断。

模板方法模式是 Android 开发中组织代码结构的重要工具,尤其适合处理有固定流程但部分步骤实现不同的场景,理解它有助于设计出更健壮、可维护的框架和库。