前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【4/25】在页面对象中启用模板方法模式(Template Method Pattern)

【4/25】在页面对象中启用模板方法模式(Template Method Pattern)

作者头像
LIYI
发布2021-02-23 16:06:10
8070
发布2021-02-23 16:06:10
举报
文章被收录于专栏:艺述论专栏艺述论专栏

这是《小游戏从0到1设计模式重构》系列内容第4篇,所有源码及资料在“程序员LIYI”公号回复“小游戏从0到1”获取。

上一小节我们应用了组合模式,对记分板对象Board进行了容器改造,实际上在目前的小游戏项目中,容器绝不仅仅只有记分板,像游戏结束页(GameOverPage)、游戏主页(IndexPage)都应该是容器对象。这一小节我们在应用模板方法模式的同时,进一步应用组合模式。

首先看一下,在Game对象中,currentPage这个类变量统一代表GameOverPage和IndexPage,将在游戏运行中依次调用:init、start、run、render、end。模板方法模式要求在父类中定义流程的总体框架,在子类中实现具体的逻辑。现在我们可以在GameOverPage与IndexPage的基类Page中,实现需要这些由Game调用的基本方法,然后在这两个子页面中提供具体的实现。

前面我们提到,页面对象本应该是容器对象,在将页面对象应用模板方法模式时,可以稍带将它实现组合模式。先看一下Page类的改动:

代码语言:javascript
复制
// page/page.js
import Box from './box.js'
class Page extends Box {
  constructor(){
    super()
    // let game = GameGlobal.game
    // game.on('touchMove', (e)=>{
    //   // 仅在当前页传递事件
    //   if (GameGlobal.game.currentPage == this){
    //     this.touchMove.bind(this)(e)
    //   }
    // })
    // game.on('touchEnd', (e)=>{
    //   // 仅在当前页传递事件
    //   if (GameGlobal.game.currentPage == this){
    //     this.touchEnd.bind(this)(e)
    //   }
    // })
  }
  /// 触点移动事件回调函数
  touchMove(e) {
    return (GameGlobal.game.currentPage == this)
  }
  /// 触点结束事件回调函数
  touchEnd(e) {
    return (GameGlobal.game.currentPage == this)
  }
  init(options) { }
  start(){}
  run(){}
  // render(){}
  end(){}
}
export default Page

在Page类中,我们使Page继承于Box,使它成为一个容器,便于接下来在子类IndexPagek中添加子元素。还有,我们在Page类中添加start、run、end这些模板方法,render方法不需要添加了,因为它在Box中已经有了。得益于js的不严谨性,我们在Page中以一种不一样的返回值,重写了touchMove、touchEnd这两个方法,使其由不返回,改为返回布尔值。稍后我们在子类中会看到这个重写的作用。

再看一个子类IndexPage:

代码语言:javascript
复制
// page/index_page.js
...
import Page from './page.js'
/**
 * 主页
 */
class IndexPage extends Page {
  ...
  constructor() {
    super()
  }
  /// 初始化
  init(options) {
    ...
    this.addElement(this.bg)
      .addElement(this.leftPanel)
      .addElement(this.rightPanel)
      .addElement(this.ball)
      .addElement(this.systemBoard)
      .addElement(this.userBoard)
      .addElement(this.audioManager)
  }
  /// 渲染
  render() {
    // 清屏
    context.clearRect(0, 0, canvas.width, canvas.height)
    super.render()
    // // 背景
    // this.bg.render()
    // /// 绘制挡板
    // this.leftPanel.render()
    // this.rightPanel.render()
    // /// 绘制小球
    // this.ball.render()
    // /// 绘制分数
    // this.systemBoard.render()
    // this.userBoard.render()
    // /// 调用音效管理者实例的渲染方法
    // this.audioManager.render()
  }
  /// 运行
  run() {
    ...
  }
  /// 触点移动事件回调函数
  touchMove(e) {
    if (super.touchMove(e)){
      this.leftPanel.touchMove(e)
    }
  }
  /// 触点结束事件回调函数
  touchEnd(e) {
    if (super.touchEnd(e)){
      this.audioManager.touchEnd(e)
    }
  }
  ...
}
module.exports = IndexPage

我们看到,在IndexPage类的touchMove和touchEnd方法中,我们通过调用父类中的模板方法touchMove或touchEnd,获知了当前事件是否需要处理。这个地方充分体现了在模板方法模式中,父类中的方法完成的是一个模板,并不是一个完全需要被覆盖的“虚函数”。(注:js中没有虚函数,虚函数是C++等高级语言中的概念。虚函数是面向对象编程中实现多态功能的一个重要组成成分,虚函数在父类中定义,在子类中被继承和覆盖。)

我们再看一下GameOverPage的源码:

代码语言:javascript
复制
// page/game_over_page.js
...
import Page from './page.js'
/**
 * 游戏结束页面
 */
class GameOverPage extends Page {
  ...
  constructor() { 
    super()
  }
	// init(options) { }
  ...
  render() {
    super.render()
    ...
  }
  /// 触点移动事件回调函数
  // touchMove(e) { }

  /// 触点结束事件回调函数
  touchEnd(e) {
    if (super.touchEnd(e)){
      // 处理游戏结束单击屏幕的逻辑
      this.audioManager.playHitAudio()
      game.start()
    }
  }
  // 开始
  // start() { }
  // 结束
  // end() { }
}
module.exports = GameOverPage

应用模板方法模式,对GameOverPage的代码影响很小。

在IndexPage类中,我们在init方法中通过父类的addElement方法添加了很多子元素:

代码语言:javascript
复制
this.addElement(this.bg)
  .addElement(this.leftPanel)
  .addElement(this.rightPanel)
  .addElement(this.ball)
  .addElement(this.systemBoard)
  .addElement(this.userBoard)
  .addElement(this.audioManager)

这些子元素都需要继承于Component,以符合组合模式的要求。实现方法是类似的,仅举Backgroud类做为示例看一下:

代码语言:javascript
复制
// page/background.js
import Component from './Component'
/**
 * 背景对象
 */
class Background extends Component {
  ...
  // constructor() { }
  ...
}
const background = Background.getInstance()
module.exports = background

基本上就是引入Component基类,然后继承,其它代码不需要修改。

看一下运行效果,和之前没有什么区别:

最后总结一下,模板方法模式由两部分结构组成,一部分是抽象父类,另一部分是具体的子类。父类负责封装固定流程,子类负责实现具体逻辑。在这一小节的重构中,Page是模板方法模式中的父类,IndexPage与GameOverPage是模板中的子类。init、start、run、render和end这些方法,是在Game类中调用的模板方法,它们在Page类中定义,在IndexPage与GameOverPage这两个子类中有各自的重写实现。touchMove和touchEnd方法,不是Page类定义的,但它们也可以算作模板方法的一部分,并且充分体现了模板方法作为模板的意义,而不仅仅是作为一个父类中被重写的方法符号。

在ES6语法中有一个叫做模板字符串的语法 ,它可以看作是模板方法模式在字符串操作上的具体运用。看一个示例:

let s = "我是

在这个字符串中,ly与location是变量,通过${}这样的语法内嵌于字符串中。整个字符串文本可以看作是一个模板父本,而内嵌的变量可以看作是重写的子元素。模板字符串内在的实现思想与模板方法模式是相似的,我们在开发中也可以学其应用的灵活性,不必拘泥于父子类的形式。

阶段源码

本小节阶段源码见:disc/第五章/5.1.4。

我讲明白没有,欢迎提问。

2021年1月30日

本文视频:

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-01-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 艺述论 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 阶段源码
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档