>웹 프론트엔드 >JS 튜토리얼 >JavaScript용 테트리스 - 리팩터링됨

JavaScript용 테트리스 - 리팩터링됨

高洛峰
高洛峰원래의
2016-10-14 09:39:091568검색

프로젝트 구조 재구성

메인 프로젝트 구조는 원래 앱의 이름을 src로 바꾸는 것인데, 이는 스크립트와 소스 코드가 적다는 것을 의미합니다. 물론 원래 스크립트 소스 코드를 저장했던 app/src도 src/scripts로 이름이 바뀌었습니다.

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

또한 기본 스크립트는 모듈로 세분화되어 있으며 재구성 과정에서 모델과 테트리스라는 두 개의 하위 디렉터리가 생성되었습니다.

구조 분석

리팩토링 전, 주로 여러 모듈을 나누어 모델 디렉터리에 배치하는 간단한 구조 분석을 수행했습니다. 리팩토링하고 새로운 기능을 작성하는 과정에서 기능적 클래스와 보조 클래스가 배치되는 tetris 디렉터리가 생성되었습니다. 그러나 가장 중요한 기능은 여전히 ​​scrits/tetris.js에 있습니다.

다음은 모델을 처음 분석할 때 그린 그림입니다.

JavaScript용 테트리스 - 리팩터링됨

리팩토링

프로그램을 작성할 때 리팩토링은 항상 매우 필요하지만 오류가 발생하기 쉬운 부분이기도 합니다. 테트리스의 전체 재구성 과정은 소스 코드의 작업 브랜치 커밋 로그에서 확인할 수 있습니다.

리팩토링에서 가장 중요한 점은 코드 구조를 변경하는 것이지만 로직을 변경하는 것은 아닙니다. 즉, 리팩토링의 모든 단계는 원래의 비즈니스 로직을 보장하는 기반으로 코드를 수정해야 합니다. 비록 100% 달성할 수는 없지만 리팩토링 중에 혼동되지 않도록 이 원칙을 따르도록 최선을 다해야 합니다. 설명할 수 없는 BUG가 발생합니다. 이 점은 『기존 코드 디자인 개선을 위한 리팩토링』이라는 책에서 언급할 만하다.

"기존 코드의 디자인 개선을 위한 리팩토링"이라는 책에서 로직을 바꾸지 않고 코드를 바꾸는 원리가 언급되어 있는지는 잘 모르겠지만, 그래도 이 책을 꼭 읽어보시길 권합니다. 리팩토링은 개발에 있어 매우 중요한 역할을 하지만, 리팩토링 과정에는 많은 디자인 패턴이 포함되므로 디자인 패턴도 읽어야 합니다.

비공개 멤버

리팩토링 과정에서 모든 클래스에 비공개 멤버 정의를 추가했습니다. 사용시 접근하면 안되는 멤버에 실수로 접근하는 것을 방지하기 위한 목적입니다.

Private 멤버 주제에 대해서는 ES5에서 Private 멤버의 ES6 기호 구현 시뮬레이션에서 논의한 적이 있습니다. 여기서는 해당 블로그에서 언급한 방법을 사용하지 않고 Symbol을 직접 사용했습니다. Babel은 Symbol()에 대한 호환성 처리를 수행했습니다. Symbol을 지원하는 브라우저에서는 ES6 Symbol이 직접 사용되며, 지원되지 않으면 Babel에서 구현된 시뮬레이션된 Symbol이 대신 사용됩니다.

비공개 멤버를 추가하는 코드는 다음과 같은 간단한 Point 클래스 코드와 같이 약간 이상해 보입니다. 다음 구현은 주로 Point 객체가 생성되면 해당 좌표가 마음대로 변경될 수 없도록 (가능한 한 많이) 보장하기 위한 것입니다. 즉, 불변입니다.

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용 테트리스 - 리팩터링됨

모델

스크립트/모델만 아래에 구현된 몇 가지 클래스는 데이터를 저장하는 데 사용되는 필드(Field ) 및 데이터 액세스를 위한 속성, 메서드는 데이터 액세스에도 사용됩니다.

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를 사용하여 Tetris 게임을 재구성하면서 이것이 TypeScript의 단점이 전혀 아니라는 것을 깊이 느꼈습니다. 몇 가지 질문이 있습니다.

정적 검사는 런타임이 아닌 개발 단계에서 많은 잠재적인 문제를 발견할 수 있습니다. 아시다시피, 문제를 조기에 발견할수록 문제를 해결하기가 더 쉽습니다.


성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.