【4/25】在頁面對(duì)象中啟用模板方法模式(Template Method Pattern)
這是《小游戲從0到1設(shè)計(jì)模式重構(gòu)》系列內(nèi)容第4篇,所有源碼及資料在“程序員LIYI”公號(hào)回復(fù)“小游戲從0到1”獲取。
上一小節(jié)我們應(yīng)用了組合模式,對(duì)記分板對(duì)象Board進(jìn)行了容器改造,實(shí)際上在目前的小游戲項(xiàng)目中,容器絕不僅僅只有記分板,像游戲結(jié)束頁(GameOverPage)、游戲主頁(IndexPage)都應(yīng)該是容器對(duì)象。這一小節(jié)我們?cè)趹?yīng)用模板方法模式的同時(shí),進(jìn)一步應(yīng)用組合模式。
首先看一下,在Game對(duì)象中,currentPage這個(gè)類變量統(tǒng)一代表GameOverPage和IndexPage,將在游戲運(yùn)行中依次調(diào)用:init、start、run、render、end。模板方法模式要求在父類中定義流程的總體框架,在子類中實(shí)現(xiàn)具體的邏輯。現(xiàn)在我們可以在GameOverPage與IndexPage的基類Page中,實(shí)現(xiàn)需要這些由Game調(diào)用的基本方法,然后在這兩個(gè)子頁面中提供具體的實(shí)現(xiàn)。
前面我們提到,頁面對(duì)象本應(yīng)該是容器對(duì)象,在將頁面對(duì)象應(yīng)用模板方法模式時(shí),可以稍帶將它實(shí)現(xiàn)組合模式。先看一下Page類的改動(dòng):
// page/page.js
import Box from './box.js'
class Page extends Box {
constructor(){
super()
// let game = GameGlobal.game
// game.on('touchMove', (e)=>{
// // 僅在當(dāng)前頁傳遞事件
// if (GameGlobal.game.currentPage == this){
// this.touchMove.bind(this)(e)
// }
// })
// game.on('touchEnd', (e)=>{
// // 僅在當(dāng)前頁傳遞事件
// if (GameGlobal.game.currentPage == this){
// this.touchEnd.bind(this)(e)
// }
// })
}
/// 觸點(diǎn)移動(dòng)事件回調(diào)函數(shù)
touchMove(e) {
return (GameGlobal.game.currentPage == this)
}
/// 觸點(diǎn)結(jié)束事件回調(diào)函數(shù)
touchEnd(e) {
return (GameGlobal.game.currentPage == this)
}
init(options) { }
start(){}
run(){}
// render(){}
end(){}
}
export default Page
在Page類中,我們使Page繼承于Box,使它成為一個(gè)容器,便于接下來在子類IndexPagek中添加子元素。還有,我們?cè)赑age類中添加start、run、end這些模板方法,render方法不需要添加了,因?yàn)樗贐ox中已經(jīng)有了。得益于js的不嚴(yán)謹(jǐn)性,我們?cè)赑age中以一種不一樣的返回值,重寫了touchMove、touchEnd這兩個(gè)方法,使其由不返回,改為返回布爾值。稍后我們?cè)谧宇愔袝?huì)看到這個(gè)重寫的作用。
再看一個(gè)子類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()
// /// 繪制分?jǐn)?shù)
// this.systemBoard.render()
// this.userBoard.render()
// /// 調(diào)用音效管理者實(shí)例的渲染方法
// this.audioManager.render()
}
/// 運(yùn)行
run() {
...
}
/// 觸點(diǎn)移動(dòng)事件回調(diào)函數(shù)
touchMove(e) {
if (super.touchMove(e)){
this.leftPanel.touchMove(e)
}
}
/// 觸點(diǎn)結(jié)束事件回調(diào)函數(shù)
touchEnd(e) {
if (super.touchEnd(e)){
this.audioManager.touchEnd(e)
}
}
...
}
module.exports = IndexPage
我們看到,在IndexPage類的touchMove和touchEnd方法中,我們通過調(diào)用父類中的模板方法touchMove或touchEnd,獲知了當(dāng)前事件是否需要處理。這個(gè)地方充分體現(xiàn)了在模板方法模式中,父類中的方法完成的是一個(gè)模板,并不是一個(gè)完全需要被覆蓋的“虛函數(shù)”。(注:js中沒有虛函數(shù),虛函數(shù)是C++等高級(jí)語言中的概念。虛函數(shù)是面向?qū)ο缶幊讨袑?shí)現(xiàn)多態(tài)功能的一個(gè)重要組成成分,虛函數(shù)在父類中定義,在子類中被繼承和覆蓋。)
我們?cè)倏匆幌翯ameOverPage的源碼:
// page/game_over_page.js
...
import Page from './page.js'
/**
* 游戲結(jié)束頁面
*/
class GameOverPage extends Page {
...
constructor() {
super()
}
// init(options) { }
...
render() {
super.render()
...
}
/// 觸點(diǎn)移動(dòng)事件回調(diào)函數(shù)
// touchMove(e) { }
/// 觸點(diǎn)結(jié)束事件回調(diào)函數(shù)
touchEnd(e) {
if (super.touchEnd(e)){
// 處理游戲結(jié)束單擊屏幕的邏輯
this.audioManager.playHitAudio()
game.start()
}
}
// 開始
// start() { }
// 結(jié)束
// end() { }
}
module.exports = GameOverPage
應(yīng)用模板方法模式,對(duì)GameOverPage的代碼影響很小。
在IndexPage類中,我們?cè)趇nit方法中通過父類的addElement方法添加了很多子元素:
this.addElement(this.bg)
.addElement(this.leftPanel)
.addElement(this.rightPanel)
.addElement(this.ball)
.addElement(this.systemBoard)
.addElement(this.userBoard)
.addElement(this.audioManager)
這些子元素都需要繼承于Component,以符合組合模式的要求。實(shí)現(xiàn)方法是類似的,僅舉Backgroud類做為示例看一下:
// page/background.js
import Component from './Component'
/**
* 背景對(duì)象
*/
class Background extends Component {
...
// constructor() { }
...
}
const background = Background.getInstance()
module.exports = background
基本上就是引入Component基類,然后繼承,其它代碼不需要修改。
看一下運(yùn)行效果,和之前沒有什么區(qū)別:

最后總結(jié)一下,模板方法模式由兩部分結(jié)構(gòu)組成,一部分是抽象父類,另一部分是具體的子類。父類負(fù)責(zé)封裝固定流程,子類負(fù)責(zé)實(shí)現(xiàn)具體邏輯。在這一小節(jié)的重構(gòu)中,Page是模板方法模式中的父類,IndexPage與GameOverPage是模板中的子類。init、start、run、render和end這些方法,是在Game類中調(diào)用的模板方法,它們?cè)赑age類中定義,在IndexPage與GameOverPage這兩個(gè)子類中有各自的重寫實(shí)現(xiàn)。touchMove和touchEnd方法,不是Page類定義的,但它們也可以算作模板方法的一部分,并且充分體現(xiàn)了模板方法作為模板的意義,而不僅僅是作為一個(gè)父類中被重寫的方法符號(hào)。
在ES6語法中有一個(gè)叫做模板字符串的語法 ,它可以看作是模板方法模式在字符串操作上的具體運(yùn)用。看一個(gè)示例:
let s = "我是${ly},來自${location}。"
在這個(gè)字符串中,ly與location是變量,通過${}這樣的語法內(nèi)嵌于字符串中。整個(gè)字符串文本可以看作是一個(gè)模板父本,而內(nèi)嵌的變量可以看作是重寫的子元素。模板字符串內(nèi)在的實(shí)現(xiàn)思想與模板方法模式是相似的,我們?cè)陂_發(fā)中也可以學(xué)其應(yīng)用的靈活性,不必拘泥于父子類的形式。
階段源碼
本小節(jié)階段源碼見:disc/第五章/5.1.4。
我講明白沒有,歡迎提問。
2021年1月30日
本文視頻:
