国产gaysexchina男同gay,japanrcep老熟妇乱子伦视频,吃奶呻吟打开双腿做受动态图,成人色网站,国产av一区二区三区最新精品

JavaScript 版俄羅斯方塊——重構(gòu)

2018-06-08 13:54 更新

JavaScript 版俄羅斯方塊 中曾提到,因?yàn)榕R時(shí)起意,所以項(xiàng)目結(jié)構(gòu)和很多命名都比較混亂。另外,計(jì)分等功能也未實(shí)現(xiàn)。這次抽空實(shí)現(xiàn)計(jì)分和速度設(shè)置,并在此之前進(jìn)行了簡(jiǎn)單的重構(gòu)。

傳送門

重構(gòu)項(xiàng)目結(jié)構(gòu)

項(xiàng)目結(jié)構(gòu)上主要是將原來(lái)的 app 更名為 src,表示腳本和 less 源碼都在這里。當(dāng)然原來(lái)存放腳本源碼的 app/src 也相更名為 src/scripts。

[root>
  |-- index.html    : 入口
  |-- js/           : 構(gòu)建生成的腳本
  |-- css/          : 構(gòu)建生成的樣式表
  |-- lib/          : bower 引入的庫(kù)
  `-- src/          : 前端源文件
        |-- less    : 樣式表源文件
        `-- scripts : 腳本(es6)源文件

除此這外,基 scripts 中細(xì)分了模塊,在重構(gòu)的過(guò)程中創(chuàng)建了 modeltetris 兩個(gè)子目錄。

結(jié)構(gòu)分析

重構(gòu)之前先進(jìn)行了簡(jiǎn)單的結(jié)構(gòu)分析,主要是將幾個(gè)模塊劃分出來(lái),放在 model 目錄下。重構(gòu)和寫新功能的過(guò)程中創(chuàng)建了 tetris 目錄,這里放的是功能類和輔助類。然而最主要的功能還是在 scrits/tetris.js 中。

下面是一開(kāi)始分析模型時(shí)畫的圖:

重構(gòu)

寫程序,重構(gòu)總是非常需要但也非常容易出錯(cuò)的部分。俄羅斯方塊的整個(gè)重構(gòu)的過(guò)程從 源碼中 working 分支 的提交日志中可以看到。

關(guān)于重構(gòu),最重要的一點(diǎn)是:改變代碼結(jié)構(gòu),但不改變邏輯。也就是說(shuō),每一步重構(gòu)都要在保證原有業(yè)務(wù)邏輯的基礎(chǔ)上對(duì)代碼進(jìn)行修改——雖然并不是 100% 能達(dá)到,但要盡最大努力遵循這個(gè)原則,才不會(huì)在重構(gòu)的過(guò)程中產(chǎn)生莫名其妙的 BUG。關(guān)于這一點(diǎn),應(yīng)該是在《重構(gòu) 改善既有代碼的設(shè)計(jì)》一書中提到的。

雖然不確定改代碼不改邏輯的原則是在 《重構(gòu) 改善既有代碼的設(shè)計(jì)》 這本書中提到的,但是這本書還是推薦大家去看一看。重構(gòu)對(duì)于開(kāi)發(fā)有著很重要的作用,不過(guò)重構(gòu)過(guò)程中涉及到很多設(shè)計(jì)模式,所以設(shè)計(jì)模式也是需要讀一讀的。

私有成員

在重構(gòu)的過(guò)程中,我為所有類都加入了私有成員定義。這樣做的目的是避免在使用它們的時(shí)候,不小心訪問(wèn)了不該訪問(wèn)的成員(一般指不小心改寫,但有時(shí)候不小心取值也可能造成錯(cuò)誤)。

關(guān)于私有成員這個(gè)話題,我曾在 ES5 中模擬 ES6 的 Symbol 實(shí)現(xiàn)私有成員 中討論過(guò)。在這里我沒(méi)有用那篇博客中提到的方法,而是直接使用了 Symbol。Babel 對(duì) Symbol() 做了兼容處理,如果是在支持 Symbol 的瀏覽器上,會(huì)直接使用 ES6 的 Symbol;不支持的,則用 Babel 實(shí)現(xiàn)的一個(gè)模擬的 Symbol 代替。

加入了私有化成員的代碼看起來(lái)有些奇怪,比如下面這個(gè)簡(jiǎn)單的 Point 類的代碼。以下的實(shí)現(xiàn)主要是為了(盡可能)保證 Point 對(duì)象一但生成,其坐標(biāo)就不能隨意改動(dòng)——也就是 Immutable。

const __ = {
    x: Symbol("x"),
    y: Symbol("y")
};

export default class Point {
    constructor(x, y) {
        this[__.x] = x;
        this[__.y] = y;
    }

    get x() {
        return this[__.x];
    }

    get y() {
        return this[__.y];
    }

    move(offsetX = 0, offsetY = 0) {
        return new Point(this.x + offsetX, this.y + offsetY);
    }
}

這段代碼還好,在寫了很多 const __ = { ... } 之后,我突然覺(jué)得非常思念 TypeScript。在 TypeScript 中只需要簡(jiǎn)單的 private _x; 就可以申明私有成員。

TypeScript 中申明的私有成員僅限于靜態(tài)檢查,最終生成的 JavaScript 腳本中,這些成員都可以在外部訪問(wèn)。不過(guò)沒(méi)關(guān)系,因?yàn)殪o態(tài)檢查可以更好的幫我們規(guī)避錯(cuò)誤。

Models

只有 scripts/model 下面實(shí)現(xiàn)的幾個(gè)類是比較純粹的模型,除了用于存儲(chǔ)數(shù)據(jù)的字段(Field)和存取數(shù)據(jù)的屬性(Property)之外,方法也都是用于存取數(shù)據(jù)的。

Point 和 BlockPoint,繼承

model/point.jsmodel/blockpoint.js 里分別實(shí)現(xiàn)了用于描述點(diǎn)(小方塊)的兩個(gè)類,區(qū)別僅僅在于 BlockPoint 多一個(gè)顏色屬性。實(shí)際上 BlockPointPoint 的子類。在 ES6 里實(shí)現(xiàn)繼承太容易了,下面是這兩個(gè)類的結(jié)構(gòu)示意

class Point {
    constructor(x, y) {
        // ....
    }
}

class BlockPoint extends Point {
    constructor(x = 0, y = 0, c = "c0") {
        super(x, y);
        // ....
    }
}

繼氶的實(shí)現(xiàn)關(guān)鍵就兩點(diǎn)需要注意:

  1. 通過(guò) extends 關(guān)鍵字實(shí)現(xiàn)繼承

  2. 如果子類中定義了構(gòu)造函數(shù) constructor,記得第一句話一定要調(diào)用父類的構(gòu)造函數(shù) super(...)。Javaer 應(yīng)該很熟悉這個(gè)要求的。

Form

Form 在這里不是“表單”的意思,而是“形狀、外形”的意思,表示一個(gè)方塊圖形(Shape)通過(guò)旋轉(zhuǎn)形成的最多4 種形態(tài),每個(gè) Form 對(duì)象是其中一種。所以 Form 其實(shí)是一組 Point 組成的。

上一個(gè)版本中沒(méi)有定義 Form 這個(gè)數(shù)據(jù)結(jié)構(gòu),是在生成 Shape 的時(shí)候生成的匿名對(duì)象。那段代碼看起來(lái)特別繞,雖然也可以提取個(gè)函數(shù)出來(lái),不過(guò)現(xiàn)在通過(guò) Form 類的構(gòu)造函數(shù)來(lái)生成,不僅達(dá)到了同樣的目的,也把 widthheight 封裝起來(lái)了。

Shape 和 SHAPES

ShapeSHAPES 跟原來(lái)區(qū)別不大。SHAPES 的生成代碼通過(guò)定義 Form 類,簡(jiǎn)化了不少。而 Shape 類在構(gòu)建后,也由于成員私有化的原因,colorforms 不能被改變了,只能獲取。

Tetris 中的游戲相關(guān)類

除了幾個(gè)比較純粹的模型類放在 model 中,主要入口 index.jstetris.js 放在腳本源碼根目錄下,其它的游戲相關(guān)類都是放在 tetris 目錄下的。這只是用包(Java概念)或命名空間(C++/C#概念)的概念對(duì)源碼進(jìn)行了一個(gè)基本的劃分。

Block 和 BlockFactory

Block 表示一個(gè)大方塊,是由四個(gè)小方塊組成的大方塊,它的原型(此原型非 JS 的 Prototype)就是 Shape。所以一個(gè) Block 會(huì)有一個(gè) Shape 原型的引用,同時(shí)保存著當(dāng)前它的位置 position 和形態(tài) formIndex,這兩個(gè)屬性在游戲過(guò)程中是可以改變的,直接影響著 Block 最終繪制出來(lái)的位置和樣子。

整有游戲中其實(shí)只有兩個(gè) Block,一個(gè)在預(yù)覽區(qū)中,另一個(gè)在游戲區(qū)定時(shí)下落并被玩家操作。

Block 對(duì)象下落到底之后就不再是 Block 了,它會(huì)被固化在游戲區(qū)。為什么要這樣設(shè)計(jì)呢?因?yàn)?Block 表示的是一個(gè)完整的大方塊,而游戲區(qū)下方的方塊一旦填滿一行就會(huì)被消除,大方塊將再也不完整。這種情況有兩個(gè)方案可以描述:

  1. 仍然以大方塊對(duì)象放在那里,但是標(biāo)記已被消除的塊,這樣在繪制的時(shí)候就可以不繪制已消除的塊。

  2. 大方塊下落完成之后就將其打散成一個(gè)個(gè)的 BlockPoint,通過(guò)矩陣管理。

很明顯,第二種方法通過(guò)二維數(shù)組實(shí)現(xiàn),會(huì)更直觀,程序?qū)懫饋?lái)也會(huì)更簡(jiǎn)單。所以我選用了第二種方法。

Block 除了描述大方塊的位置和形態(tài)之外,也會(huì)配合游戲控制進(jìn)行一些數(shù)據(jù)運(yùn)算和變化,比如位置的變化:moveLeft()moveRight()、moveDown() 等,以及形態(tài)的變化 rotate();還有幾個(gè) fastenXxxx 方法,生成 BlockPoint[] 用于繪制或判斷下一個(gè)位置是否可以放置。關(guān)于這一點(diǎn),在 JavaScript 版俄羅斯方塊 中已經(jīng)談過(guò)。

BlockFactory 功能未變,仍然是產(chǎn)生一個(gè)隨機(jī)方塊。

Puzzle 和 Matrix

之前對(duì) Puzzle 和 Matrix 的定義有點(diǎn)混淆,這里把它們區(qū)分開(kāi)了。

Puzzle 用于繪制瀏覽區(qū)和預(yù)覽區(qū),它除了描述一個(gè)指定長(zhǎng)寬的繪制區(qū)域之外,還有存儲(chǔ)著兩個(gè)重要的對(duì)象,block: Blockfastened: BlockPoint[],也就是上面提到的運(yùn)動(dòng)中的方塊,和固定下來(lái)的若干小方塊。

Puzzle 本向不維護(hù) blockfastened,但它要繪制這兩個(gè)重要數(shù)據(jù)對(duì)象中的所有 BlockPoint。

Matrix 不再是一個(gè)類,它是兩個(gè)數(shù)據(jù)。一個(gè)是 Puzzle 中的 matrix 屬性,維護(hù)著由 <div>(行) 和 <span>(單元) 組成的繪制區(qū);另一個(gè)是 Tetris 中的 matrix 屬性,維護(hù)著一個(gè) BlockPoint 的矩陣,也就是 Puzzle::fastened 的矩陣形態(tài),它更容易通過(guò)固化或刪除等操作來(lái)改變。

由于 Tetris::matrix 在大部分時(shí)間是不變的,則 Puzzle 繪制的時(shí)候需要的只是其中其中非空部分的列表,所以這里有一個(gè)比較好的業(yè)務(wù)邏輯是:在 Tetris::matrix 變化的時(shí)候,從它重新生成 Puzzle::fastened,由 Puzzle 繪制時(shí)使用。

有點(diǎn)遺憾,寫此博文的時(shí)候發(fā)現(xiàn)重構(gòu)之后忘了實(shí)現(xiàn)這一優(yōu)化處理,仍然是在每次 Tetris::render 的時(shí)候都會(huì)去重新生成 Puzzle::fastened。不過(guò)沒(méi)關(guān)系,下個(gè)版本一定記得處理這個(gè)事情。

Eventable

在重構(gòu)和寫新功能的過(guò)程中,發(fā)現(xiàn)了事件的重要性,好些處理都會(huì)用到事件。

比如在點(diǎn)擊暫停/恢復(fù) 和 重新開(kāi)始 的時(shí)候,需要去判斷當(dāng)前游戲的狀態(tài),并根據(jù)狀態(tài)的情況來(lái)觸發(fā)到底是不是真的暫停或重新開(kāi)始。

又比如,在計(jì)分和速度選擇功能中,如果計(jì)分達(dá)到一定程度,就需要觸發(fā)提速。

上面提到的這些都可以使用觀察者模式來(lái)設(shè)計(jì),則事件就是觀察者模式的一個(gè)典型實(shí)現(xiàn)。要實(shí)現(xiàn)自己的事件處理機(jī)制其實(shí)不難,但是這里可以偷偷懶,直接借用 jQuery 的事件處理,所以定義了 Eventable 類用于封裝 jQuery 的事件處理,所有支持事件的業(yè)務(wù)類都可以從它繼承。

封裝很簡(jiǎn)單,這里采用的是封裝事件代理對(duì)象的方式,具體可以看源代碼,一共只有 20 多行,很容易懂。也可以在構(gòu)造函數(shù)中把 this 封裝一個(gè) jQuery 對(duì)象出來(lái)代理事件處理,這種方式可以將事件處理函數(shù)中的 this 指向自己(自己指 Eventable 對(duì)象)。不過(guò)還好,這個(gè)項(xiàng)目中不需要關(guān)心事件處理函數(shù)中的 this

StateManager

在實(shí)現(xiàn) Tetris 中的主要游戲邏輯的時(shí)候,發(fā)現(xiàn)狀態(tài)管理并不簡(jiǎn)單,尤其是加了 暫停/恢復(fù) 按鈕之后,暫停狀態(tài)就分為代碼暫停和人工暫停兩種情況,對(duì)于兩種情況的恢復(fù)操作也是有區(qū)別的。除此之外還有游戲結(jié)束的狀態(tài)……所以干脆就定義個(gè) StateManager 來(lái)管理狀態(tài)了。

StateManager 維護(hù)著游戲的狀態(tài),提供改變狀態(tài)的方法,也提供判斷狀態(tài)的屬性。如果 JavaScript 有接口語(yǔ)法的話,這個(gè)接口大概是這樣的

interface IStateManager {
    get isPaused(): boolean;
    get isPausedByManual(): boolean;
    get isRestartable(): boolean;
    get isOver(): boolean;

    pause(byWhat);
    resume(byWhat);
    start();
    over();
}

我又開(kāi)始想念 TypeScript 了

InfoPanel 和 CommandPanel

InfoPanel 主要用于積分和速度的管理,包括與用戶的交互(UI)。CommandPanel 則是負(fù)責(zé)兩個(gè)按鈕事件的處理。

Tetris

說(shuō)實(shí)在的,我仍然認(rèn)為 Tetris 的代碼有點(diǎn)復(fù)雜,還需要重構(gòu)簡(jiǎn)化。不過(guò)嘗試了一下之后發(fā)現(xiàn)這并不是一件很容易的事情,所以就留待后面的版本來(lái)處理了。

小結(jié)

這次對(duì)俄羅斯方塊游戲的重構(gòu)只是一個(gè)初步的重構(gòu),最初的目的只是想把模型定義清楚,不過(guò)也對(duì)業(yè)務(wù)處理進(jìn)行了一些拆分。模型定義的目的是達(dá)到了,但是業(yè)務(wù)拆分仍然不盡滿意。

工作上之前的兩個(gè)項(xiàng)目都是用的 TypeScript 1.8,雖然是 TypeScript 1.8 有一些坑在那里,但是 TypeScript 的靜態(tài)語(yǔ)言特性,尤其是靜態(tài)檢查對(duì)大型 JavaScript 項(xiàng)目還是有很大幫助的。之前一直認(rèn)為 TypeScript 增加了代碼量,也降低了 JavaScript 的靈活度,但這次用 ES6 重構(gòu)俄羅斯方塊游戲讓我深深的感受到,這根本不是 TypeScript 的缺點(diǎn),它至少可以解決 JavaScript 中的這幾個(gè)問(wèn)題:

  • 靜態(tài)檢查在開(kāi)發(fā)階段就能發(fā)現(xiàn)很多潛在的問(wèn)題,而不是在運(yùn)行的時(shí)候才能發(fā)現(xiàn)問(wèn)題。要知道,問(wèn)題發(fā)現(xiàn)得越早改起來(lái)越容易。

  • 編輯器(我用的 VSCode)的智能提示和自動(dòng)完成功能在 TypeScript 的嚴(yán)格語(yǔ)法下非常好用,一個(gè)點(diǎn)出來(lái)就知道哪些方法可以調(diào)用,哪些不能。而對(duì)于 JavaScript 這方面就要弱一些了,編輯器不是按語(yǔ)義來(lái)分析,而是看代碼中出現(xiàn)了哪些,這樣難免會(huì)出現(xiàn)寫代碼不小心對(duì)象和方法不匹配的情況。

所以,下個(gè)版本我準(zhǔn)備嘗試用 TypeScript 2.0 來(lái)改寫。

新篇來(lái)啦:JavaScript 版俄羅斯方塊——轉(zhuǎn)換為 TypeScript

傳送門

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)