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

169448949

共 4343字,需浏览 9分钟

 · 2021-01-30

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


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


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


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


// 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:


// 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的源码:


// 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方法添加了很多子元素:


this.addElement(this.bg)
.addElement(this.leftPanel)
.addElement(this.rightPanel)
.addElement(this.ball)
.addElement(this.systemBoard)
.addElement(this.userBoard)

.addElement(this.audioManager)


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


// page/background.js
import Component from './Component'
/**
* 背景对象
*/
class Background extends Component {
...
// constructor() { }
...
}
const background = Background.getInstance()

module.exports = background


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

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


babc82a04be57db38a4ed0fda2552f25.webp

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


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


let s = "我是${ly},来自${location}。"


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


阶段源码


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


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


2021年1月30日


本文频:


浏览 35
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报