4장 : 타입코드 처리하기
의문점
- enum 을 클래스로 굳이 다 나눠야함? → 복잡도에 따라서 선택!
- 대출부분
학습목표
- if 문에서 else를 사용하지말것과 switch문을 사용하지 말것으로 ‘이른 바인딩’ 제거란?
- ‘클래스로 타입 코드 대체’, 그리고 ‘클래스로의 코드 이관’으로 if 문 제거하기란?
- ‘메서드 전문화’로 문제가 있는 일반성 제거하기란?
- ‘인터페이스에서만 상속’받을 것으로 코드 간 커플링(결합) 방지하기
- ‘메서드의 인라인화’ 및 ‘삭제 후 컴파일하기’를 통한 불필요한 메서드 제거
키워드
1. 간단한 if문 리팩터링
4.1 규칙 : if문에서 else를 사용하지 말것
정의
- 프로그램에서 인식 하지 못하는 타입인지를 검사하지 않는 한, if문에서 else사용하지 말것
- if-else 를 사용하면 코드에서 결정이 내려지는 지점을 고정하게 됨 → if-else 위치 이후 다른 변형 도입할 수없기 때문에 코드 유연성 저하 (아직 이해X)
- if-else문은 하드코딩의 결정
- 어떤 input 된 key를 검사할때 else if 체인구문 피할 수 없음
- 하지만 보통은 사용자 입력, DB값 가져오는 등 app외부 입력받는 프로그램 경계에서 발생하기 때문에 문제시 되지않음
- 이러한 외부 데이터 타입을 내부에서 제어가능한 데이터 타입으로 매핑하기
- 독립된 if문은 검사(check)로 간주, if-else문은 의사결정(decision)으로 간주함
- 이 규칙은 else문 찾기로 검증이 가능함
스멜
- 이른 바인딩 (early binding)
- 컴파일 할때 if-else같은 의사결정 동작은 컴파일시 처리가 됨 → 어플리케이션에 고정 → 재컴파일 없이는 수정 불가
- if 문을 수정해야 변경할 수 있기 때문에, 추가에 의한 변경을 방해함
- 늦은 바인딩 (late binding)
- 코드가 실행되는 순간에 동작이 결정됨
- 이른 바인딩 (early binding)
의도
- if문은 조건 연산자로 흐름을 제어함 → 다음 실행코드 결정
- OOP의 객체는 더 강력한 제어흐름 연산자임
- 인터페이스 사용한 두가지 이상의 구현이 있으면, 인스턴스화 하는 클래스에 따라 실행하는 코드 결정 가능
- 본질적으로 이 규칙은 더 강력하고 유연한 도구인 객체를 사용하는 방안 찾게함
실습
enum Input { UP, DOWN, LEFT, RIGHT } interface Input2{ isRight() : boolean; isLeft() : boolean; isUp() : boolean; isDown() : boolean; } class RIGHT implements Input2{ isRight() {return true;} . . . } class LEFT implements Input2{ . . . . } class UP implements Input2{ . . . . } class DOWN implements Input2{ . . . . }
function handleInputs(){ while (inputs.length > 0) { let current = inputs.pop(); handleInput(current) } } function handleInput(current: Input){ if (current === Input.LEFT) moveHorizontal(-1); else if (current === Input.RIGHT) moveHorizontal(1); else if (current === Input.UP) moveVertical(-1); else if (current === Input.DOWN) moveVertical(1); } // handleInput()-> 일치여부검사 변경 function handleInput2(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); }
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()); });
4.1 리팩터링 패턴 : 클래스로 타입코드 대체
- 설명
- 열거형 → 인터페스로 변환 / 열거형값 → 클래스
- 클래스로 코드이관과 함꼐 사용 (추가를 통한 변경)
- 열거형 값 → 클래스 변경시
- 해당값과 관련된 기능을 그룹화 가능
- 기능과 데이터 함께 제공 → 기능을 해당 값의 특징에 맞게 만들기 가능
- 열거형에 값 추가할때 연결 로직 확인해야하지만,
인터페이스를 구현한 새 클래스 추가는 해당 클래스에 메서드의 구현이 필요할 뿐, 새로운 클래스 사용하기 전까지 다른 코드 수정하지 않아도 됨 - 티셔츠 크기의 타입코드
→ 단순 int 는 타입코드 사용 추적 어려움
→ 항상 즉시! 열거형으로
→ 클래스로 타입코드 대체 래팩터링
- 절차
- 임시이름 인터페이스 도입 & 인터페이스에는 열거형(enum)의 각값에 대한 메서드 존재
- e.g. : interface TrafficLight2{}
- 열거형의 각 값에 해당 클래스 만듬 / 클래스에 해당 메서드 제외 메서드는 false 반환
- e.g. : class red imperments TrafficLight2{ isRed() {retrun true;} } { isYellow() {retrun false;} }
- 열거형 이름 바꾸기 → 컴파일러 오류 발생
- enum TrafficLight → enum RawTrafficLight
- 타입을 이전이름 → 임시이름으로 변경 → 일치성 검사 새 메서드로 변경
- if(current === TrafficLight.RED) → if(current.isRed())
- 남아있는 열거형 값에대한 참조 대신 새로운 클래스를 인스턴스화하여 교체
- TrafficLight.RED → new Red()
- 오류 X ⇒ 인터페이스 이름을 모든위치에서 영구적으로 변경
- interface TrafficLight2{} → interface TrafficLight{}
- 임시이름 인터페이스 도입 & 인터페이스에는 열거형(enum)의 각값에 대한 메서드 존재
- 참고로 여기서 isRed 및 isYellow 는 임시적인 것임
- 설명
4.1 규칙 : 클래스로 코드이관하기
마법임
//1-1 class Right implements Input { handleInput(){ 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); } } //1-2 this로 class Right implements Input { handleInput(){ if (input === this) moveHorizontal(1); else if (input === this) moveHorizontal(-1); else if (input === this) moveVertical(1); else if (input === this) moveVertical(-1); } } //2. interface interface Input { isRight(): boolean; isLeft(): boolean; isUp(): boolean; isDown(): boolean; handle(): void; } //3-1. is 메서드를 인라인메서드로 변 class Right implements Input { handleInput(){ if (false) moveHorizontal(1); else if (true) moveHorizontal(-1); else if (true) moveVertical(1); else if (true) moveVertical(-1); } } //3-2 true 부분의 메서드호출만 두기 나머지 제거 class Right implements Input { isRight() { return true; } isLeft() { return false; } isUp() { return false; } isDown() { return false; } handle() { moveHorizontal(1); } } //3-4 이름 변경 interface Input { isRight(): boolean; isLeft(): boolean; isUp(): boolean; isDown(): boolean; handle(): void; } //4. 메서드 호출로변경 function handleInput(input: Input) { input.handle(); }
이 결과
- 적은 인지부하
4.1 리팩터링 패턴 : 클래스로 코드 이관하기
- 기능을 클래스로 옮기기 때문에 → 클래스로 타입코드 대체패턴의 연장선
- 그 결과 if문 제거 → 데이터에 더 가까이 이동
- 특정 값과 연결된 기능이, 값에 해당하는 클래스로 이동 → 불변속성의 지역화
4.1 불필요한 메서드 인라인화
hnadleInput() 함수를 앞에서는 도입
리팩터링 순환적임 : 리팩터링 가능하게 하는 항목 추가 → 다시 제거
hnadleInput() 함수 목적은 가독성이지만, 이제는 X → 제거
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 리팩터링 패턴 : 메서드의 인라인화
- 이책의 주제는 코드추가와 코드 제거
- 본 패턴은 후자임 → 가독성 도움X로서 제거
- 방법
- 호출하는 모든 곳으로 코드를 옮기기
- 고려사항
- 메서드가 한줄만 있는경우 → 다섯줄 제한 규칙 때문
- 메서드 인라인화가 복잡하지 않은지 여부 → 작업은 동일한 추상화 수준일것
- 예제
- deposit() 메서드와 tranfer()메서드 분할 → deposit메서드 잘못호출시 출금없이 돈 입금 → 결합이 바람직
2. 긴 if문의 리팩터링
개요
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); } }
if문은 함수 시작에만 배치 위반 & else if 체인구문
//메서드 추출 하기 function drawMap(g: CanvasRenderingContext2D) { for (let y = 0; y < map.length; y++) { for (let x = 0; x < map[y].length; x++) { colorOfTile(g, y, x); if (map[y][x] !== Tile.AIR && map[y][x] !== Tile.PLAYER) g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } } // else 사용하지 말것 규칙 위반 function colorOfTile(g: CanvasRenderingContext2D, y: number, x: 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"; }
열거형 각 값에 해당 클래스 생성 & 열거형이름 변경 & 일치성 검사 메서드로 변경
enum RawTile { AIR, FLUX, UNBREAKABLE, PLAYER, STONE, FALLING_STONE, BOX, FALLING_BOX, KEY1, LOCK1, KEY2, LOCK2 } interface Tile2 { isAir(): boolean; isFlux(): boolean; isUnbreakable(): boolean; isPlayer(): boolean; isStone(): boolean; isFallingStone(): boolean; isBox(): boolean; isFallingBox(): boolean; isKey1(): boolean; isLock1(): boolean; isKey2(): boolean; isLock2(): boolean; } class Air implements Tile2 { isAir() { return true; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class Flux implements Tile2 { isAir() { return false; } isFlux() { return true; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class Unbreakable implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return true; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class Player implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return true; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class Stone implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return true; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class FallingStone implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return true; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class Box implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return true; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class FallingBox implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return true; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class Key1 implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return true; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } } class Lock1 implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return true; } isKey2() { return false; } isLock2() { return false; } } class Key2 implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return true; } isLock2() { return false; } } class Lock2 implements Tile2 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return true; } } //일치성 검사 새 메서드로 경 function colorOfTile(g: CanvasRenderingContext2D, y: number, x: 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 의 자로형이 여전히 문제임
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], ]; let inputs: Input[] = []; 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(); } } } }
Tile 오류가 여전히 있음 → 임시이름 사용하는 이유
임시이름 사용 X → 문제 발견 X & 잘 작동한다고 생각
4.2 일반성 제거
remove 함수 문제는 타일의 타입으로 map 의 모든위치에 주언타입의 타일 제거 → 하지만 실제로는 Lock1 또는 Lock2 만 제거
Tile 의 특정인스턴스인지 확인하지않고 유사한지만 확인함 → 너무 일반적임
//remove복제 //-> ===tile 을 ===Tile.Lock1으로 변경 //-> ===Tile.Lock1 을 .isLock1()로 변 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 moveHorizontal(dx: number) { if (map[playery][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[playery][playerx + dx + dx].isAir() && !map[playery + 1][playerx + dx].isAir()) { map[playery][playerx + dx + dx] = map[playery][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); } }
4.2 리팩터링 패턴 : 메서드 전문화
프로그래머의 본능에 반하는 패턴
- 일반화 & 재사용본능 → but 책임 흐려지고, 다양한위치에서 호출시 문제됨
메서드 전문화는 적은위치에서 호출되어 필요성이 없어지면 더 빨리제거
절차
- 전문화하려는 메서드 복제
- 메서드 중 하나 이름을 새로 사용할 메서드의 이름으로 변경 → 메서드 제거
- 매개변수 제거에 따라 메서드 수정해서 오류 없도록
- 이전의 호출을 새로운것을 사용하도록 변경
결국 해당 메서드에 대해서 훨씬 더 이해하기 쉬워짐 (e.g. 체스)
4.2 switch 가 허용되는 유일한경우
동작하지 않는 열거형 인덱스로 map 생성
인덱스는 일반적으로 DB나 파일 에 저장시 사용
전체 map 변경하는 대신 열거형 인덱스에서 새로운 클래스 사용하도록 함수만들기가 좋음
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], ];
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]); } } }
본 switch 는 예외케이스로 위반하지않음
4.2 규칙 : switch 를 사용하지 말것
- 정의
- default 케이스가 없고, 모든 case에 반환 값이 있는 경우가 X이면 switch 사용하지말것
- switch는 모든값에 대한 처리 실행필요 X
- 문제점
- default 지원함 → 중복없이 여러값 지정하지만 불변속성임
- 새로운값 추가시 여전히 유요한지 컴파일로러 판단X
- 컴파일러는 새로 추가한 값의 처리 잊은건지, default 에 지정하고자한것인지 구분 불가
- 해결
- default 사용 X
- 문제점
- fall-through 로직임
- 문제점
- break 사용 누락할수도
- 해결
- 모든 케이스에 return 지정
- 문제점
- 스멜
- switch는 스멜의 이름임
- 불변속성의 전역화
- 의도
- else if 체인문으로 변환하고 이를 다시 클래스로 만듬
- 정의
4.2 if 제거하기
이것도 마찬가지로 ‘클래스로 코드이관’ 패턴 적용
function colorOfTile(g: CanvasRenderingContext2D, y: number, x: 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"; }
4.3 코드 중복처리
개요
drawMap 의 중간에 if가 존재 여전히 규칙위반
메서드 추출
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); }
‘클래스로 코드이관패턴’으로 메서드를 Tile클래스를 옮길 수 있음
//코드이관 function drawTile(g: CanvasRenderingContext2D, x: number, y: number) { map[y][x].draw(g, x, y); }
interface Tile { isAir(): boolean; isFlux(): boolean; isUnbreakable(): boolean; isPlayer(): boolean; isStone(): boolean; isFallingStone(): boolean; isBox(): boolean; isFallingBox(): boolean; isKey1(): boolean; isLock1(): boolean; isKey2(): boolean; isLock2(): boolean; color(g: CanvasRenderingContext2D): void; draw(g: CanvasRenderingContext2D, x: number, y: number): void; } class Air implements Tile { isAir() { return true; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { } draw(g: CanvasRenderingContext2D, x: number, y: number) { } } class Flux implements Tile { isAir() { return false; } isFlux() { return true; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#ccffcc"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#ccffcc"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } class Unbreakable implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return true; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#999999"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#999999"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } class Player implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return true; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { } draw(g: CanvasRenderingContext2D, x: number, y: number) { } } class Stone implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return true; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#0000cc"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#0000cc"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } class FallingStone implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return true; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#0000cc"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#0000cc"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } class Box implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return true; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#8b4513"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#8b4513"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } class FallingBox implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return true; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#8b4513"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#8b4513"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } class Key1 implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return true; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#ffcc00"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#ffcc00"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } class Lock1 implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return true; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#ffcc00"; } 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 { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return true; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#00ccff"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#00ccff"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } } class Lock2 implements Tile { isAir() { return false; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return true; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#00ccff"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#00ccff"; 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].draw(g, x, y); } //인라인화 후 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); } } }
4.3 인터페이스 대신 추사클래스 사용은 불가능?
- 사용할수있음 → 코드 중복을 피할 수있음
- 단점
- 인터페이스 사용시 개발자는 능동적으로 해야함 → 잘못해서 속성을 잊거나, 하면안되는오버라이딩 방지가능
- 즉 내용잊어버리고 시간이 흐르고 만든 Tile 의 유형을 추가해야할 때 문제시됨
- 결국 ‘인터페이스에서만 상속’받을것 규칙으로 공식화함
4.3 규칙: 인터페이스 에서만 상속받을것
정의
상속은 오직 인터페이스를 통해서만 받음
추상클래스 상속 이유
- 일부 메서드에서는 기본구현 제공, 다른 메서드는 추상화
- 중복을 줄이고, 코드 줄 줄이기 경우 편리
추상클래스 단점
- 코드 공유는 커플링(결합)을 유발함 → 커플링은 추상클래스의 코드의미
- 추상클래스경우에 두가지 메서드 가 있음 → 하나의 하위클래스에는 전자, 다른것은 후자 → 유일한 옵션은 메서드 중 하나만 빈 버전으로 재정의
- 기본 구현 메서드 경우 두가지 버전
- 모든 하위클래스에 해당 메서드가 필요한 경우 → 메서드를 클래스 밖으로 쉽게 이동 가능
- 일부 하위 클래스에서 메서드 재정의 → 기본구현이 있기 때문에 컴파일러로 재정의가 필요한 메서드 못잡아냄
스멜
- Gof 디자인패턴의 ‘상속보다 컴포지션(객체구성)이 더 좋다’ 원칙에서 규칙 도출
의도
- 우리가 상속받기위해 다른 객체를 참고함으로써 코드를 공유
4.3 클래스에 있는 코드 중복은 무엇?
- 코드 복제는 변경이 필요할 때 수정내용을 프로그램 전체에 반영하는 방식으로 변경 → 유지보수성 저해
- 코드 중복은 분기(divergence)를 조장함 → 해결방법? 다음장으로
4.4 복잡한 if체인 구문 리팩터링
개요
아래 두 함수에 두개의 ‘||’ 표현식이 있음 → 이 구조를 보존하면서 리팩토링 해야 함
function moveHorizontal(dx: number) { if (map[playery][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[playery][playerx + dx + dx].isAir() && !map[playery + 1][playerx + dx].isAir()) { map[playery][playerx + dx + dx] = map[playery][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 moveVertical(dy: number) { if (map[playery + dy][playerx].isFlux() || map[playery + dy][playerx].isAir()) { moveToTile(playerx, playery + dy); } else if (map[playery + dy][playerx].isKey1()) { removeLock1(); moveToTile(playerx, playery + dy); } else if (map[playery + dy][playerx].isKey2()) { removeLock2(); moveToTile(playerx, playery + dy); } }
변경
//함수선언 function moveHorizontal(dx: number) { if (map[playery][playerx + dx].isEdible()) { moveToTile(playerx + dx, playery); } else if (map[playery][playerx + dx].isPushable() && map[playery][playerx + dx + dx].isAir() && !map[playery + 1][playerx + dx].isAir()) { map[playery][playerx + dx + dx] = map[playery][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 moveVertical(dy: number) { if (map[playery + dy][playerx].isEdible()) { moveToTile(playerx, playery + dy); } else if (map[playery + dy][playerx].isKey1()) { removeLock1(); moveToTile(playerx, playery + dy); } else if (map[playery + dy][playerx].isKey2()) { removeLock2(); moveToTile(playerx, playery + dy); } }
// 인터페이스 만들어주기 interface Tile { isAir(): boolean; isFlux(): boolean; isUnbreakable(): boolean; isPlayer(): boolean; isStone(): boolean; isFallingStone(): boolean; isBox(): boolean; isFallingBox(): boolean; isKey1(): boolean; isLock1(): boolean; isKey2(): boolean; isLock2(): boolean; color(g: CanvasRenderingContext2D): void; draw(g: CanvasRenderingContext2D, x: number, y: number): void; isEdible(): boolean; isPushable(): boolean; } class Air implements Tile { isAir() { return true; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { } draw(g: CanvasRenderingContext2D, x: number, y: number) { } isEdible() { return true; } isPushable() { return false; } } class Flux implements Tile { isAir() { return false; } isFlux() { return true; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#ccffcc"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#ccffcc"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } isEdible() { return true; } isPushable() { return false; } } //생략...
클래스로의 코드 이관 실시
//전 function moveHorizontal(dx: number) { if (map[playery][playerx + dx].isEdible()) { moveToTile(playerx + dx, playery); } else if (map[playery][playerx + dx].isPushable() && map[playery][playerx + dx + dx].isAir() && !map[playery + 1][playerx + dx].isAir()) { map[playery][playerx + dx + dx] = map[playery][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) { map[playery][playerx + dx].moveHorizontal(dx); // 클래스로 코드 이 interface Tile { isAir(): boolean; isFlux(): boolean; isUnbreakable(): boolean; isPlayer(): boolean; isStone(): boolean; isFallingStone(): boolean; isBox(): boolean; isFallingBox(): boolean; isKey1(): boolean; isLock1(): boolean; isKey2(): boolean; isLock2(): boolean; color(g: CanvasRenderingContext2D): void; draw(g: CanvasRenderingContext2D, x: number, y: number): void; isEdible(): boolean; isPushable(): boolean; moveHorizontal(dx: number): void; } class Air implements Tile { isAir() { return true; } isFlux() { return false; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { } draw(g: CanvasRenderingContext2D, x: number, y: number) { } isEdible() { return true; } isPushable() { return false; } moveHorizontal(dx: number) { moveToTile(playerx + dx, playery); } } class Flux implements Tile { isAir() { return false; } isFlux() { return true; } isUnbreakable() { return false; } isPlayer() { return false; } isStone() { return false; } isFallingStone() { return false; } isBox() { return false; } isFallingBox() { return false; } isKey1() { return false; } isLock1() { return false; } isKey2() { return false; } isLock2() { return false; } color(g: CanvasRenderingContext2D) { g.fillStyle = "#ccffcc"; } draw(g: CanvasRenderingContext2D, x: number, y: number) { g.fillStyle = "#ccffcc"; g.fillRect(x * TILE_SIZE, y * TILE_SIZE, TILE_SIZE, TILE_SIZE); } isEdible() { return true; } isPushable() { return false; } moveHorizontal(dx: number) { moveToTile(playerx + dx, playery); } }
인라인 메서드 리팩토링
//전 class Right implements Input { isRight() { return true; } isLeft() { return false; } isUp() { return false; } isDown() { return false; } handle() { moveHorizontal(1); } } class Left implements Input { isRight() { return false; } isLeft() { return true; } isUp() { return false; } isDown() { return false; } handle() { moveHorizontal(-1); } } // class Right implements Input { isRight() { return true; } isLeft() { return false; } isUp() { return false; } isDown() { return false; } handle() { map[playery][playerx + 1].moveHorizontal(1); } } class Left implements Input { isRight() { return false; } isLeft() { return true; } isUp() { return false; } isDown() { return false; } handle() { map[playery][playerx - 1].moveHorizontal(-1); } }
4.5 필요없는 코드 제거하기
- 개요
- 새로운 메서드 도입, 일부 삭제 했지만 더 제거가능
- IDE는 사용하지 않는 함수 표시함 → 즉시 함수 제거!
- 인터페이스는 공용이므로 인터페이스 메서드가 사용되는지 여부 알려주는 IDE 없음 → 쉽게 삭제할 수없음
- 4.5 리팩터링 패턴 : 삭제 후 컴파일하기
- 설명
- 용도는 인터페이스 전체범위를 알고 있을 때 인터페이스에서 사용하지 않는 메서드 제거하기
- 메서드 삭제하고 컴파일러에서 허용하는지 확인
- 구현 중에는 X
- 코드 베이스가 만료된 코드가 있으면 코드의 위치를 아래로 끌어내림
- 코드 읽고 무시하는데 시간소요
- 컴파일과 분석이 느려짐
- 테스트 어렵게함
- 사용하지 않는 메서드 식별가능한데 이를 속이는것이 인터페이스
- 메서드가 인터페이스에 있다?
- 범위 밖의 코드 사용위한 것?
- 범위 내의 코드에 대한 메서드가 필요
- 편집기는 차이점을 구분할 수없음 → 유일 안전한 옵션은 모든 인터페이스 메서드가 범위 밖에서 사용된다고 가정
- 메서드가 인터페이스에 있다?
- 인터페이스가 범위 내에서 사용된다는것 알고있다면 수동으로 정리할것
- 설명
요약
- if 문에서 else를 사용하지말것과 switch문을 사용하지 말것으로 ‘이른 바인딩’ 제거란?
- else, switch 는 프로그램의 가장자리에 있어야함
- 모두 낮은 수준(추상화 수준)의 제어흐름 연산자임 → 2번으로
- ‘클래스로 타입 코드 대체’, 그리고 ‘클래스로의 코드 이관’으로 if 문 제거하기란?
- 클래스로 타입코드 대체, 클래스로 코드이관 패턴으로 switch와 else구문을 높은 수준의 클래스와 메서드로 대체해야함
- ‘메서드 전문화’로 문제가 있는 일반성 제거하기란?
- 지나치게 일반화된 메서드는 리팩터링 방해
- ‘인터페이스에서만 상속’받을 것으로 코드 간 커플링(결합) 방지하기
- 추상클래스와 클래스 상속을 사용해 코드 재사용 방지위함
- 불필요하게 긴밀한 커플링발생시
- ‘메서드의 인라인화’ 및 ‘삭제 후 컴파일하기’를 통한 불필요한 메서드 제거
- 가독성에 도움이 되지않는 메서드 제거
나눔
'cleanCode > FiveLinesOfCode' 카테고리의 다른 글
[FiveLinesOfCode] 6장: 유사한 코드 융합하기 (1) | 2023.10.29 |
---|---|
[FiveLinesOfCode] 5장 유사한 코드 융합하기 (1) | 2023.10.29 |
[FiveLinesOfCode] 3장 : 긴 코드 조각내기 (1) | 2023.10.03 |
[FiveLinesOfCode] 2장 : 리팩터링 깊게 들여다보기 (0) | 2023.10.03 |
[FiveLinesOfCode] 1장 : 리팩터링, 리팩터링 하기 (0) | 2023.10.03 |