ホームページ >ウェブフロントエンド >jsチュートリアル >JavaScript 用テトリス - リファクタリング
プロジェクト構造を再構築する
主なプロジェクト構造は、元のアプリの名前を src に変更することです。これは、スクリプトとソース コードが少なくなることを意味します。もちろん、元々スクリプトのソース コードを保存していた app/src も src/scripts という名前に変更されました。
[root> |-- index.html : 入口 |-- js/ : 构建生成的脚本 |-- css/ : 构建生成的样式表 |-- lib/ : bower 引入的库 `-- src/ : 前端源文件 |-- less : 样式表源文件 `-- scripts : 脚本(es6)源文件
さらに、ベース スクリプトはモジュールに細分化され、再構築プロセス中に 2 つのサブディレクトリ、model と tetris が作成されました。
構造解析
リファクタリングの前に、主にいくつかのモジュールを分割してモデルディレクトリに配置する簡単な構造解析を実行しました。新しい関数のリファクタリングと作成のプロセス中に、関数クラスと補助クラスが配置される tetris ディレクトリが作成されました。ただし、最も重要な関数は依然として scrits/tetris.js にあります。
以下は、最初にモデルを分析したときに描いた絵です:
リファクタリング
プログラムを書くとき、リファクタリングは常に非常に必要ですが、非常にエラーが発生しやすい部分でもあります。テトリスの再構築プロセス全体は、ソース コードの作業ブランチのコミット ログから確認できます。
リファクタリングに関する最も重要な点は、ロジックではなくコード構造を変更することです。つまり、リファクタリングの各ステップでは、元のビジネス ロジックを確保することに基づいてコードを変更する必要があります。100% 達成可能ではありませんが、リファクタリング中に混乱しないように、この原則に従うよう最善を尽くす必要があります。プロセスで不可解なバグが発生します。この点については、『既存コードの設計を改善するためのリファクタリング』という書籍で言及されているはずです。
ロジックを変更せずにコードを変更するという原則が「既存コードの設計を改善するためのリファクタリング」という本の中で言及されているかどうかはわかりませんが、それでもこの本を読むことをお勧めします。開発においてリファクタリングは非常に重要な役割を果たしますが、リファクタリングのプロセスには多くのデザインパターンが含まれるため、デザインパターンも読み取る必要があります。
プライベートメンバー
リファクタリングプロセス中に、すべてのクラスにプライベートメンバー定義を追加しました。これの目的は、メンバーを使用する際にアクセスすべきではないメンバーに誤ってアクセスすることを避けるためです (通常は誤って書き換えることを指しますが、誤って値を設定するとエラーが発生する場合もあります)。
プライベート メンバーのトピックについては、ES5 でのプライベート メンバーの ES6 シンボル実装のシミュレーションで説明しました。ここでは、そのブログで紹介されている方法を使用せず、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); } }
モデル
スクリプト/モデルのみ 以下に実装されるいくつかのクラスは、データを格納するために使用されるフィールド (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 在大部分时间是不变的,则 Puzzle 绘制的时候需要的只是其中其中非空部分的列表,所以这里有一个比较好的业务逻辑是:在 Tetris::matrix 变化的时候,从它重新生成 Puzzle::fastened,由 Puzzle 绘制时使用。
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(); }
InfoPanel 和 CommandPanel
InfoPanel 主要用于积分和速度的管理,包括与用户的交互(UI)。CommandPanel 则是负责两个按钮事件的处理。
Tetris
说实在的,我仍然认为 Tetris 的代码有点复杂,还需要重构简化。不过尝试了一下之后发现这并不是一件很容易的事情,所以就留待后面的版本来处理了。
小结
このテトリス ゲームの再構築は単なる予備的な再構築であり、本来の目的はモデルを明確に定義することだけですが、業務処理も分割されます。モデル定義の目的は達成されましたが、事業分割はまだ満足のいくものではありません。
作業中の以前の 2 つのプロジェクトはどちらも TypeScript 1.8 を使用していました。TypeScript 1.8 にはいくつかの落とし穴がありますが、TypeScript の静的言語機能、特に静的チェックは大規模な JavaScript プロジェクトにとって依然として非常に役立ちます。 TypeScript はコード量を増やして JavaScript の柔軟性を低下させると思っていましたが、今回 ES6 を使用してテトリス ゲームを再構築することで、少なくともこの問題は解決できると深く感じました。 JavaScript でのいくつかの質問:
静的検査では、実行時ではなく開発段階で多くの潜在的な問題が検出されます。ご存知のとおり、問題は早期に発見されるほど、修正が容易になります。