【Refactor】【TypeScript】以類別替代型別 重構法



重點

  • 先寫測試,用測試來保護邏輯。
  • 把原本是 type, enum 的型別,重構為 classes
  • 通常這種程式碼會出現在 if, else 檢查上,用來判定業務邏輯的選項分支規則。

實作

  • before

    // enum import {Input} from "./input"; export const moveHorizontal = (x: number) => { console.log("x: ", x); }; ; export const moveVertical = (y: number) => { console.log("moveVertical: ",); return y; }; export function handleInput(input: Input) { // console.log("input: ", input); if (input === Input.LEFT) { moveHorizontal(-1) } if (input === Input.RIGHT) { moveHorizontal(1) } if (input === Input.UP) { moveVertical(-1) } if (input === Input.DOWN) { moveVertical(1) } // 多一種 input 就要回頭改程式碼! } // check logic

    test

    import * as fns from "./main"; import {Input} from "./input"; describe('main', function () { const spyMoveHorizontal = jest.spyOn(fns, 'moveHorizontal'); const spyMoveVertical = jest.spyOn(fns, 'moveVertical'); beforeEach(() => { spyMoveHorizontal.mockReset() spyMoveVertical.mockReset() }); it('should move left', () => { fns.handleInput(Input.LEFT) expect(spyMoveHorizontal).toHaveBeenCalledWith(-1) }); it('should move right', () => { fns.handleInput(Input.RIGHT) expect(spyMoveHorizontal).toHaveBeenCalledWith(1) }); it('should move up', () => { fns.handleInput(Input.UP) expect(spyMoveVertical).toHaveBeenCalledWith(-1) }); it('should move down', () => { fns.handleInput(Input.DOWN) expect(spyMoveVertical).toHaveBeenCalledWith(1) }); });
  • after

    // abstract interface export const moveHorizontal = (x: number) => { console.log("x: ", x); }; export const moveVertical = (y: number) => { console.log("moveVertical: ",); return y; }; interface InputAction { move: () => void } // refactored check logic export function handleInput(input: InputAction) { // console.log("input: ", input); input.move() } // refactor implementation(v1) interface Input2 { isLeft: () => boolean isRight: () => boolean isUp: () => boolean isDown: () => boolean } export class Left implements Input2 { isDown(): boolean { return false; } isLeft(): boolean { return true; } isRight(): boolean { return false; } isUp(): boolean { return false; } } export class Right implements Input2 { isDown(): boolean { return false; } isLeft(): boolean { return false; } isRight(): boolean { return true; } isUp(): boolean { return false; } } export class Up implements Input2 { isDown(): boolean { return false; } isLeft(): boolean { return false; } isRight(): boolean { return false; } isUp(): boolean { return true; } } export class Down implements Input2 { isDown(): boolean { return true; } isLeft(): boolean { return false; } isRight(): boolean { return false; } isUp(): boolean { return false; } } // implementation(v2) // move 方法自帶在每個 Action 身上 export class LeftAction implements InputAction { move(): void { moveHorizontal(-1) } } export class RightAction implements InputAction { move(): void { moveHorizontal(1) } } export class UpAction implements InputAction { move(): void { moveVertical(-1) } } export class DownAction implements InputAction { move(): void { moveVertical(1) } }
    import * as fns from "./main"; import {DownAction, LeftAction, RightAction, UpAction} from "./main"; describe('main', function () { const spyMoveHorizontal = jest.spyOn(fns, 'moveHorizontal'); const spyMoveVertical = jest.spyOn(fns, 'moveVertical'); beforeEach(() => { spyMoveHorizontal.mockReset() spyMoveVertical.mockReset() }); it('should move left', () => { fns.handleInput(new LeftAction()) expect(spyMoveHorizontal).toHaveBeenCalledWith(-1) }); it('should move right', () => { fns.handleInput(new RightAction()) expect(spyMoveHorizontal).toHaveBeenCalledWith(1) }); it('should move up', () => { fns.handleInput(new UpAction()) expect(spyMoveVertical).toHaveBeenCalledWith(-1) }); it('should move down', () => { fns.handleInput(new DownAction()) expect(spyMoveVertical).toHaveBeenCalledWith(1) }); });

後話

  • 若多了一種 input 檢查,不就每個 classes 都要加嗎?

後來想想,好像也是 🤔

  • 原本看的重構只是「進行到一半」,書上還有寫到更進階的重構,是個「抽象型別」的重構法,這就比較符合心中的想像了。

REF

  • 重構的時機與實作 — 五行程式碼規則