首頁 web前端 js教程 JavaScript 版俄羅斯方塊-重構

JavaScript 版俄羅斯方塊-重構

Oct 14, 2016 am 09:39 AM

重建專案結構

專案結構上主要是將原來的 app 更名為 src,表示腳本和 less 原始碼都在這裡。當然原來存放腳本原始碼的 app/src 也相更名為src/scripts。

[root>
  |-- index.html    : 入口
  |-- js/           : 构建生成的脚本
  |-- css/          : 构建生成的样式表
  |-- lib/          : bower 引入的库
  `-- src/          : 前端源文件
        |-- less    : 样式表源文件
        `-- scripts : 脚本(es6)源文件
登入後複製

除此這外,基 scripts 中細分了模組,在重建的過程中創建了 model 和 tetris 兩個子目錄。

結構分析

重建之前先進行了簡單的結構分析,主要是將幾個模組劃分出來,放在 model 目錄下。在重構和寫入新功能的過程中創建了 tetris 目錄,這裡放的是功能類別和輔助類別。然而最主要的功能還是在 scrits/tetris.js 中。

下面是一開始分析模型時畫的圖:

JavaScript 版俄羅斯方塊-重構

重構

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

關於重構,最重要的一點是:改變程式碼結構,但不改變邏輯。也就是說,每一步重構都要在確保原有業務邏輯的基礎上對程式碼進行修改──雖然並不是100% 能達到,但要盡力遵循這個原則,才不會在重構的過程中產生莫名其妙的BUG。關於這一點,應該是在《重建 改善既有程式碼的設計》一書中提到的。

雖然不確定改代碼不改邏輯的原則是在 《重構 改善既有代碼的設計》 這本書中提到的,但是這本書還是推薦大家去看一看。重構對於開發有著很重要的作用,不過重構過程中牽涉到許多設計模式,所以設計模式也是需要讀一讀的。

私有成員

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

關於私有成員這個主題,我曾在 ES5 中模擬 ES6 的 Symbol 實作私有成員 中討論過。在這裡我沒有用那篇部落格中提到的方法,而是直接使用了 Symbol。 Babel 對 Symbol() 做了相容處理,如果是在支援 Symbol 的瀏覽器上,會直接使用 ES6 的 Symbol;不支援的,則用 Babel 實作的一個模擬的 Symbol 取代。

加入了私有化成員的程式碼看起來有些奇怪,像是下面這個簡單的 Point 類別的程式碼。以下的實作主要是為了(盡可能)保證 Point 物件一但生成,其座標就不能隨意改變──也就是 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);
    }
}
登入後複製

JavaScript 版俄羅斯方塊-重構

Models

只有 scripts/model 下面實現的幾個類別是比較純粹的模型,除了用於儲存資料的欄位(Field)和存取資料的屬性(Property)之外,方法也都是用於存取資料的。

Point 和 BlockPoint,继承

model/point.js 和 model/blockpoint.js 里分别实现了用于描述点(小方块)的两个类,区别仅仅在于 BlockPoint 多一个颜色属性。实际上 BlockPoint 是 Point 的子类。在 ES6 里实现继承太容易了,下面是这两个类的结构示意

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

继氶的实现关键就两点需要注意:

通过 extends 关键字实现继承

如果子类中定义了构造函数 constructor,记得第一句话一定要调用父类的构造函数 super(...)。Javaer 应该很熟悉这个要求的。

Form

Form 在这里不是“表单”的意思,而是“形状、外形”的意思,表示一个方块图形(Shape)通过旋转形成的最多4 种形态,每个 Form对象是其中一种。所以 Form 其实是一组 Point 组成的。

上一个版本中没有定义 Form 这个数据结构,是在生成 Shape 的时候生成的匿名对象。那段代码看起来特别绕,虽然也可以提取个函数出来,不过现在通过 Form 类的构造函数来生成,不仅达到了同样的目的,也把 width 的 height 封装起来了。

Shape 和 SHAPES

Shape 和 SHAPES 跟原来区别不大。SHAPES 的生成代码通过定义 Form 类,简化了不少。而 Shape 类在构建后,也由于成员私有化的原因,color 和 forms 不能被改变了,只能获取。

Tetris 中的游戏相关类

除了几个比较纯粹的模型类放在 model 中,主要入口 index.js 和 tetris.js 放在脚本源码根目录下,其它的游戏相关类都是放在tetris 目录下的。这只是用包(Java概念)或命名空间(C++/C#概念)的概念对源码进行了一个基本的划分。

Block 和 BlockFactory

Block 表示一个大方块,是由四个小方块组成的大方块,它的原型(此原型非 JS 的 Prototype)就是 Shape。所以一个 Block 会有一个 Shape 原型的引用,同时保存着当前它的位置 position 和形态 formIndex,这两个属性在游戏过程中是可以改变的,直接影响着 Block 最终绘制出来的位置和样子。

整有游戏中其实只有两个 Block,一个在预览区中,另一个在游戏区定时下落并被玩家操作。

Block 对象下落到底之后就不再是 Block 了,它会被固化在游戏区。为什么要这样设计呢?因为 Block 表示的是一个完整的大方块,而游戏区下方的方块一旦填满一行就会被消除,大方块将再也不完整。这种情况有两个方案可以描述:

仍然以大方块对象放在那里,但是标记已被消除的块,这样在绘制的时候就可以不绘制已消除的块。

大方块下落完成之后就将其打散成一个个的 BlockPoint,通过矩阵管理。

很明显,第二种方法通过二维数组实现,会更直观,程序写起来也会更简单。所以我选用了第二种方法。

Block 除了描述大方块的位置和形态之外,也会配合游戏控制进行一些数据运算和变化,比如位置的变化:moveLeft()、moveRight()、moveDown() 等,以及形态的变化 rotate();还有几个 fastenXxxx 方法,生成BlockPoint[] 用于绘制或判断下一个位置是否可以放置。关于这一点,在 JavaScript 版俄罗斯方块 中已经谈过。

BlockFactory 功能未变,仍然是产生一个随机方块。

Puzzle 和 Matrix

之前对 Puzzle 和 Matrix 的定义有点混淆,这里把它们区分开了。

Puzzle 用于绘制浏览区和预览区,它除了描述一个指定长宽的绘制区域之外,还有存储着两个重要的对象,block: Block 和fastened: BlockPoint[],也就是上面提到的运动中的方块,和固定下来的若干小方块。

Puzzle 本向不维护 block 和 fastened,但它要绘制这两个重要数据对象中的所有 BlockPoint。

Matrix 不再是一个类,它是两个数据。一个是 Puzzle 中的 matrix 属性,维护着由

(行) 和 (单元) 组成的绘制区;另一个是 Tetris 中的 matrix 属性,维护着一个 BlockPoint 的矩阵,也就是 Puzzle::fastened 的矩阵形态,它更容易通过固化或删除等操作来改变。

由于 Tetris::matrix 在大部分时间是不变的,则 Puzzle 绘制的时候需要的只是其中其中非空部分的列表,所以这里有一个比较好的业务逻辑是:在 Tetris::matrix 变化的时候,从它重新生成 Puzzle::fastened,由 Puzzle 绘制时使用。

JavaScript 版俄羅斯方塊-重構

Eventable

在重构和写新功能的过程中,发现了事件的重要性,好些处理都会用到事件。

比如在点击暂停/恢复 和 重新开始 的时候,需要去判断当前游戏的状态,并根据状态的情况来触发到底是不是真的暂停或重新开始。

又比如,在计分和速度选择功能中,如果计分达到一定程度,就需要触发提速。

上面提到的这些都可以使用观察者模式来设计,则事件就是观察者模式的一个典型实现。要实现自己的事件处理机制其实不难,但是这里可以偷偷懒,直接借用 jQuery 的事件处理,所以定义了 Eventable 类用于封装 jQuery 的事件处理,所有支持事件的业务类都可以从它继承。

封装很简单,这里采用的是封装事件代理对象的方式,具体可以看源代码,一共只有 20 多行,很容易懂。也可以在构造函数中把this 封装一个 jQuery 对象出来代理事件处理,这种方式可以将事件处理函数中的 this 指向自己(自己指 Eventable 对象)。不过还好,这个项目中不需要关心事件处理函数中的 this。

StateManager

在实现 Tetris 中的主要游戏逻辑的时候,发现状态管理并不简单,尤其是加了 暂停/恢复 按钮之后,暂停状态就分为代码暂停和人工暂停两种情况,对于两种情况的恢复操作也是有区别的。除此之外还有游戏结束的状态……所以干脆就定义个 StateManager 来管理状态了。

StateManager 维护着游戏的状态,提供改变状态的方法,也提供判断状态的属性。如果 JavaScript 有接口语法的话,这个接口大概是这样的

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

    pause(byWhat);
    resume(byWhat);
    start();
    over();
}
登入後複製

JavaScript 版俄羅斯方塊-重構

InfoPanel 和 CommandPanel

InfoPanel 主要用于积分和速度的管理,包括与用户的交互(UI)。CommandPanel 则是负责两个按钮事件的处理。

Tetris

说实在的,我仍然认为 Tetris 的代码有点复杂,还需要重构简化。不过尝试了一下之后发现这并不是一件很容易的事情,所以就留待后面的版本来处理了。

小结

這次對俄羅斯方塊遊戲的重構只是一個初步的重構,最初的目的只是想把模型定義清楚,不過也對業務處理進行了一些拆分。模型定義的目的是達到了,但是業務拆分仍然不滿意。

工作上之前的兩個專案都是使用的 TypeScript 1.8,雖然是 TypeScript 1.8 有一些坑在那裡,但是 TypeScript 的靜態語言特性,尤其是靜態檢查對大型 JavaScript 專案還是有很大幫助的。之前我一直認為TypeScript 增加了程式碼量,也降低了JavaScript 的靈活度,但這次用ES6 重構俄羅斯方塊遊戲讓我深深的感受到,這根本不是TypeScript 的缺點,它至少可以解決JavaScript 中的這幾個問題:

靜態檢查在開發階段就能發現很多潛在的問題,而不是在運作的時候才能發現問題。要知道,問題發現越早改起來越容易。


本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

Video Face Swap

Video Face Swap

使用我們完全免費的人工智慧換臉工具,輕鬆在任何影片中換臉!

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

前端熱敏紙小票打印遇到亂碼問題怎麼辦? 前端熱敏紙小票打印遇到亂碼問題怎麼辦? Apr 04, 2025 pm 02:42 PM

前端熱敏紙小票打印的常見問題與解決方案在前端開發中,小票打印是一個常見的需求。然而,很多開發者在實...

神秘的JavaScript:它的作用以及為什麼重要 神秘的JavaScript:它的作用以及為什麼重要 Apr 09, 2025 am 12:07 AM

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

誰得到更多的Python或JavaScript? 誰得到更多的Python或JavaScript? Apr 04, 2025 am 12:09 AM

Python和JavaScript開發者的薪資沒有絕對的高低,具體取決於技能和行業需求。 1.Python在數據科學和機器學習領域可能薪資更高。 2.JavaScript在前端和全棧開發中需求大,薪資也可觀。 3.影響因素包括經驗、地理位置、公司規模和特定技能。

JavaScript難以學習嗎? JavaScript難以學習嗎? Apr 03, 2025 am 12:20 AM

學習JavaScript不難,但有挑戰。 1)理解基礎概念如變量、數據類型、函數等。 2)掌握異步編程,通過事件循環實現。 3)使用DOM操作和Promise處理異步請求。 4)避免常見錯誤,使用調試技巧。 5)優化性能,遵循最佳實踐。

如何實現視差滾動和元素動畫效果,像資生堂官網那樣?
或者:
怎樣才能像資生堂官網一樣,實現頁面滾動伴隨的動畫效果? 如何實現視差滾動和元素動畫效果,像資生堂官網那樣? 或者: 怎樣才能像資生堂官網一樣,實現頁面滾動伴隨的動畫效果? Apr 04, 2025 pm 05:36 PM

實現視差滾動和元素動畫效果的探討本文將探討如何實現類似資生堂官網(https://www.shiseido.co.jp/sb/wonderland/)中�...

如何使用JavaScript將具有相同ID的數組元素合併到一個對像中? 如何使用JavaScript將具有相同ID的數組元素合併到一個對像中? Apr 04, 2025 pm 05:09 PM

如何在JavaScript中將具有相同ID的數組元素合併到一個對像中?在處理數據時,我們常常會遇到需要將具有相同ID�...

JavaScript的演變:當前的趨勢和未來前景 JavaScript的演變:當前的趨勢和未來前景 Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

console.log輸出結果差異:兩次調用為何不同? console.log輸出結果差異:兩次調用為何不同? Apr 04, 2025 pm 05:12 PM

深入探討console.log輸出差異的根源本文將分析一段代碼中console.log函數輸出結果的差異,並解釋其背後的原因。 �...

See all articles