模板方法

106 阅读8分钟

模板方法

模板是对多种事物的结构、形式、行为的模式化总结,而模板方法模式(Template Method)则是对一系列类行为(方法)的模式化。我们将总结出来的行为规律固化在基类中,对具体的行为实现则进行抽象化并交给子类去完成,如此便实现了子类对基类模板的套用。

生存法则

我们的现实生活中还有很多模板方法模式的实例,如工作流程、项目管理等。我们先从一个简单的例子开始。哺乳动物的生存技能(行为)是多样化的,有的能上天,有的能入海,但都离不开觅食这个过程,如鲸在海里觅食,蝙蝠在空中捕捉昆虫,而人类则可以利用各种交通工具到想去的地方用餐。

既然鲸、人类、蝙蝠都是动物,那么一定得具备动物最基本的生存技能,所以我们建模时要体现其“动”与“吃”这两种本能行为,缺一不可。虽然哺乳动物的生存技能有着天壤之别,但它们的生存方法毫无二致。倘若为每种动物都编写一遍同样的方法,必定会造成代码冗余。我们不如将这个生存法则抽离出来,就像表格一样,作为一个通用的模板方法,定义在哺乳动物类中。

package template;

public abstract class Mammal {
    public abstract void move();
    
    public abstract void eat();
    
    public final void live(){
        move();
        eat();
    }
}

如代码,哺乳动物类分别定义了“移动”与“进食”两种动物本能,利用抽象方法关键字“abstract”声明凡是哺乳动物必须实现这两个行为。接着第7行的生存方法live()则以实体方法的形式出现,这就意味着所有哺乳动物都要以此为模板,这便是我们要抽离出来的模板方法了。可以看到第8行与第9行我们在模板方法中分别调用了move()方法与eat()方法,固化下来的生存法则必须先“移动”再“进食”才能完成“捕食”。此外,我们使用了关键字“final”使此模板方法不能被重写修改。

package template;

public class Whale extends Mammal {
    @Override
    public void move() {
        System.out.println("鲸鱼在水里游着。。。");
    }

    @Override
    public void eat() {
        System.out.println("捕鱼吃。。。");
    }
}
package template;

public class Human extends Mammal{
    @Override
    public void move() {
        System.out.println("人类在路上开着车");
    }

    @Override
    public void eat() {
        System.out.println("去公司挣钱吃饭");
    }
}

鲸类与人类都继承了哺乳动物基类,较之前的代码更加简单了,它们只需要实现自己独特的生存技能move()与eat(),至于生存方法live()则直接由基类而来。哺乳动物的基因模板得以继承,容不得半点改动。至此,模板方法模式已经构建完成。

最后我们来看在客户端如何让哺乳动物们生龙活虎起来

package template;

public class Client {
    public static void main(String[] args) {
        Mammal mammal = new Whale();
        mammal.live();
        mammal = new Human();
        mammal.live();
    }
}

哺乳动物统一调用了通用的模板方法,以此作为生存法则就能很好地存活下去。可以看到第13行的输出中,动物们都拥有各自的生存技能,在自然环境下各显神通。

项目管理模板

模板方法非常简单实用,我们可以让它再包含一些逻辑,就像一套既定的工作流程,来为后人铺路。如图所示,当我们做一些简单的软件项目管理时,常常会采用传统的瀑布模型,这时我们可以把整个项目周期分为5个阶段,分别是需求分析、软件设计、代码开发、质量测试、上线发布

要以模板方法模式来实现项目管理的瀑布模型,我们首先得定义一个瀑布模型项目管理类,抽象出所有项目阶段以供实体方法调用

瀑布模型项目管理类PM

package template;

public abstract class PM {
    public abstract String analyze();//需求分析
    
    public abstract String design(String project);//软件设计
    
    public abstract String develop(String project);//代码开发
    
    public abstract boolean test(String project);//质量测试
    
    public abstract void release(String project);//上线发布
    
    protected final void kickoff(){
        String requirement = analyze();
        String designCode = design(requirement);
        do{
            designCode = develop(designCode);
        }while (!test(designCode));//测试失败则需要修改代码
            release(designCode);
        
    }
}

瀑布模型项目管理类分别声明了项目管理周期中各阶段的分步抽象方法,其中包括需求分析analyze()、软件设计design()、代码开发develop()、质量测试test()、上线发布release()。这些步骤的实现统统由子类去自由发挥,例如第9行的质量测试方法test(),子类可以进行人工测试,也可以实现自动化测试,此处不必关心这些实现细节。站在项目管理的角度来看,抽象类应该关注的是对大局的操控,把控项目进度,避免造成资源浪费,譬如程序员在没有确立技术框架的情况下就进行代码开发,难免会引入不必要的工作量,可见模板方法的重要性。基于此,我们在代码第13行定义了模板方法,在项目启动方法kickoff()中从宏观上制订了整个项目的固定流程,由第14行开始首先进行需求分析,再交给架构师进行软件设计,接着程序员设计文档进行代码开发或者修改bug的迭代流程,直至测试通过为止,最终上线发布。整个项目的实施阶段被组织起来,充分展现了瀑布模型项目周期

瀑布模型的模板已经准备就绪,下面轮到具体的项目子类去填补(实现)空缺了。碰巧这时公司决定开发一套人力资源管理系统,由于项目比较简单,预估在一个季度内就能完成,因此项目组决定用瀑布模型进行管理。于是我们果断立项并继承了瀑布模型模板

人力资源管理系统项目类HRProject

package template;

import java.util.Random;

public class HRProject extends PM {

   private Random random = new Random();

   @Override
   public String analyze() {
      System.out.println("分析师:需求分析……");
      return "人力资源管理系统需求";
   }

   @Override
   public String design(String project) {
      System.out.println("架构师:程序设计……");
      return "设计(" + project + ")";
   }

   @Override
   public String develop(String project) {
      //修复bug
      if (project.contains("bug")) {
         System.out.println("开  发:修复bug……");
         project = project.replace("bug", "");
         project = "修复(" + project + ")";
         if (random.nextBoolean()) {
            project += "bug";//可能会引起另一个bug
         }
         return project;
      }

      //开发系统功能
      System.out.println("开  发:写代码……");
      if (random.nextBoolean()) {
         project += "bug";//可能会产生bug
      }
      return "开发(" + project + ")";
   }

   @Override
   public boolean test(String project) {
      if (project.contains("bug")) {
         System.out.println("测  试:发现bug……");
         return false;
      }
      System.out.println("测  试:用例通过……");
      return true;
   }

   @Override
   public void release(String code) {
      System.out.println("管理员:上线发布……");
      System.out.println("====================最终产品====================");
      System.out.println(code);
      System.out.println("================================================");
   }
}

客户端类Client

package template;

public class Client {
    public static void main(String[] args) {
        PM pm = new HRProject();
        pm.kickoff();
    }
}

当然,对于基类模板中的步骤方法并不是必须要用抽象方法,而是完全可以用实体方法去实现一些通用的操作。如果子类需要个性化就对其进行重写变更,不需要就直接继承。做软件设计切勿生搬硬套、照本宣科,能够根据具体场景进行适当的变通,才是对设计模式更灵活、更恰当的运用。

虚实结合

总之,模板方法模式可以将总结出来的规律沉淀为一种既定格式,并固化于模板中以供子类继承,对未确立下来的步骤方法进行抽象化,使其得以延续、多态化,最终架构起一个平台,使系统实现在不改变预设规则的前提下,对每个分步骤进行个性化定义的目的。下面我们来拆解模板方法模式的类结构

AbstractClass(抽象基类):定义出原始操作步骤的抽象方法(primitiveOperation)以供子类实现,并作为在模板方法中被调用的一个步骤。此外还实现了不可重写的模板方法,其可将所有原始操作组织起来成为一个框架或者平台。对应本章例程中的瀑布模型项目管理类PM。

ConcreteClassA、ConcreteClassB(实现类A、实现类B):继承自抽象基类并且对所有的原始操作进行分步实现,可以有多种实现以呈现每个步骤的多样性。对应本章例程中的人力资源管理系统项目类HRProject。

Go版本代码

package template

import (
    "fmt"
    "math/rand"
    "strings"
    "time"
)

type PM interface {
    analyze() string
    design(string) string
    develop(string) string
    test(string) bool
    release(string)
}

type PMDirector struct {
}

func (d PMDirector) Kickoff(pm PM) {
    requirement := pm.analyze()
    designCode := pm.design(requirement)
    for {
        designCode = pm.develop(designCode)

        if pm.test(designCode) {
            break
        }
    }
    pm.release(designCode)
}

type HRPRoject struct {
}

func (h *HRPRoject) analyze() string {
    fmt.Println("分析师:需求分析……")
    return "人力资源管理系统需求"
}

func (h *HRPRoject) design(project string) string {
    fmt.Println("架构师:程序设计……")
    return "设计(" + project + ")"
}

func (h *HRPRoject) develop(project string) string {
    //修复bug
    rand.Seed(time.Now().UnixNano())
    if strings.Contains(project, "bug") {
        fmt.Println("开发:修复bug。。。")
        project = strings.ReplaceAll(project, "bug", "")
        project = "修复{" + project + "}"
        if rand.Float32() < 0.5 {
            project += "bug"
        }
        return project
    }

    //开发系统功能
    fmt.Println("开发:写代码。。。")
    if rand.Float32() < 0.5 {
        project += "bug"
    }
    return "开发{" + project + "}"
}

func (h *HRPRoject) test(project string) bool {
    if strings.Contains(project, "bug") {
        fmt.Println("测试:发现bug")
        return false
    }
    fmt.Println("测试通过")
    return true
}

func (h *HRPRoject) release(project string) {
    fmt.Println("上线发布。。。")
    fmt.Println("==============最终产品=============")
    fmt.Println(project)
    fmt.Println("===================================")
}
package main

import "desginPatterns/template"

func main() {
    pm := template.PMDirector{}
    pm.Kickoff(&template.HRPRoject{})
}