webpack源码模板方法模式

17 阅读5分钟

模板方法模式

模板方法模式是算法执行的一个骨架。将算法分解为一系列步骤,每一个步骤即为一个方法。在父类中定义所有的步骤方法,并且在一个方法(模板方法)依次执行这个算法的过程。相同的步骤抽取到父类实现,子类在不修改结构的情况下重写算法的特定步骤。这种类型的设计模式属于行为型模式。 父类中了定义所有的方法以及算法执行过程,子类只是重写某些特殊实现,不改变整个算法结构。 问题 假如你正在开发一款分析公司文档的数据挖掘程序。 用户需要向程序输入各种格式 (PDF、 DOC 或 CSV) 的文档, 程序则会试图从这些文件中抽取有意义的数据, 并以统一的格式将其返回给用户。 该程序的首个版本仅支持 DOC 文件。 在接下来的一个版本中, 程序能够支持 CSV 文件。 一个月后, 你 “教会” 了程序从 PDF 文件中抽取数据。

340648129-d64dfd11-35ad-40a8-b89b-974ec9a30d4f.png 一段时间后, 你发现这三个类中包含许多相似代码。 尽管这些类处理不同数据格式的代码完全不同, 但数据处理和分析的代码却几乎完全一样。 如果能在保持算法结构完整的情况下去除重复代码, 这难道不是一件很棒的事情吗? 还有另一个与使用这些类的客户端代码相关的问题: 客户端代码中包含许多条件语句, 以根据不同的处理对象类型选择合适的处理过程。 如果所有处理数据的类都拥有相同的接口或基类, 那么你就可以去除客户端代码中的条件语句, 转而使用多态机制来在处理对象上调用函数。

340646675-a081dee7-5de2-456d-b9be-9e519ce7aa80.png 首先, 我们将所有步骤声明为 抽象类型, 强制要求子类自行实现这些方法。 在我们的例子中, 子类中已有所有必要的实现, 因此我们只需调整这些方法的签名, 使之与超类的方法匹配即可。

现在, 让我们看看如何去除重复代码。 对于不同的数据格式, 打开和关闭文件以及抽取和解析数据的代码都不同, 因此无需修改这些方法。 但分析原始数据和生成报告等其他步骤的实现方式非常相似, 因此可将其提取到基类中, 以让子类共享这些代码。

正如你所看到的那样, 我们有两种类型的步骤:

  1. 抽象步骤必须由各个子类来实现
  2. 可选步骤已有一些默认实现, 但仍可在需要时进行重写

还有另一种名为钩子的步骤。 钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。

抽象基类包含的方法:

  1. 模板方法,根据算法结构调用相应步骤。可用 final最终修饰模板方法以防止子类对其进行重写。
  2. 抽象方法:必须由各个子类来实现。
  3. 可选方法:已有一些默认的实现,但仍可在需要时进行重写。
  4. 钩子方法:钩子是内容为空的可选步骤。 即使不重写钩子, 模板方法也能工作。 钩子通常放置在算法重要步骤的前后, 为子类提供额外的算法扩展点。

优点: ①封装不变部分, 扩展可变部分 把认为是不变部分的算法封装到父类实现, 而可变部分的则可以通过继承来继续扩展。

②提取公共部分代码, 便于维护

③行为由父类控制, 子类实现

基本方法是由子类实现的, 因此子类可以通过扩展的方式增加相应的功能, 符合开闭原则。

缺点: ①子类执行的结果影响了父类的结果,这和我们平时设计习惯颠倒了,在复杂项目中,会带来阅读上的难度。

②可能引起子类泛滥和为了继承而继承的问题

例子

步骤1 创建一个抽象类,它的模板方法被设置为 final。

public abstract class Game {
   abstract void initialize();
   abstract void startPlay();
   abstract void endPlay();
 
   //模板
   public final void play(){
 
      //初始化游戏
      initialize();
 
      //开始游戏
      startPlay();
 
      //结束游戏
      endPlay();
   }
}

步骤2 创建扩展了上述类的实体类。

public class Cricket extends Game {
 
   @Override
   void endPlay() {
      System.out.println("Cricket Game Finished!");
   }
 
   @Override
   void initialize() {
      System.out.println("Cricket Game Initialized! Start playing.");
   }
 
   @Override
   void startPlay() {
      System.out.println("Cricket Game Started. Enjoy the game!");
   }
}
public class Football extends Game {
 
   @Override
   void endPlay() {
      System.out.println("Football Game Finished!");
   }
 
   @Override
   void initialize() {
      System.out.println("Football Game Initialized! Start playing.");
   }
 
   @Override
   void startPlay() {
      System.out.println("Football Game Started. Enjoy the game!");
   }
}

步骤3 使用 Game 的模板方法 play() 来演示游戏的定义方式。

public class TemplatePatternDemo {
   public static void main(String[] args) {
 
      Game game = new Cricket();
      game.play();
      System.out.println();
      game = new Football();
      game.play();      
   }
}

webpack源码模板方法模式

最终的执行代码是动态生成的。其中执行代码生成的过程就使用到了模板模式。先看下代码生成的过程。

// Hook.js
compile(options) {
   throw new Error("Abstract: should be overriden");
}

// HookCodeFactory.js
compile(options) {
   factory.setup(this, options);
   return factory.create(options);
}

// 只实现了content方法,create方法继承自父类HookCodeFactory
class SyncBailHookCodeFactory extends HookCodeFactory {
   content({ onError, onResult, resultReturns, onDone, rethrowIfPossible }) {
       //...
   }
}

class HookCodeFactory {
    create(options) {
        //..
        switch (this.options.type) {
            case "sync":
                fn = new Function(
                    this.args(),
                    '"use strict";\n' +
                    this.header() +
                    this.content({ /*...*/ })
                );
                break;
                //...
        }
        return fn;
    }
    
    args (...) { /*...*/ }
    
    header (...) { /*...*/ }
}

代码生成逻辑在codeFactory类中,当调用hook.call()时会调用到hook.compile()方法来动态生成代码。动态生成的过程是一致的,因此收敛到父类HookCodeFactory的create方法中,在子类中只是实现差异化的部分如这里的content(...)。

看到这里HookCodeFactory提供了执行模板new Function部分,子类实现差异化部分content(...)。

该模式定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。