【学习笔记javascript设计模式与开发实践(模板方法模式)----11】

第11章 模板方法模式

在javascript开发中用到继承的场景其实并不是很多,很多时候我们都喜欢用mix-in的方式给对象扩展属性。但这不代表继承在javascript里没有用武之地,虽然没有真正的类和继承机制,但我们可以通过原型prototype来变相地实现继承。

11.1 模板方法模式的定义和组成

模板方法模式是一种只需要用来使用继承就可以实现的非常简单的模式。

模板方法模式由两部分组成,第一部分是抽象父类,第二部分是具体的实现子类。通常在抽象父类中封装了子类的算法框架,包括实现一些公共方法以及封装子类中所有方法的执行顺序。子类通过继承这个抽象类,也继承了整个算法结构,并且可以选择重写父类的方法。

假如我们有一些平等的子类,各个子类之间有一些相同的行为,也有一些不同的行为。如果相同和不同的行为都混合在各个子类的实现中,说明这些相同的行为会在各个子类中重复出现。但实际上,相同的行为可以被搬移到另外一个单一的地方,模板方法模式就是为了解决这个问题而生的。在模板方法模式中,子类实现中的相同部分被上移到父类中,而将不同的部分留待子类来实现。这也很好的体现了泛化思想。

 

11.2 第一个例子----Coffee  or  Tea

咖啡与茶是一个经典的例子,经常用来讲解模板方法模式,这个例子的原型来自《Head First 设计模式》。这一节我们就用javascript来实现这个例子。

 

11.2.1 先泡一杯咖啡

1.       把水煮沸

2.       用沸水冲泡咖啡

3.       把咖啡倒进杯子

4.       加糖和牛奶

 

通过下面这段代码,我们就能得到一杯咖啡

var Coffee = function(){};
Coffee.prototype.boilWater = function(){
  console.log(‘把水煮沸’);
}
 
Coffee.prototype.brewCoffeeGriends =function(){
 console.log(‘用沸水冲泡咖啡’);
}
 
Coffee.prototype.pourInCup = function(){
 console.log(‘把咖啡倒进杯子’);
}
 
Coffee.prototype.addSugarAndMilk =function(){
  console.log(‘加糖加牛奶’);
}
 
Coffee.prototype.init = function(){
  this.boilWater();
  this. brewCoffeeGriends();
  this. pourInCup();
  this. addSugarAndMilk();
}
 
var coffee = new Coffee();
coffee.init();

11.2.2 泡一壶茶

1.       把水煮沸

2.       用沸水浸泡茶叶

3.       把茶水倒进杯子

4.       加柠檬

 

var Tea = function(){};
 Tea.prototype.boilWater = function(){
   console.log('把水煮沸');
  }
 Tea.prototype.steepTeaBag = function(){
   console.log('用沸水浸泡茶叶')
  }
 Tea.prototype.pourInCup = function(){
   console.log('把茶水倒进杯子')
  }
 
 Tea.prototype.addLemon = function(){
   console.log('加柠檬');
  }
 
 Tea.prototype.init = function(){
     this.boilWater();
     this.steepTeaBag();
     this.pourInCup();
     this.addLemon();
 var tea = new Tea();
 tea.init();


11.2.3 分离共同点

现在我们分别泡好了一杯咖啡和一壶茶,经过思考和比较,我们发现咖啡和茶的冲泡赛程是大同小异,

泡咖啡

泡茶

把水煮沸

把水煮沸

用沸水冲泡咖啡

用沸水浸泡茶叶

把咖啡倒塌杯子

把茶水倒进杯子

加糖加奶

加柠檬

 

我们找到泡咖啡和泡茶主要以下不同点

l   原不同

l   泡的方式不同

l   加入的调料不同

经过抽象,不管是泡咖啡还是泡茶,我们都能整理为下面四步

1.       把水煮沸

2.       用沸水冲泡饮料

3.       把饮料倒进杯子

4.       加调料

所以不管是冲泡还是浸泡,我们都能给它一个新的方法名称,比如说brew()。同理,不管是加糖和牛奶,还是加柠檬,我们都可以称之为addCondimets()。

让我忘记最开始创建Coffee和Tea类,都被我们用Beverage来表示如下:

var Beverage =function(){};
Beverage.prototype.boilWater=function(){
    console.log('把水者沸');
};
Beverage.prototype.brew=function(){};//空方法,应该由子类重写
Beverage.prototype.pourInCup=function(){};
Beverage.prototype.addCondiments=function(){};
Beverage.prototype.init=function(){
   this.boilWater();
   this.brew();
   this.pourInCup();
   this.addCondiments();
}

11.2.4 创建Coffee子类和Tea子类

现在创建一个Beverage类的对象对我们来说没有意义,因为世界上能喝的东西没有一种真正叫“饮料”的,饮料在这里还只是一个抽象的存在。接下来我们要创建咖啡类和茶类,并让我们继承饮料:

var Coffee = function(){};
Coffee.prototype = new Beverage();

接下来要重写抽象父类中的一些方法,只有“把水煮沸”这个行为可以直接使用父类Beverage中的boilWater方法,其方法都需要在Coffee子类中重写。

Coffee.prototype.brew = function(){
  console.log(‘用沸水冲泡咖啡’);
}
 
Coffee.prototype.pourInCup = function(){
 console.log(‘把咖啡倒进杯子’);
}
 
Coffee.prototype.addCondiments = function(){
 console.log(‘加糖和牛奶’);
}

var coffee = new Coffee();
coffee.init();

到此我们的Coffee类已经完成了,当调用coffee对象的init方法时,由于coffee对象和Coffee构造器的原型prototype上都没有对应的init方法,所以该请求会顺着原型链,被委托给Coffee的“父类”Beverage原型上的init方法。

而Beverage.prototype.init方法中已经规定好了泡饮料的顺序,所以我们能成功地泡出一杯咖啡。

 

接下来照葫芦画瓢,来创建Tea类

var Tea = function(){};
Tea.prototype = new Beverage();
Tea.prototype.brew = function(){
  console.log('用沸水浸泡茶叶');
}
Tea.prototype.pourInCup = function(){
 console.log('把茶倒进杯子');
}
Tea.prototype.addCondiments = function(){
    console.log('加柠檬');
}
 
var tea = new Tea();
tea.init();

本章一直讨论的是模板方法模式,那么在上面的例子中,到底谁才是所谓的模板方法呢?答案是Beverage.prototype.init。

Beverage.prototype.init被称为模板方法的原因是,该方法中封装了子类的算法框架,它作为一个算法的模板,指导子类以何种顺序去执行哪些方法。在Beverage.prototype.init方法中,算法内的每一个步骤都清楚地展示在我们眼前。

11.3 抽象类

首先要说明的是,模板方法模式是一种严重依赖抽象类的设计模式。javascript在语言层面并没有提供对象抽象类的支持,欠也很难模拟抽象类的实现。这一节我们将着重讨论java中抽像类的作用,以及javascript没有抽象类时所做出的让步和变通。

11.3.1 抽象类的作用

在java中,类分为两种,一种为具体类,另一种为抽象类。具体类可以被实例化,抽象类不能被实例化。要了解抽象类不能被实例化的原因,我们可以思考“饮料”这个抽象类。

想象这一个场景:我们口渴了,去便利店想买一瓶饮料,我们不直接跟店员说:“来一瓶饮料。”哪果我们说了,那么店员接下来肯定问:“要什么饮料?”饮料只是一个抽象名词,只有当我们真正明确了饮料类型之后,才能得到一杯咖啡、茶、或者可乐。

由于抽象类不能被实例化,如果有人编写了一个抽象类,那么这个抽象类一定是用来被某些具体类继承的。

抽象类和接口一样可以用于向上转型,在静态类型语言中,编译器对类型的检查总是一个绕不过的话题与困扰。虽然类型检查可以提高程序的安全性,但繁琐而严格的类型检查也时常会让程序员觉得麻烦。把对象的真正类型隐藏在抽象类或者接口之后,这些对象才可以被互相替换使用。这可以让我们的java程序尽量遵守依赖倒置原则。

除了用于向上转型,抽象类也可以表示一种契约。继承了这个抽象类的所有子类都将拥有跟抽象类一致的接口方法,抽象类的主要作用就是为它的子类定义这些公共接口。

11.3.3 java实现Coffee和Tea的例子(略)

11.3.4 javascript没有抽象类的缺点和解决方案

不能保证实现类实现所有抽象类中的抽像方法。解决方案如:

用鸭子类型来模拟接口检查,以便确保子类中确实重写了父类的方法。但模拟接口检查会带来不必要的复杂性,而且要求程序员主动进行这些接口检查,这就要求我们在业务代码中添加一些跟业务逻辑无关的代码。

让Beverage.prototype.brew等方法直接抛出一个异常,如果因为粗心忘记编写Coffee.prototype.brew方法,那么至少我们会在程序运行时得到一个错误:

Beverage.prototype.brew = function(){
  throw new Error(‘子类必须重写brew方法’);
}

11.4 模板方法模式的使用场景

从大的方面来讲,模板方法模式常被架构师用于搭建项目的框架,架构师写好了框架骨架,程序员继承框架的结构之后,负责往里面填空,比如java程序员大多使用过HttpServlet技术开发项目,一个基于HttpServlet的程序包含7个生命周期,这7个生命周期分别对应一个do方法

doGet()

doHead()

doPost()

doPut()

doDelete()

doOption()

doTrace()

HttpServlet类还提供了一个service方法,它就是这里的模板方法,service规定了这些do方法的执行顺序,而这些do方法的个体实现则需要HttpServlet的子类来提供。

在web开发中也能找到很多模板方法模式的场景,比如我们构建一系列的UI组件,这些组件的构建过程一般如下:

1.       初始化一个Div容器

2.       通过ajax请求拉取相应的数据

3.       把数据渲染到div容器里面,完成组件的构造

4.       通知用户组件渲染完毕

于是我们可以把4个步骤都抽象到父类的模板方法里面,父类中还可以顺便提供第1步和第4步的具体实现。当子类继承这个父类之后,会重写模板方法里面的第2步和第3步。

11.5 钩子方法

通过模板方法模式,我们在父类中封装了子类的算法框架。这些算法框架在正常状态下是适用于大多数子类的,但如果有一些特别“个性”的子类呢?比如我们在饮料类中封装了饮料的冲泡顺序

1.       把水煮沸

2.       用沸水冲泡饮料

3.       把饮料倒进杯子

4.       加调料

这4个冲泡饮料的步骤适用于咖啡和茶,在我们的饮料店里,根据这4个步骤作出来的咖啡和茶,一直顺序地提供绝大部分客人享用。但有一些客人喝咖啡不加调料的。既然Beverage作为父类,已经规定好了冲泡饮料的4个步骤,那么有什么办法可以让子类不受这个约束呢?

钩子方法(hook)可以用来解决这个问题,放置钩子是隔离变化的一种常见手段。我们在父类中容易变化的地方放置钩子,钩子可以有一个默认实现,空间要不要“挂钩”,这由子类自行决定。钩子方法返回结果决定了模板方法后面的执行步骤,也就是程序接下来走向,这样一来,程序就拥有了变化的可能。

在这个例子里,我们把挂钩的名字定为customerWantsCondiments,接下来将挂钩放入Beverage类,看看我们如何得到一杯不需要糖和牛奶的咖啡:

var Beverage= function(){};
Beverage.prototype.boilWater = function(){
   console.log('把水煮沸');
}

Beverage.prototype.brew = function(){
  throw new Error('子类必须重写brew方法')
}

Beverage.prototype.pourInCup = function(){
  throw new Error('子类必须重写pourInCup方法')
}

Beverage.prototype.addCondiments = function(){
  throw new Error('子类必须重写addCondiments方法')
}

Beverage.prototype.customerWantsCondiments = function(){
  return true;
}

Beverage.prototype.init = function(){
   this.boilWater();
   this.brew();
   this.pourInCup();
   if(this.customerWantsCondiments()){//如果挂钩返回true,则需要调料
      this.addCondiments();
   }
}

var CoffeeWithHook = function(){};
CoffeeWithHook.prototype = new Beverage();
CoffeeWithHook.prototype.brew = function(){
   console.log('用沸水冲泡咖啡');
}
CoffeeWithHook.prototype.pourInCup = function(){
   console.log('把咖啡倒进杯子')
}
CoffeeWithHook.prototype.addCondiments = function(){
    console.log('加糖和牛奶')
}

CoffeeWithHook.prototype.customerWantsCondiments = function(){
   return window.confirm('请问需要调料吗?');
}

var coffeeWithHook = new CoffeeWithHook();
coffeeWithHook.init();

11.6 好莱坞原则

学习完模板方法模式之后,我们要引入一个新的设计原则----著名的“好莱坞原则”。

好莱坞无疑是演员的天堂,但好莱坞也有很多找不到工作的新演员,许多新人演员在好莱坞把简历递给演艺公司之后就回家等待电话。有时候该演员等得不耐烦了,给演艺公司打电话询问情况,演艺公司往往这样回答“不要来找我,我会给你打电话”。

在设计中,这样的规则就称为好莱坞原则。在这一原则的指导下,我们允许底层组件将自己的挂钩到高层组件中,而高层组件会决定什么时候,以何种方式使用这些底层组件,高层组件对待底层组件的方式,跟演艺公司对待新人演员是一样的,都是“别调用我们,我们会调用你”

模板方法模式是好莱坞原则的一个典型使用场景,它与好莱坞原则的联系非常明显,当我们用模板方法模式编写一个程序时,就意味着子类放弃了对自己的控制权,而改为父类通知子类,哪些方法应该什么时候被调用。

除此之外,好莱坞原则常常应用于其它模式和场景,例如发布—订阅模式和回调函数。

l   发布—订阅模式

在发布—订阅模式中,发布者会把消息推送给订阅者,这取代了原先不断去fetch消息的形式。例如假设我们乘坐出租去一个不了解的地方,除了每5秒钟就问司机“是否到达目的地”之外,还可以在车上美美地睡上一觉,然后跟司机说好,等到了就叫醒你。这也相当于好莱坞原则中提到的“别调用我,我们会去调用你”

l   回调函数

在ajax异步请求中,由于不知道请求返回的具体时间,而通过轮询去判断是否返回数据,这显然是不理智的行为。所以我们通常会把接下来的操作放在回调函数中,传入发起ajax异步请求的函数。当数据在回调函数里,然后把主动权交给另外一个函数。至于回调函数什么时候被执行,则是另外一个函数控制。

11.7 真的需要“继承”吗?

模板方法模式是基于继承的一种设计模式,父类封装了子类的算法框架和方法的执行顺序,子类继承父类以后,父类通知子类执行这些方法,好莱坞原则很好地诠释了这种设计技巧,即高层组件调用底层组件。

我们通过模板方法模式,编写了一个Coffeeor Tea的例子。模板方法模式是为数不多的基于继承的设计模式,但javascript语言实际上没有提供真正的类式继承,继承是通过对象与对象之间的委托来实现的。也就是说,虽然我们在形式上借鉴了提供类式继承的语言,但本章学习到的模板方法模式并不十分正宗。而且在javascript这般灵活的语言中,实现这样一例子,是否真的继承这种重武器呢?

在好莱坞原则的指导之下,下面的这段代码可以达到和继承一样的效果

var Beverage =function(param) {
    var boilWater=function() {
        console.log('把水煮沸');
    };
    var brew= param.brew|| function() {
            throw new Error('必须传递brew方法')
        };
    var pourInCup= param.pourInCup||function() {
            throw new Error('必须传递pourInCup方法')
        };
    var addCondiments= param.addCondiments||function() {
            throw new Error('必须传递addCondiments方法');
        }
    var F=function() {
    };
    F.prototype.init=function() {
        boilWater();
        brew();
        pourInCup();
        addCondiments();
    }
    return F;
}

var Coffee=Beverage({
    brew: function () {
      console.log('用沸水冲泡咖啡')
    },
    pourInCup: function () {
      console.log('把咖啡倒塌杯子')
    },
    addCondiments: function () {
        console.log("加糖加奶");
    }
});

var Tea = Beverage({
    brew: function () {
        console.log('用沸水冲泡茶叶')
    },
    pourInCup: function () {
        console.log('把茶倒塌杯子')
    },
    addCondiments: function () {
        console.log("加柠檬");
    }
});

var  coffee=newCoffee();
coffee.init();

var tea=newTea();
tea.init();

11.8 小结

模板方法模式是一种典型的通过封装变化提高系统的扩展性的设计模式。在传统的面向对象语言中,一个运用了模板方法模式的程序中,子类的方法种类和执行顺序都是不变的,所以我们把这部分逻辑抽象到父类的模板方法里面。而子类的方法具体怎么实现则是可变的,于是我们把这总分变化的逻辑封装到子类中。通过增加新的子类,我们便能给系统增加新的功能,并不需要改动抽象父类以及其他子类,这也是符合开放—封闭原则的。

但在javascript中,我们很多时候都不需要依样画瓢地去实现一个模板方法模式,高阶函数是更好的选择。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ava实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),可运行高分资源 Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现的毕业设计&&课程设计(包含运行文档+数据库+前后端代码),Java实现
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。下面详细介绍C语言的基本概念和语法。 1. 变量和数据类型 在C语言中,变量用于存储数据,数据类型用于定义变量的类型和范围。C语言支持多种数据类型,包括基本数据类型(如int、float、char等)和复合数据类型(如结构体、联合等)。 2. 运算符 C语言中常用的运算符包括算术运算符(如+、、、/等)、关系运算符(如==、!=、、=、<、<=等)、逻辑运算符(如&&、||、!等)。此外,还有位运算符(如&、|、^等)和指针运算符(如、等)。 3. 控制结构 C语言中常用的控制结构包括if语句、循环语句(如for、while等)和switch语句。通过这些控制结构,可以实现程序的分支、循环和多路选择等功能。 4. 函数 函数是C语言中用于封装代码的单元,可以实现代码的复用和模块化。C语言中定义函数使用关键字“void”或返回值类型(如int、float等),并通过“{”和“}”括起来的代码块来实现函数的功能。 5. 指针 指针是C语言中用于存储变量地址的变量。通过指针,可以实现对内存的间接访问和修改。C语言中定义指针使用星号()符号,指向数组、字符串和结构体等数据结构时,还需要注意数组名和字符串常量的特殊性质。 6. 数组和字符串 数组是C语言中用于存储同类型数据的结构,可以通过索引访问和修改数组中的元素。字符串是C语言中用于存储文本数据的特殊类型,通常以字符串常量的形式出现,用双引号("...")括起来,末尾自动添加'\0'字符。 7. 结构体和联合 结构体和联合是C语言中用于存储不同类型数据的复合数据类型。结构体由多个成员组成,每个成员可以是不同的数据类型;联合由多个变量组成,它们共用同一块内存空间。通过结构体和联合,可以实现数据的封装和抽象。 8. 文件操作 C语言中通过文件操作函数(如fopen、fclose、fread、fwrite等)实现对文件的读写操作。文件操作函数通常返回文件指针,用于表示打开的文件。通过文件指针,可以进行文件的定位、读写等操作。 总之,C语言是一种功能强大、灵活高效的编程语言,广泛应用于各种领域。掌握C语言的基本语法和数据结构,可以为编程学习实践打下坚实的基础。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值