Ch4: 讓型別碼能運作
VCR
Ch3 的 Refactor 完的最後留下一連串的 if-else,讓 Ch4 來教你怎樣優雅的 Refactor 這些 if-else if-else 吧!
- Before EXTRACT METHODE
- After EXTRACT METHODE
function handleInputs() {
while (inputs.length > 0) {
let current = inputs.pop();
if (current === Input.RIGHT) moveHorizontal(1);
else if (current === Input.LEFT) moveHorizontal(-1);
else if (current === Input.DOWN) moveVertical(1);
else if (current === Input.UP) moveVertical(-1);
}
}
function handleInputs() {
while (inputs.length > 0) {
let current = inputs.pop();
handleInput(current);
}
}
function handleInput(input: Input) {
if (input === Input.RIGHT) moveHorizontal(1);
else if (input === Input.LEFT) moveHorizontal(-1);
else if (input === Input.DOWN) moveVertical(1);
else if (input === Input.UP) moveVertical(-1);
}
4.1 重構一個簡單的 if 陳述句
在 EXTRACT METHODE 的規則中,我們只能留下一大串 else if, 但也無法再使用 EXTRACT METHODE 而且也不符合 FIVE LINES,那該怎麼辦呢?
那就不要用!
4.1.1 NEVER USE if WITH else
- 不要寫死決策
- 把第三方資料類型映射到自己的資料類型
window.addEventListener("keydown", (e) => {
if (e.key === LEFT_KEY || e.key === "a") inputs.push(Input.LEFT);
else if (e.key === UP_KEY || e.key === "w") inputs.push(Input.UP);
else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(Input.RIGHT);
else if (e.key === DOWN_KEY || e.key === "s") inputs.push(Input.DOWN);
});
異味
早期綁定(early-binding)會阻止新增變更,只能透過修改 if 來進行變更。 後期綁定(late-binding)可以在執行程式最後才確定行為,比較理想。
意圖
為了避免使用 if 來決定程式流程,盡量使用物件,使用物件可以根據實例化的類別來決定執行程式碼。
參考
- 用類別替代型別碼 REPLACE TYPE CODE WITH CLASSES (4.1.3)
- 引入策略模式 INTRODUCE STRATEGY PATTERN (5.4.2)
4.1.2 套用規則
使用 interface 來替換 enum
- step 1
- step 2
- step 3
- step 4
- step 5
enum Input {
RIGHT,
LEFT,
UP,
DOWN,
}
interface Input2 {
isRight(): boolean;
isLeft(): boolean;
isUp(): boolean;
isDown(): boolean;
}
class Right implements Input2 {
isRight() { return true; }
isLeft() { return false; }
isUp() { return false; }
isDown() { return false; }
}
class Left implements Input2 { ... }
class Up implements Input2 { ... }
class Down implements Input2 { ... }
function handleInput(input: Input) {
if (input === Input.LEFT) moveHorizontal(-1);
else if (input === Input.RIGHT) moveHorizontal(1);
else if (input === Input.UP) moveVertical(-1);
else if (input === Input.DOWN) moveVertical(1);
}
function handleInput(input: Input2) {
if (input.isLeft()) moveHorizontal(-1);
else if (input.isRight()) moveHorizontal(1);
else if (input.isUp()) moveVertical(-1);
else if (input.isDown()) moveVertical(1);
}
window.addEventListener("keydown", (e) => {
if (e.key === LEFT_KEY || e.key === "a") inputs.push(Input.LEFT);
else if (e.key === UP_KEY || e.key === "w") inputs.push(Input.UP);
else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(Input.RIGHT);
else if (e.key === DOWN_KEY || e.key === "s") inputs.push(Input.DOWN);
});
window.addEventListener("keydown", (e) => {
if (e.key === LEFT_KEY || e.key === "a") inputs.push(new Left());
else if (e.key === UP_KEY || e.key === "w") inputs.push(new Up());
else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(new Right());
else if (e.key === DOWN_KEY || e.key === "s") inputs.push(new Down());
});
window.addEventListener("keydown", (e) => {
if (e.key === LEFT_KEY || e.key === "a") inputs.push(Input.LEFT);
else if (e.key === UP_KEY || e.key === "w") inputs.push(Input.UP);
else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(Input.RIGHT);
else if (e.key === DOWN_KEY || e.key === "s") inputs.push(Input.DOWN);
});
function handleInput(input: Input) {
if (input === Input.LEFT) moveHorizontal(-1);
else if (input === Input.RIGHT) moveHorizontal(1);
else if (input === Input.UP) moveVertical(-1);
else if (input === Input.DOWN) mmoveVertical(1);
}
window.addEventListener("keydown", (e) => {
if (e.key === LEFT_KEY || e.key === "a") inputs.push(new Left());
else if (e.key === UP_KEY || e.key === "w") inputs.push(new Up());
else if (e.key === RIGHT_KEY || e.key === "d") inputs.push(new Right());
else if (e.key === DOWN_KEY || e.key === "s") inputs.push(new Down());
});
function handleInput(input: Input) {
if (input.isLeft()) moveHorizontal(-1);
else if (input.isRight()) moveHorizontal(1);
else if (input.isUp()) moveVertical(-1);
else if (input.isDown()) moveVertical(1);
}
4.1.3 REPLACE TYPE CODE WITH CLASSES
描述
這個模式把 enum 轉換成 interface,把 enum 的值轉換成 class,這樣可以在每個值加入屬性,局部化該特定值得相關功能。 把 enum 轉換成 class 時,可以不需要考慮其他 enum 的值而把功能和資料結合在一起。 平常加入新的 enum 值,需要處理該 enum 所有地方的 if-else,或是 switch,而這個模式只需在 class 內實作新方法,不需要修改其他程式碼。
遇到需要檢查有關聯性的型別時 code,盡量都先轉換成 enum,再套用這個模式。
const SMALL = 33;
const MEDIUM = 37;
const LARGE = 42;
enum TShirtSizes {
SMALL = 33,
MEDIUM = 37,
LARGE = 42,
}
處理步驟
- 請引入一個暫時的 interface,取個臨時的名字。這個介面應該要含有 enum 的每個值所對應的方法。
- 建立與 enum 的每個值對應的類別,除了與該類別相對應的方法返回 true 之外,介面中的所有方法都應該返回 false。
- 將 enum 重新命名。這樣會讓使用原本 enum 的所有地方回傳執錯誤訊息。
- 把型别的舊名稱改成臨時名稱,並用新的方法替換相等性檢查。
- 將剩下的參照到 enum 值的位置都替換為實例化新類別的處理方式。
- 最後當不再回報錯誤時,把介面的名稱全都修改為永久使用的名稱。
範例
- Initial
- step 1
- step 2
- step 3
- step 4
- step 5
- step 6
- Final
enum TrafficLight {
RED,
YELLOW,
GREEN,
}
const CYCLE = [TrafficLight.RED, TrafficLight.GREEN, TrafficLight.YELLOW];
function updateCarForLight(current: TrafficLight) {
if (current === TrafficLight.RED) car.stop();
else car.drive();
}
interface TrafficLight2 {
isRed(): boolean;
isYellow(): boolean;
isGreen(): boolean;
}
class Red implements TrafficLight2 {
isRed() {
return true;
}
isYellow() {
return false;
}
isGreen() {
return false;
}
}
class Yellow implements TrafficLight2 {
isRed() {
return false;
}
isYellow() {
return true;
}
isGreen() {
return false;
}
}
class Green implements TrafficLight2 {
isRed() {
return false;
}
isYellow() {
return false;
}
isGreen() {
return true;
}
}
enum TrafficLight {
RED,
YELLOW,
GREEN,
}
enum RawTrafficLight {
RED,
YELLOW,
GREEN,
}
function updateCarForLight(current: TrafficLight) {
if (current === TrafficLight.RED) car.stop();
else car.drive();
}
function updateCarForLight(current: TrafficLight2) {
if (current.isRed()) car.stop();
else car.drive();
}
const CYCLE = [TrafficLight.RED, TrafficLight.GREEN, TrafficLight.YELLOW];
const CYCLE = [new Red(), new Green(), new Yellow()];
interface TrafficLight2 {
// ...
}
interface TrafficLight {
// ...
}
interface TrafficLight {
isRED(): boolean;
isYELLOW(): boolean;
isGREEN(): boolean;
}
class Red implements TrafficLight {
isRed() {
return true;
}
isYellow() {
return false;
}
isGreen() {
return false;
}
}
class Yellow implements TrafficLight {
isRed() {
return false;
}
isYellow() {
return true;
}
isGreen() {
return false;
}
}
class Green implements TrafficLight {
isRed() {
return false;
}
isYellow() {
return false;
}
isGreen() {
return true;
}
}
const CYCLE = [new Red(), new Yellow(), new Green()];
function updateCarForLight(current: TrafficLight) {
if (current.isRED()) car.stop();
else car.drive();
}
這次的重構是為了為面的鋪陳,後面的章節會針對 isXXX()這類的 method 進行重構。
4.1.4 PUSHING CODE INTO CLASSES
- Initial
- step 1
- step 2
- step 3
- Final
interface Input {
isRight(): boolean;
isLeft(): boolean;
isUp(): boolean;
isDown(): boolean;
}
class Right implements Input {
isRight() { return true; }
isLeft() { return false; }
isUp() { return false; }
isDown() { return false; }
}
class Left implements Input { ... }
class Up implements Input { ... }
class Down implements Input { ... }
function handleInput(input: Input) {
if (input.isLeft()) moveHorizontal(-1);
else if (input.isRight()) moveHorizontal(1);
else if (input.isUp()) moveVertical(-1);
else if (input.isDown()) moveVertical(1);
}
把 handleInput 放進 class 裡
class Right implements Input {
...
isRight() { return true; }
isLeft() { return false; }
isUp() { return false; }
isDown() { return false; }
handleInput(){
if (this.isLeft()) moveHorizontal(-1);
else if (this.isRight()) moveHorizontal(1);
else if (this.isUp()) moveVertical(-1);
else if (this.isDown()) moveVertical(1);
}
}
class Left implements Input { ... }
class Up implements Input { ... }
class Down implements Input { ... }
在 interface 裡加入 handleInput
interface Input {
isRight(): boolean;
isLeft(): boolean;
isUp(): boolean;
isDown(): boolean;
}
interface Input {
isRight(): boolean;
isLeft(): boolean;
isUp(): boolean;
isDown(): boolean;
handle(): void;
}
此時如果 new Right()
class Right implements Input {
...
isRight() { return true; }
isLeft() { return false; }
isUp() { return false; }
isDown() { return false; }
handleInput(){
if (false) moveHorizontal(-1);
else if (true) moveHorizontal(1);
else if (false) moveVertical(-1);
else if (false) moveVertical(1);
}
}
可以簡化成
class Right implements Input {
...
isRight() { return true; }
isLeft() { return false; }
isUp() { return false; }
isDown() { return false; }
handleInput(){
moveHorizontal(1);
}
}
因為是實作 interface 裡的 handle 所以可以廠 handleInput 改成 handle
interface Input {
isRight(): boolean;
isLeft(): boolean;
isUp(): boolean;
isDown(): boolean;
handle(): void;
}
class Right implements Input {
...
isRight() { return true; }
isLeft() { return false; }
isUp() { return false; }
isDown() { return false; }
handle(){
moveHorizontal(1);
}
}
interface Input {
// ...
handle(): void;
}
class Left implements Input {
// ...
handle() {
moveHorizontal(-1);
}
}
class Right implements Input {
// ...
handle() {
moveHorizontal(1);
}
}
class Up implements Input {
// ...
handle() {
moveVertical(-1);
}
}
class Down implements Input {
// ...
handle() {
moveVertical(1);
}
}
4.1.5 重構模式: PUSH CODE INTO CLASSES
描述
- REPLACE TYPE CODE WITH CLASSES 的延伸
- 功能與資料會聚集到相應的 class
- 意在消除 if-else
處理步驟
- 把原本的 function 複製到複製到相關的 class 裡。把複製好的 function 字眼移除。把 if 內的方法更換成 this, 因為使用 class 內的方法。
- 把該 method 新增到該 class 實作的 interface 中,並取稍微不一樣的名字,方便提示修改。
- 遍歷所有相關 class 中的新方法,移除多餘的 if-else
- 把 class 內新增的方法改成 interface 內相應的方法,代表我們在 class 內完成了實作
- 將原本 function 內的方法換成 interface 的方法
- Initial
- step 1
- step 2
- step 3
- step 4
- step 5
- Final
interface TrafficLight {
isRed(): boolean;
isYellow(): boolean;
isGreen(): boolean;
}
class Red implements TrafficLight {
isRed() {
return true;
}
isYellow() {
return false;
}
isGreen() {
return false;
}
}
class Yellow implements TrafficLight {
isRed() {
return false;
}
isYellow() {
return true;
}
isGreen() {
return false;
}
}
class Green implements TrafficLight {
isRed() {
return false;
}
isYellow() {
return false;
}
isGreen() {
return true;
}
}
function updateCarForLight(current: TrafficLight) {
if (current.isRed()) car.stop();
else car.drive();
}
interface TrafficLight {
// ...
updateCar(): void;
}
class Red implements TrafficLight {
// ...
updateCarForLight() {
if (this.isRed()) car.stop();
else car.drive();
}
}
class Yellow implements TrafficLight {
// ...
updateCarForLight() {
if (this.isRed()) car.stop();
else car.drive();
}
}
class Green implements TrafficLight {
// ...
updateCarForLight() {
if (this.isRed()) car.stop();
else car.drive();
}
}
- Before
- Because
- After
class Red implements TrafficLight {
// ...
updateCarForLight() {
if (this.isRed()) car.stop();
else car.drive();
}
}
class Yellow implements TrafficLight {
// ...
updateCarForLight() {
if (this.isRed()) car.stop();
else car.drive();
}
}
class Green implements TrafficLight {
// ...
updateCarForLight() {
if (this.isRed()) car.stop();
else car.drive();
}
}
class Red implements TrafficLight {
// ...
updateCarForLight() {
if (true) car.stop();
else car.drive();
}
}
class Yellow implements TrafficLight {
// ...
updateCarForLight() {
if (false) car.stop();
else car.drive();
}
}
class Green implements TrafficLight {
// ...
updateCarForLight() {
if (false) car.stop();
else car.drive();
}
}
class Red implements TrafficLight {
// ...
updateCarForLight() {
car.stop();
}
}
class Yellow implements TrafficLight {
// ...
updateCarForLight() {
car.drive();
}
}
class Green implements TrafficLight {
// ...
updateCarForLight() {
car.drive();
}
}
- Before
- After
class Red implements TrafficLight {
// ...
updateCarForLight() {
car.stop();
}
}
class Yellow implements TrafficLight {
// ...
updateCarForLight() {
car.drive();
}
}
class Green implements TrafficLight {
// ...
updateCarForLight() {
car.drive();
}
}
class Red implements TrafficLight {
// ...
updateCar() {
car.stop();
}
}
class Yellow implements TrafficLight {
// ...
updateCar() {
car.drive();
}
}
class Green implements TrafficLight {
// ...
updateCar() {
car.drive();
}
}
- Before
- After
function updateCarForLight(current: TrafficLight) {
if (current.isRed()) car.stop();
else car.drive();
}
function updateCarForLight(current: TrafficLight) {
current.updateCar();
}
interface TrafficLight {
isRed(): boolean;
isYellow(): boolean;
isGreen(): boolean;
updateCar(): void;
}
class Red implements TrafficLight {
isRed() {
return true;
}
isYellow() {
return false;
}
isGreen() {
return false;
}
updateCar() {
car.stop();
}
}
class Yellow implements TrafficLight {
isRed() {
return false;
}
isYellow() {
return true;
}
isGreen() {
return false;
}
updateCar() {
car.drive();
}
}
class Green implements TrafficLight {
isRed() {
return false;
}
isYellow() {
return false;
}
isGreen() {
return true;
}
updateCar() {
car.drive();
}
}
function updateCarForLight(current: TrafficLight) {
current.updateCar();
}
4.1.6 移除多餘的 function
- Before
- After
function handleInputs() {
while (inputs.length > 0) {
let current = inputs.pop();
handleInput(current);
}
}
function handleInput(input: Input) {
input.handle();
}
function handleInputs() {
while (inputs.length > 0) {
let input = inputs.pop();
input.handle();
}
}
4.1.7 重構模式: INLINE METHOD
描述
在上個範例中,我們使用重構方法 PUSH CODE INTO CLASSES 將 function 複製並新增到 class 裡面來進祥 refactor, 被複製剩下的 function 就是多餘的 function,這時候就可以使用 INLINE METHOD 來移除多餘的 function。
處理步驟
- 把 function 名稱暫時換掉,這時候會有錯誤訊息在所有使用該 function 的地方。
- 複製這個 function 的內容,並留意他的參數。
- 在錯誤訊息的地方把 function 內容貼上,並把參數對應到正確的位置。
- 沒有錯誤訊息時,移除多餘的 function。
範例
- Initial
- step 1
- step 2
function deposit(to: string, amount: number) {
let accountId = database.find(to);
database.updateOne(accountId, { $inc: { balance: amount } });
}
function transfer(from: string, to: string, amount: number) {
deposit(from, -amount);
deposit(to, amount);
}
- Befroe
- After
function deposit(to: string, amount: number) {
// ...
}
function deposit2(to: string, amount: number) {
// ...
}
- Before
- After
function transfer(from: string, to: string, amount: number) {
deposit(from, -amount);
deposit(to, amount);
}
function transfer(from: string, to: string, amount: number) {
let fromAccountId = database.find(from);
database.updateOne(fromAccountId, { $inc: { balance: -amount } });
let toAccountId = database.find(to);
database.updateOne(toAccountId, { $inc: { balance: amount } });
}
4.2 重構大型的if陳述句
if ONLY AT START
- 原本的程式碼
- 使用EXTRACT METHOD
function drawMap(g: CanvasRenderingContext2D) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
if (map[y][x] === Tile.FLUX)
g.fillStyle = "#ccffcc";
else if (map[y][x] === Tile.UNBREAKABLE)
g.fillStyle = "#999999";
else if (map[y][x] === Tile.STONE || map[y][x] === Tile.FALLING_STONE)
g.fillStyle = "#0000cc";
else if (map[y][x] === Tile.BOX || map[y][x] === Tile.FALLING_BOX)
g.fillStyle = "#8b4513";
else if (map[y][x] === Tile.KEY1 || map[y][x] === Tile.LOCK1)
g.fillStyle = "#ffcc00";
else if (map[y][x] === Tile.KEY2 || map[y][x] === Tile.LOCK2)
g.fillStyle = "#00ccff";
if (map[y][x] !== Tile.AIR && map[y][x] !== Tile.PLAYER)
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
}
function drawMap(g: CanvasRenderingContext2D) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
colorOfTile(g, x, y)
if (map[y][x] !== Tile.AIR && map[y][x] !== Tile.PLAYER)
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
}
function colorOfTile(g: CanvasRenderingContext2D, x: number, y: number) {
if (map[y][x] === Tile.FLUX)
g.fillStyle = "#ccffcc";
else if (map[y][x] === Tile.UNBREAKABLE)
g.fillStyle = "#999999";
else if (map[y][x] === Tile.STONE || map[y][x] === Tile.FALLING_STONE)
g.fillStyle = "#0000cc";
else if (map[y][x] === Tile.BOX || map[y][x] === Tile.FALLING_BOX)
g.fillStyle = "#8b4513";
else if (map[y][x] === Tile.KEY1 || map[y][x] === Tile.LOCK1)
g.fillStyle = "#ffcc00";
else if (map[y][x] === Tile.KEY2 || map[y][x] === Tile.LOCK2)
g.fillStyle = "#00ccff";
}
NEVER USE if WITH else
- 定義新介面
- 定義新的類別實作新介面
- 參考舊enum
- 舊enum改名以看到編譯錯誤的位置
interface Tile2 {
isFlux(): boolean;
isUnbreakable(): boolean;
isStone(): boolean;
//... <----------| enum的所有值都有其對應的方法
}
class Flux implements Tile2 {
isFlux() { return true; }
isUnbreakable() { return false; }
isStone() { return false; }
// ...
}
enum Tile {
AIR,
FLUX,
UNBREAKABLE,
PLAYER,
STONE, FALLING_STONE,
BOX, FALLING_BOX,
KEY1, LOCK1,
KEY2, LOCK2
}
enum RawTile {
AIR,
FLUX,
UNBREAKABLE,
PLAYER,
STONE, FALLING_STONE,
BOX, FALLING_BOX,
KEY1, LOCK1,
KEY2, LOCK2
}
- 本來的程式碼
- 使用新的方法取代本來的enum
function colorOfTile(g: CanvasRenderingContext2D, x: number, y: number) {
if (map[y][x] === Tile.FLUX)
g.fillStyle = "#ccffcc";
else if (map[y][x] === Tile.UNBREAKABLE)
g.fillStyle = "#999999";
else if (map[y][x] === Tile.STONE || map[y][x] === Tile.FALLING_STONE)
g.fillStyle = "#0000cc";
else if (map[y][x] === Tile.BOX || map[y][x] === Tile.FALLING_BOX)
g.fillStyle = "#8b4513";
else if (map[y][x] === Tile.KEY1 || map[y][x] === Tile.LOCK1)
g.fillStyle = "#ffcc00";
else if (map[y][x] === Tile.KEY2 || map[y][x] === Tile.LOCK2)
g.fillStyle = "#00ccff";
}
function colorOfTile(g: CanvasRenderingContext2D, x: number, y: number) {
if (map[y][x].isFlux())
g.fillStyle = "#ccffcc";
else if (map[y][x].isUnbreakable())
g.fillStyle = "#999999";
else if (map[y][x].isStone() || map[y][x].isFallingStone())
g.fillStyle = "#0000cc";
else if (map[y][x].isBox() || map[y][x].isFallingBox())
g.fillStyle = "#8b4513";
else if (map[y][x].isKey1() || map[y][x].isLock1())
g.fillStyle = "#ffcc00";
else if (map[y][x].isKey2() || map[y][x].isLock2())
g.fillStyle = "#00ccff";
}
- 剩下最後兩個錯誤
let map: Tile[][] = [
[2, 2, 2, 2, 2, 2, 2, 2],
[2, 3, 0, 1, 1, 2, 0, 2],
[2, 4, 2, 6, 1, 2, 0, 2],
[2, 8, 4, 1, 1, 2, 0, 2],
[2, 4, 1, 1, 1, 9, 0, 2],
[2, 2, 2, 2, 2, 2, 2, 2]
];
function remove(tile: Tile) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
if (map[y][x] === tile) {
map[y][x] = new Air();
}
}
}
}
這兩個錯誤需要特殊的處置,因此我們將逐一進行處理。
4.2.1 去除泛化通用性
- Listing 4.71 原本的程式碼
function remove(tile: Tile) {
for (let y = 0; y < map.length; y++) {
for let x = 0; x < map[y].length; x++) {
if (map[y][x] === tile) {
map[y][x] = new Air();
}
}
}
}
- 觀察本來的方法的使用方式
- 複製一份方法成remove2
- 使用方式
- 改名去參數
- 使用方法來檢查
/// ...
remove(new Lock1());
/// ...
remove(new Lock2());
/// ...
remove(new Lock1());
/// ...
remove(new Lock2());
/// ...
function remove2(tile: Tile) {
// ...
}
function remove2(tile: Tile) {
// ... for Lock1
}
function remove2(tile: Tile) {
// ... for Lock2
}
function removeLock1() {
for (let y = 0; y < map.length; y++) {
for let x = 0; x < map[y].length; x++) {
if (map[y][x] === Tile.LOCK1) {
map[y][x] = new Air();
}
}
}
}
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();
}
}
}
}
4.2.2 重構模式:特定化方法(SPECIALIZE METHOD)
我們天生希望讓程式碼泛化通用並且能重複使用,但這樣做可能會有問題,因為它會模糊責任的範圍,也表示程式碼可以從各種地方呼叫取用。 愈專用特定化的方法,被呼叫取用的地方就愈少,這意味著很快就會變得不再使用,反而可以移除掉。
處理步驟
- 複製我們想要特定化的方法。
- 將其中一個方法重新命名為新的固定名稱,然後刪除(或替換)我們要特定化的參數。
- 根據需要修正方法,使其沒有錯誤。
- 將舊的呼叫改為使用新的呼叫。
- 原本的程式碼
- 複製想要特定化的方法
- 重新命名為新的固定名稱,然後刪除我們要特定化的參數
- 修正方法,即使沒有錯誤
- 修改呼叫方式
function canMove(start: Tile, end: Tile, dx: number, dy: number)
{
return dx * abs(start.x - end.x)
=== dy * abs(start.y - end.y)
|| dy * abs(start.x - end.x)
=== dx * abs(start.y - end.y);
}
/// ...
if (canMove(start, end, 1, 0)) // Rook城堡
/// ...
if (canMove(start, end, 1, 1)) // Bishop主教
/// ...
if (canMove(start, end, 1, 2)) // Knight騎士
/// ...
function canMove(start: Tile, end: Tile, dx: number, dy: number)
{
return dx * abs(start.x - end.x)
=== dy * abs(start.y - end.y)
|| dy * abs(start.x - end.x)
=== dx * abs(start.y - end.y);
}
function canMove(start: Tile, end: Tile, dx: number, dy: number)
{
return dx * abs(start.x - end.x)
=== dy * abs(start.y - end.y)
|| dy * abs(start.x - end.x)
=== dx * abs(start.y - end.y);
}
function rookCanMove(start: Tile, end: Tile)
{
return 1 * abs(start.x - end.x)
=== 0 * abs(start.y - end.y)
|| 0 * abs(start.x - end.x)
=== 1 * abs(start.y - end.y);
}
function rookCanMove(start: Tile, end: Tile)
{
return abs(start.x - end.x)
=== 0
|| 0
=== abs(start.y - end.y);
}
if (rookCanMove(start, end))
4.2.3 只能用switch
本來使用enum索引來建立map,在資料庫或檔案中也會使用類似的方式來儲存資料。 在實務上,通常無法更改現有的外部資料來適應重構的處理。 不如建立一個新的函式,把我們從enum索引帶到新的類別。
- 引入transformTile
let rawMap: RawTile[][] = [
[2, 2, 2, 2, 2, 2, 2, 2],
[2, 3, 0, 1, 1, 2, 0, 2],
[2, 4, 2, 6, 1, 2, 0, 2],
[2, 8, 4, 1, 1, 2, 0, 2],
[2, 4, 1, 1, 1, 9, 0, 2],
[2, 2, 2, 2, 2, 2, 2, 2]
];
let map: Tile2[][];
function assertExhausted(x: never): never {
throw new Error("Unexpected object: " + x);
}
function transformTile(tile: RawTile) {
switch (tile) {
case RawTile.AIR: return new Air();
case RawTile.PLAYER: return new Player();
case RawTile.UNBREAKABLE: return new Unbreakable();
case RawTile.STONE: return new Stone();
case RawTile.FALLING_STONE: return new FallingStone();
case RawTile.BOX: return new Box();
case RawTile.FALLING_BOX: return new FallingBox();
case RawTile.FLUX: return new Flux();
case RawTile.KEY1: return new Key1();
case RawTile.LOCK1: return new Lock1();
case RawTile.KEY2: return new Key2();
case RawTile.LOCK2: return new Lock2();
default: assertExhausted(tile);
}
}
function transformMap() {
map = new Array(rawMap.length);
for (let y = 0; y < rawMap.length; y++) {
map[y] = new Array(rawMap[y].length);
for (let x = 0; x < rawMap[y].length; x++) {
map[y][x] = transformTile(rawMap[y][x]);
}
}
}
window.onload = () => {
transformMap();
gameLoop();
}
tansformMap符合五行程式碼。但tansformTile違反了五行程式碼,也違反了另一條「NEVER USE switch」
在TypeScript中... enum(最舉)是代表數字的名稱,例如在C#中,它不像在Java中是個類別。因此,我們不需要在數字和enum之間進行任何轉換,可以直接使用enum索引,就像在之前程式碼中一樣的用法。
4.2.4 規則:不要使用switch(NEVER USE switch)
陳述
除非每種情況都有預設值和返回,否則千萬不要使用switch。
(Never use switch unless you have no default and return in every case)
解說
switch有兩個方便之處,但有副作用
- switch支援default,可能忘記處理新的值,造成新的值落入了default
- switch有「貫穿邏輯(fall-through logic)」的特性,程式會一直執行所有的case值到遇到break。容易忘記加上break或沒有注意到break。
在TypeScript中...
Switch陳述句還是很有用的,因為可以讓編譯器檢查我們是否都映射了所有的enum值。我們需要引入一個「魔法函式」才能實現這項功能,但它是專屬TypeScript的功能,其做法超出了本書的範圍。幸運的是,這個函式永遠不會改變,而這個模式在TypeScript中一直都是有效的。
異味
在Martin Fowler的Refactoring中,switch被稱為程式碼異味。 switch所關注的程式上下脈絡是如何在「這裡」處理值X。 相反地,把功能推入類別中則是把焦點放在資料,也就是依照這個值(物件)處理情況X。 只關注於程式上下脈絡所表示的是把「不變性」進一步從其資料中移開,從而讓「不變性」變成全域化。
意圖
這項規則(NEVER USE switch)有個優雅的附帶作用,可以把switch轉成一連串的else if之後,就能轉換成類別,最後削除if條件式。
4.2.5 削除if
使用INLINE METHOD
- 原本的程式碼
- 把colorOfTile內聯(INLINE METHOD)成color
function colorOfTile(g: CanvasRenderingContext2D, x: number, y: number) {
if (map[y][x].isFlux())
g.fillStyle = "#ccffcc";
else if (map[y][x].isUnbreakable())
g.fillStyle = "#999999";
else if (map[y][x].isStone() || map[y][x].isFallingStone())
g.fillStyle = "#0000cc";
else if (map[y][x].isBox() || map[y][x].isFallingBox())
g.fillStyle = "#8b4513";
else if (map[y][x].isKey1() || map[y][x].isLocak1())
g.fillStyle = "#ffcc00";
else if (map[y][x].isKey2() || map[y][x].isLock2())
g.fillStyle = "#00ccff";
}
function colorOfTile(g: CanvasRenderingContext2D, x: numebr, y: number)
{
map[y][x].color(g);
}
interface Tile {
// ...
color(g: CanvasRenderingContext2D): void;
}
class Air implements Tile {
// ...
color(g: CanvasRenderingContext2D) {
}
}
class Flux implements Tile {
// ...
color(g: CanvasRenderingContext2D) {
g.fillStyle = "#ccffcc";
}
}
取代本來的colofOfTile
- 本來的程式碼
- 移除colorOfTile
function drawMap(g: CanvasRenderingContext2D) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
colorOfTile(g, x, y);
if (!map[y][x].isAir() && !map[y][x].isPlayer())
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
}
function colorOfTile(g: CanvasRenderingContext2D, x: number, y: number)
{
map[y][x].color(g);
}
function drawMap(g: CanvasRenderingContext2D) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
map[y][x].color(g);
if (!map[y][x].isAir() && !map[y][x].isPlayer())
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
}
// ... 移除colorOfTile()
4.3 處理程式碼重複的問題
如果想要冒一點險,可以跳過提取方法並將它內聯到下面的處理中,直接把程式碼移到類別中。但請確定已經提交過程式嗎。因為在出現問題時可以復原回到這個提交時間點。
- 原本的程式碼
- 從for迴圈提取
- 把程式碼移到類別中
function drawMap(g: CanvasRenderingContext2D) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
map[y][x].color(g);
if (!map[y][x].isAir() && !map[y][x].isPlayer())
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
}
}
function drawMap(g: CanvasRenderingContext2D) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
drawTile(g, x, y)
}
}
}
function drawTile(g: CanvasRenderingContext2D, x: number, y: number)
{
map[y][x].color(g);
if (!map[y][x].isAir() && !map[y][x].isPlayer())
g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}
function drawTile(g: CanvasRenderingContext2D, x: number, y: number)
{
map[y][x].draw(g, x, y);
if (!map[y][x].isAir() && !map[y][x].isPlayer())
g.fillRect(
x * TILE_SIZE,
y * TILE_SIZE,
TILE_SIZE,
TILE_SIZE);
}
interface Tile {
// ...
draw(g: CanvasRenderingContext2D, x: number, y: number): void;
}
class Air implements Tile {
// ...
draw(g: CanvasRenderingContext2D, x: number, y: number)
{
// empty method
}
}
class Flux implements Tile {
// ...
draw(g: CanvasRenderingContext2D, x: number, y: number)
{
g.fillStyle = "#ccffcc";
g.fillRect(
x * TILE_SIZE,
y * TILE_SIZE,
TILE_SIZE,
TILE_SIZE);
}
}
最後整理如下
- 本來的程式碼
- 移除drawTile
function drawMap(g: CanvasRenderingContext2D) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
drawTile(g, x, y)
}
}
}
function drawTile(g: CanvasRenderingContext2D, x: number, y: number)
{
map[y][x].draw(g);
}
function drawMap(g: CanvasRenderingContext2D) {
for (let y = 0; y < map.length; y++) {
for (let x = 0; x < map[y].length; x++) {
map[y][x].draw(g, x, y);
drawTile(g, x, y)
}
}
}
// 移除drawTile()
4.3.1 不能使用抽象類別來代替介面嗎?
可以,這可以避免程式碼重複。 介面會強制我們為每個新的類別進行主動操作。不會意外忘記某個屬性或覆寫了不應該覆寫的東西。 在寫完程式六個月後最為麻煩,當忘掉了程式是怎麼運作時,可能需要回來新增一個tile型別
4.3.2 規則:只能從介面來繼承(ONLY INHERIT FROM INTERFACES)
陳述
只能從介面來繼承
解說
只能從介面繼承,而不能從類別或抽象類別來繼承。 如此可以減少程式碼的重複。 抽象類別提供的預設實作,程式碼共用可能會導致耦合。
異味
這項規則是從Design Pattern一書中的「優先使用物件組合而非繼承」演化而來。
意圖
應該透過參照其他物件來分享程式碼,而不是用繼承的方式。
4.3.3 重複的程式碼是怎麼一回事?
很多情況下,重複的程式碼是不好的。但是要思考一下為什麼會有這樣的情況發生?若修改到重複部份,則需要在整支程式中找出所有重複的內容來修,是很不好的。
如果程式中有重複的程式碼,而我們只在其中一處進行了更改,那就變成有了兩種不同的功用。程式碼的重複是不好的,因為這種做法會鼓勵程式碼產生分歧。
4.4 重構一對複雜的if陳述句
- 原本的程式
- 新增isEdible/isPushable介面
- 把程式碼移到類別中之後的程式碼
function moveHorizontal(dx: number) {
if (map[player][playerx + dx].isFlux()
|| map[playery][playerx + dx].isAir())
moveToTile(playerx + dx, playery);
} else if ((map[playery][playerx + dx].isStone()
|| map[playery][playerx + dx].isBox())
&& map[palyery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = map[playerx][playerx + dx];
moveToTile(playerx + dx, playery);
} else if (map[playery][playerx + dx].isKey1()) {
removeLock1();
moveToTile(playerx + dx, playery);
} else if (map[playery][playerx + dx].isKey2()) {
removeLock2();
moveToTile(playerx + dx, playery);
}
}
function moveHorizontal(dx: number) {
if (map[player][playerx + dx].isEdible())
moveToTile(playerx + dx, playery);
} else if ((map[playery][playerx + dx].isPushable())
&& map[palyery][playerx + dx + dx].isAir()
&& !map[playery + 1][playerx + dx].isAir()) {
map[playery][playerx + dx + dx] = map[playerx][playerx + dx];
moveToTile(playerx + dx, playery);
} else if (map[playery][playerx + dx].isKey1()) {
removeLock1();
moveToTile(playerx + dx, playery);
} else if (map[playery][playerx + dx].isKey2()) {
removeLock2();
moveToTile(playerx + dx, playery);
}
interface Tile {
// ...
isEdible(): boolean;
isPushable(): boolean;
}
class Box implements Tile {
// ...
isEdible() { return false; }
isPushable() { return true; }
}
class Air implements Tile {
// ...
isEdible() { return true; }
isPushable() { return false; }
}
}
function moveHorizontal(dx: number) {
map[playery][playerx + dx].moveHorizontal(dx);
}
interface Tile {
// ...
moveHorizontal(dx: number): void;
}
class Box 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 Key1 implements Tile {
// ...
moveHorizontal(dx: number) {
removeLock1();
moveToTile(playerx + dx, playery);
}
}
class Lock1 implements Tile {
// ...
moveHorizontal(dx: number) { }
}
class Air implements Tile {
// ...
moveHorizontal(playerx + dx, playery);
}
4.5 移除無用的程式碼
由於介面都是公開的,沒有IDE能告知介面中的方法是否有被使用。 有可能是未來使用,或許也可能被外部範圍的某些程式使用。 一般情況下,無法輕易從介面中刪除方法。
4.5.1 重構模式:刪除後再編譯(TRY DELETE THEN COMPILE)
處理步驟
編譯。沒有錯誤。
從介面中刪除方法。
編譯。
a. 如果編譯後出現錯誤,請復原,然後再繼續。
b. 如果編譯後無錯誤,請逐一檢查每個類別,看看是否能繼續刪除相同的方法且編譯不出現錯誤。
範例
- 原本的程式
interface A {
m1(): void;
m2(): void;
}
class B implements A {
m1() { console.log("m1"); }
m2() { this.m3(); }
m3() { console.log("m3"); }
}
let a = new B();
a.m1();
總結
- 「NEVER USE if WITH else」和「NEVER USE switch」這兩規則表示,應該只在程式的外圍使用else或switch。else和switch都屬於低層級的控制流程運算子。應該使用「REPLACE TYPE CODE WITH CLASSES」和「PUSH CODE INTO CLASSES」來替換。
- 過於泛用化的方法會阻礙重構進行。使用「SPECIALIZED METHOD」
- 「ONLY INHERIT FROM INTERFACES」禁止使用抽象類別和類別繼承來重用程式碼,因為會導致不必要的緊密耦合。
- 「INLINE METHOD」及「TRY DELETE THEN COMPILE」可以在重構後清理程式碼。