Ch5: 把相似的程式碼統合在一起
5.1 Unifying Similar classes
接續上一章,updateTile依然違反了許多規則,最顯著的就是"不要讓IF跟ELSE一起用"規則 引入isStony, isBoxy,可被理解為,像石頭一樣做動,像箱子一樣做動
function updateTile(x: number, y: number) {
if ((map[y][x].isStone() || map[y][x].isFallingStone())
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new FallingStone();
map[y][x] = new Air();
} else if ((map[y][x].isBox() || map[y][x].isFallingBox())
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new FallingBox();
map[y][x] = new Air();
} else if (map[y][x].isFallingStone()) {
map[y][x] = new Stone();
} else if (map[y][x].isFallingBox()) {
map[y][x] = new Box();
}
}
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new FallingStone();
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new FallingBox();
map[y][x] = new Air();
} else if (map[y][x].isFallingStone()) {
map[y][x] = new Stone();
} else if (map[y][x].isFallingBox()) {
map[y][x] = new Box();
}
}
interface Tile {
// ...
isStony(): boolean;
isBoxy(): boolean;
}
class Air implements Tile {
// ...
isStony() { return false; }
isBoxy() { return false; }
}
回傳一個常數的method我們叫constant method(常數方法)
我們可以合併兩個class是因為這兩個class共享了回傳不同常數的常數方法
Steps:
讓兩個class除了常數方法以外都相等
合併class (書上說很像分數加法,要先把分母變一樣再加起來)
先比較兩顆石頭類
class Stone implements Tile {
isAir() { return false; }
isFallingStone() { return false; }
isFallingBox() { return false; }
isLock1() { return false; }
isLock2() { return false; }
draw(g: CanvasRenderingContext2D,
x: number, y: number) {
// ...
}
moveVertical(dy: number) { }
isStony() { return true; }
isBoxy() { return false; }
moveHorizontal(dx: number) {
// ...
}
}
class FallingStone implements Tile {
isAir() { return false; }
isFallingStone() { return true; }
isFallingBox() { return false; }
isLock1() { return false; }
isLock2() { return false; }
draw(g: CanvasRenderingContext2D,
x: number, y: number) {
// ...
}
moveVertical(dy: number) { }
isStony() { return true; }
isBoxy() { return false; }
moveHorizontal(dx: number) {
}
}
- 把moveHorizontal一樣
- 用if(true){}包起來
- 個別加上condition
- 把各自缺的部份,補到對方去,這樣除了constant method之外都一樣了
class Stone implements Tile {
// ...
moveHorizontal(dx: number) {
if (map[playery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = this;
moveToTile(playerx + dx, playery);
}
}
}
class FallingStone implements Tile {
// ...
moveHorizontal(dx: number) {
}
}
class Stone implements Tile {
// ...
moveHorizontal(dx: number) {
if (true) {
if (map[playery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = this;
moveToTile(playerx + dx, playery);
}
}
}
}
class FallingStone implements Tile {
// ...
moveHorizontal(dx: number) {
if (true) { }
}
}
class Stone implements Tile {
// ...
moveHorizontal(dx: number) {
if (this.isFallingStone() === false) {
if (map[playery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = this;
moveToTile(playerx + dx, playery);
}
}
}
}
class FallingStone implements Tile {
// ...
moveHorizontal(dx: number) {
if (this.isFallingStone() === true) { }
}
}
class Stone implements Tile {
// ...
moveHorizontal(dx: number) {
if (this.isFallingStone() === false) {
if (map[playery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = this;
moveToTile(playerx + dx, playery);
}
}
else if (this.isFallingStone() === true) {
}
}
}
class FallingStone implements Tile {
// ...
moveHorizontal(dx: number) {
if (this.isFallingStone() === false) {
if (map[playery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = this;
moveToTile(playerx + dx, playery);
}
}
else if (this.isFallingStone() === true) {
}
}
}
- 接下來引入falling field
- 在constructor給falling賦值
- 修改isFallingStone
- 改成回傳falling field
- 在constructor吃入參數
class Stone implements Tile {
// ...
isFallingStone() { return false; }
}
class FallingStone implements Tile {
// ...
isFallingStone() { return true; }
}
class Stone implements Tile {
private falling: boolean;
constructor() {
this.falling = false;
}
// ...
isFallingStone() { return false; }
}
class FallingStone implements Tile {
private falling: boolean;
constructor() {
this.falling = true;
}
// ...
isFallingStone()
}
class Stone implements Tile {
// ...
isFallingStone() { return false; }
}
class FallingStone implements Tile {
// ...
isFallingStone() { return true; }
}
class Stone implements Tile {
// ...
isFallingStone() { return this.falling; }
}
class FallingStone implements Tile {
// ...
isFallingStone() { return this.falling; }
}
class Stone implements Tile {
private falling: boolean;
constructor(falling: boolean) {
this.falling = falling;
}
// ...
}
- 接下來compiler會報錯
- 修好他
/// ...
new Stone();
/// ...
/// ...
new FallingStone(true);
/// ...
/// ...
new Stone(false);
/// ...
/// ...
new Stone(true);
/// ...
- Before
- After
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new FallingStone();
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new FallingBox();
map[y][x] = new Air();
} else if (map[y][x].isFallingStone()) {
map[y][x] = new Stone();
} else if (map[y][x].isFallingBox()) {
map[y][x] = new Box();
}
}
class Stone implements Tile {
// ...
isFallingStone() { return false; }
moveHorizontal(dx: number) {
if (map[playery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = this;
moveToTile(playerx + dx, playery);
}
}
}
class FallingStone implements Tile {
// ...
isFallingStone() { return true; }
moveHorizontal(dx: number) { }
}
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Stone(true);
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new FallingBox();
map[y][x] = new Air();
} else if (map[y][x].isFallingStone()) {
map[y][x] = new Stone(false);
} else if (map[y][x].isFallingBox()) {
map[y][x] = new Box();
}
}
class Stone implements Tile {
constructor(private falling: boolean) { }
// ...
isFallingStone() { return this.falling; }
moveHorizontal(dx: number) {
if (this.isFallingStone() === false) {
if (map[playery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = this;
moveToTile(playerx + dx, playery);
}
} else if (this.isFallingStone() === true) {
}
}
}
In TypeScript … Constructors behave a little differently than in most languages. First, we can have only one constructor, and it is always called constructor. Second, putting public or private in front of a parameter to the constructor automatically makes an instance variable and assigns it the value of the argument. So the following are equivalent.
class Stone implements Tile {
private falling: boolean;
constructor(falling: boolean) {
this.falling = falling;
}
}
class Stone implements Tile {
constructor(
private falling: boolean) { }
}
回頭看一下發現moveHorizontal有if搭配else,是不是又想起哪幾招...
當把falling變成type code,就可以使出連續技 -> into enum type -> Replace type code with classes -> push code into classes
- Before
- 轉成enum
- Replace Type Code with Classes
- 發起push code into classes招數
- 把funciton通通塞進class,然後調整condition
/// ...
new Stone(true);
/// ...
new Stone(false);
/// ...
class Stone implements Tile {
constructor(private falling: boolean) { }
// ...
isFallingStone() {
return this.falling;
}
}
enum FallingState {
FALLING, RESTING
}
/// ...
new Stone(FallingState.FALLING);
/// ...
new Stone(FallingState.RESTING);
/// ...
class Stone implements Tile {
constructor(private falling: FallingState) { }
// ...
isFallingStone() {
return this.falling
=== FallingState.FALLING;
}
}
interface FallingState {
isFalling(): boolean;
isResting(): boolean;
}
class Falling implements FallingState {
isFalling() { return true; }
isResting() { return false; }
}
class Resting implements FallingState {
isFalling() { return false; }
isResting() { return true; }
}
new Stone(new Falling());
new Stone(new Resting());
class Stone implements Tile {
constructor(private falling:
FallingState) { }
// ...
isFallingStone() {
return this.falling.isFalling();
}
}
interface FallingState {
// ...
moveHorizontal(
tile: Tile, dx: number): void;
}
class Falling implements FallingState {
// ...
moveHorizontal(tile: Tile, dx: number) {
}
}
class Resting implements FallingState {
// ...
moveHorizontal(tile: Tile, dx: number) {
if (map[playery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = tile;
moveToTile(playerx + dx, playery);
}
}
}
class Stone implements Tile {
// ...
moveHorizontal(dx: number) {
this.falling.moveHorizontal(this, dx);
}
}
5.1.1 重構模式: Unify Similar Classes
描述:
當有兩個或多個class,只差在constant method,可以服用此藥方去整合他們
Unifying classes is great because having fewer classes usually means we uncover more structure.
程序
The first phase is to make all the non-basis methods equal. For each of these methods, perform these steps:
a. In the body of each version of the method, add an enclosing if (true) { } around the existing code.
b. Replace true with an expression calling all the basis methods and comparing their result to their constant values.
c. Copy the body of each version, and paste it with an else into all the other versions.
Now that only the basis methods are different, the second phase begins by introducing a field for each method in the basis and assigning its constant in the constructor.
Change the methods to return the new fields instead of the constants.
Compile to ensure that we have not broken anything yet.
For each class, one field at a time:
a Copy the default value of the field, and then make the default value a parameter.
b Go through the compiler errors, and insert the default value as an argument.
6 After all the classes are identical, delete all but one of the unified classes, and fix all the compile errors by switching to the remaining class.
5.2 整合簡單條件式
假設兩個分支做一樣的事情,用 OR (||) 整合起來
導入新方法drop(), rest()來設定新的falling field
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Stone(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Box(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isFallingStone()) {
map[y][x] = new Stone(new Resting());
} else if (map[y][x].isFallingBox()) {
map[y][x] = new Box(new Resting());
}
}
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Stone(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Box(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isFallingStone()) {
map[y][x].rest();
} else if (map[y][x].isFallingBox()) {
map[y][x].rest();
}
}
大部分的method是空的
interface Tile {
// ...
drop(): void;
rest(): void;
}
class Stone implements Tile {
// ...
drop() { this.falling = new Falling(); }
rest() { this.falling = new Resting(); }
}
class Flux implements Tile {
// ...
drop() { }
rest() { }
}
整合相同程式碼到同一個分支
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Stone(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Box(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isFallingStone()) {
map[y][x].rest();
} else if (map[y][x].isFallingBox()) {
map[y][x].rest();
}
}
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Stone(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Box(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isFallingStone()
|| map[y][x].isFallingBox()) {
map[y][x].rest();
}
}
5.2.1 Refactoring pattern: COMBINE IFS
if (expression1) {
// body
} else if (expression2) {
// same body
}
if ((expression1) || (expression2)) {
// body
}
5.3 整合複雜條件式
進一步發現第一個if只是把一塊變成石頭,把另外一塊變成空氣,可以用上一節的方法簡化
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Stone(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y + 1][x] = new Box(new Falling());
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
function updateTile(x: number, y:
number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
發現兩個body做的事情一樣,再一次整合
function updateTile(x: number, y:
number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
function updateTile(x: number, y:
number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()
|| map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
5.3.1 Using arithmetic rules for conditions
用數學的加法乘法法則來記憶AND跟OR,OR(||)可以當成加(+), AND(&)可以當成乘(*) 圖像記憶法,因為OR(||)有兩條槓,可以組成(+),AND(&)感覺就像有一個x藏在裡面
Commutative Laws:
a + b = b + a
a × b = b × a
Associative Laws:
(a + b) + c = a + (b + c)
(a × b) × c = a × (b × c)
Distributive Law:
a × (b + c) = a × b + a × c
(a + b) × c = a × c + a × b
5.3.2 Rule: USE PURE CONDITIONS
STATEMENT: Conditions should always be pure.
Pure意思沒有side effect,像讀檔案的時候最好是先讀了,再移動cursor。 這個規則來自於常見的command query 分離法則,commands指的是任何會產生side effect的事情,queries代表是pure的事情。 一個簡單的遵循法則是,只允許回傳void的method做有side effect的事情。
作者的目的是為了把get data跟改動data兩個動作分開來,也更好命名。
class Reader {
private data: string[];
private current: number;
readLine() {
this.current++;
return this.data[this.current] || null;
}
}
/// ...
let br = new Reader();
let line: string | null;
while ((line = br.readLine()) !== null) {
console.log(line);
}
class Reader {
private data: string[];
private current: number;
nextLine() {
this.current++;
}
readLine() {
return this.data[this.current] || null;
}
}
/// ...
let br = new Reader();
for (; br.readLine() !== null; br.nextLine()) {
let line = br.readLine();
console.log(line);
}
5.3.3 Applying condition arithmetic
function updateTile(x: number, y: number) {
if (map[y][x].isStony()
&& map[y + 1][x].isAir()
|| map[y][x].isBoxy()
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
function updateTile(x: number, y: number) {
if ((map[y][x].isStony()
|| map[y][x].isBoxy())
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
接著push || into classes
function updateTile(x: number, y: number) {
if ((map[y][x].isStony()
|| map[y][x].isBoxy())
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
function updateTile(x: number, y: number) {
if (map[y][x].canFall()
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
5.4 跨類別統合程式碼
Step1. 將行為放進Class
// 在更新磚塊的動作中,如果磚塊可以掉落做「掉落」
function updateTile(x: number, y: number) {
if (map[y][x].canFall()
&& map[y + 1][x].isAir()) {
map[y][x].drop();
map[y + 1][x] = map[y][x];
map[y][x] = new Air();
} else if (map[y][x].isFalling()) {
map[y][x].rest();
}
}
// 在磚塊的「更新」動作中,如果能掉落執行「掉落」
function updateTile(x: number, y: number) {
map[y][x].update(x, y)
}
interface Tile {
update(x: number, y: number): void;
}
class Air implements Tile {
update(x: number, y: number): {
// 空氣磚不需要對「掉落」有反應
};
}
class Stone implements Tile {
update(x: number, y: number): {
if (map[y+1][x].isAir()) {
// 掉落中
this.falling = new Falling();
map[y+1][x] = this;
map[y][x] = new Air();
} else if (this.falling.isFalling()) {
// 到底了
this.falling = new Resting();
}
};
}
從本來在「環境」判斷所來「何人」對其進行對應動作;改成在「角色」裡,判斷所處環境決定其行為
- Stone和Air都implement自Tile interface
- 即:Stone和Air都是一種Tile
Step2. 抽取「策略」
class Stone implements Tile {
update(x: number, y: number) {
if (map[y+1][x].isAir()) {
this.falling = new Failing()
map[y+1][x] = this
map[y][x] = new Air()
} else if (this.falling.isFalling()) {
this.falling = new Resting()
}
}
}
class Box implements Tile {
update(x: number, y: number) {
if (map[y+1][x].isAir()) {
this.falling = new Failing()
map[y+1][x] = this
map[y][x] = new Air()
} else if (this.falling.isFalling()) {
this.falling = new Resting()
}
}
}
class FallingStrategy {
update(tile, x: number, y: number) {
if (map[y+1][x].isAir()) {
this.falling = new Failing();
map[y+1][x] = tile;
map[y][x] = new Air();
} else if (this.falling.isFalling()) {
this.falling = new Resting()
}
}
}
class Stone implements Tile {
constructor(strategy: FallingStrategy)
update(x: number, y: number) {
this.strategy.update(this, x, y)
}
}
class Box implements Tile {
constructor(strategy: FallingStrategy)
update(x: number, y: number) {
this.strategy.update(this, x, y)
}
}
Step3. 僅在開頭使用if
class FallingStrategy {
update(x: number, y: number) {
if (map[y+1][x].isAir()) {
this.falling = new Failing()
map[y+1][x] = this
map[y][x] = new Air()
} else if (this.falling.isFalling()) {
this.falling = new Resting()
}
}
}
class FallingStrategy {
private drop(tile, x, y) {
map[y+1][x] = tile;
map[y][x] = new Air()
}
update(tile: Tile, x: number, y: number) {
this.falling = (map[y+1][x].isAir())
? new Falling()
: new Resting();
this.drop(tile, x, y)
}
}
以上這三個步驟,稱為「引入策略模式」
策略模式Strategy Pattern
- 很多模式都是策略模式的變形
- 若Strategy有attribute (variable而不是function)
會被稱為狀態模式State Pattern
- 若Strategy有attribute (variable而不是function)
- 變異(Variance)是策略模式的目的
- 使用策略模式的情境
- 希望在程式碼中引入「變異」
- 希望在各個class間統合行為
規則:「不要讓介面只有一個實作」
- 只有一個實作的介面並不會增加可讀性
- 「介面」代表的是有變化;如果沒有變化,抽取介面會徒增心智的負擔
抽象化增加了真正的複雜性,但減少了感知的複雜性
Abstraction trades an increase in real complexity for a decrease in perceived complexity
規則:「從實作提取介面」
- 將「抽取介面」延遲到真正需要的時候才進行
- 例如,想引入變異(Variance)的時候
class ArraySum {
private processor: SumProcessor;
constructor(accumulator: number) {
processor = new SumProcessor(accumulator)
}
process(arr: number[]) {
for (let i=0; i< arr.length; i++) {
this.processor.processElement(arr[i])
}
return this.processor.getAccumulator();
}
}
class SumPorcessor {
constructor(private accumulator: number) {}
getAccumulator() {
return this.accumulator;
}
processElement(e: number) {
this.accumulator += e;
}
}
class BatchProcessor {
constructor(private processor: ElementProcessor) {}
process(arr: number[]) {
for (let i=0; i<arr.length; i++) {
this.processor.processElement(arr[i]);
}
return this.processor.getAccumulator();
}
}
interface ElementProcessor {
getAccumulator(): number;
processElement(e: number): void;
}
class MinimumProcessor implements ElementProcessor {
getAccumulator() { return this.accumulator; }
processElement(e: number) {
// The variance
if (this.accumulator > e) {
this.accumulator = e;
}
}
}
class SumProcessor implements ElementProcessor {
getAccumulator() { return this.accumulator; }
processElement(e: number) {
// The variance
this.accumulator += e;
}
}
5.5 統合相同的函式
function removeLock1() {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
if (map[y][x].isLock1()) { // 只有這裡有差
map[y][x] = new Air();
}
}
}
}
function removeLock2() {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
if (map[y][x].isLock2()) { // 只有這裡有差
map[y][x] = new Air();
}
}
}
}
function remove(shouldRemove: RemoveStrategy) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
if (shouldRemove.check(map[y][x])) {
map[y][x] = new Air();
}
}
}
}
5.6 統合相似的程式碼
class Key1 implements Tile {
draw(g: CanvasRenderingContext2D, x: number, y: number) {
g.fillStyle = "#ffcc00";
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
moveHorizontal(dx: number) {
removeLock1();
moveToTile(playerx + dx, playery);
}
}
class Lock1 implements Tile {
isLock1() { return true; }
isLock2() { return false; }
draw(g: CanvasRenderingContext2D, x: number, y: number) {
g.fillStyle = "#ffcc00";
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
class Key2 implements Tile {
draw(g: CanvasRenderingContext2D, x: number, y: number) {
g.fillStyle = "#00ccff";
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
moveHorizontal(dx: number) {
removeLock2();
moveToTile(playerx + dx, playery);
}
}
class Lock2 implements Tile {
isLock1() { return false; }
isLock2() { return true; }
draw(g: CanvasRenderingContext2D, x: number, y: number) {
g.fillStyle = "#00ccff";
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
class KeyConfiguration { // 封裝Key, Lock會使用到的參數們
constructor(
private color: string,
private _1: boolean, // 若未來需要引入第3或第4個key/lock的組合,可以透過把boolena改成number實現
private removeStrategy: RemoveStrategy
) { }
getColor() { return this.color; }
is1() { return this._1; }
getRemoveStrategy() { return this.removeStrategy; }
}
class Key implements Tile { // 整合Key1與Key2,改以keyConf注入做出差別
constructor(private keyConf: KeyConfiguration) { }
isLock1() { return false; }
isLock2() { return false; } // ??? 幹嘛不也從this.keyConf.is1()拿?
draw(g: CanvasRenderingContext2D, x: number, y: number) {
g.fillStyle = this.keyConf.getColor();
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
moveHorizontal(dx: number) {
remove(this.keyConf.getRemoveStrategy()); // 引入策略模式
moveToTile(playerx + dx, playery);
}
}
class Lock implements Tile { // 整合Lock1, Lock2
constructor(private keyConf: KeyConfiguration) { }
isLock1() { return this.keyConf.is1(); }
isLock2() { return !this.keyConf.is1(); } // 因為不是Lock1就一定是Lock2,省下一個參數
draw(g: CanvasRenderingContext2D, x: number, y: number) {
g.fillStyle = this.keyConf.getColor();
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
moveHorizontal(dx: number) { }
}
5.6的重構,因為注意到key和lock這兩個class彼此是有關連的,刻意對他們做出耦合 (KeyConfiguration),下一章會繼續討論KeyConfiguration的整理
小結
- 當有類似的程式碼需要合併時,有三種方式可以辦到
- 統合相似的類別
- 合併ifs
- 引入策略模式
- 規則:「使用純條件式」,指出條件式不該存有副作用(Side Effect)
- 簡化條件式可以使用布林運算的算術簡化方式來處理
- 規則:「不要讓介面只有一個實作」
- 不要為了抽象化而抽象化
- 應該從既有的實作提取介面,在真正需要的時候才進行抽象處理