본문 바로가기
책/파이브 라인스 오브 코드

4장 - 타입 코드 처리하기 [간단한 if 문 리팩터링]

by jeounpar 2024. 2. 3.

if 문에서 else를 사용하지 말 것

if 문에서 else를 사용하지 말라는 이유는 다음과 같다.

1. if-else를 사용하면 코드에서 결정이 내려지는 지점을 고정하게 된다 -> 기능 추가에 있어서 코드의 유연성이 떨어진다.

2. if-else는 하드코딩된 상수와 같다.

3. if-else는 이른 바인딩(early binding)이다. 이것의 반대는 늦은 바인딩(late binding)이고, 늦은 바인딩은 코드 추가를 통한 변경을 가능하게 한다. (클래스로 타입 코드 대체, 전략 패턴 도입)

이른 바인딩(early binding) : 컴파일 타임에 결정

늦은 바인딩(late binding) : 런타임에 결정

(이 책에서 바인딩의 의미는 2장에서 설명한 컴포지션을 설명하는 듯..)

 

숫자 배열에서 평균을 구하는 메서드가 다음과 같다면

function average(arr: number[]) {
	if (size(arr) === 0) throw "Empty array not allowd";
	else return sum(arr) / size(arr);
}

아래 코드와 같이 else를 제거하고 배열의 size를 검사하는 로직을 메서드로 분리한다.

function average(arr: number[]) {
	assertNotEmpty(arr);
	return sum(arr) / size(arr);
}

function assertNotEmpty(arr: number[]) {
	if (size(arr) === 0) throw "Empty array not allowd";
}

 

if-else 제거의 첫 단계 : Enum을 Interface로 변경

Input Enum을 대체할 Inputs Interface를 생성한다.

enum Input {
	UP,
	DOWN,
	LEFT,
	RIGHT,
}

interface Inputs {
	isRight(): boolean;
	isLeft(): boolean;
	isUp(): boolean;
	isDown(): boolean;
}

그 후 Right, Left, Up, Down 에 해당하는 클래스를 만들고 각 클래스 방향(왼쪽, 오른쪽, 위, 아래)에 해당하는 메서드를 제외하고 모두 false를 리턴하도록 코드를 작성한다.

class Right implements Inputs {
	isRight(): boolean {
		return true;
	}
	isLeft(): boolean {
		return false;
	}
	isUp(): boolean {
		return false;
	}
	isDown(): boolean {
		return false;
	}
}

class Left implements Inputs {
	...
}

class Up implements Inputs {
	...
}

class Down implements Inputs {
	...
}

 

handleInput 메서드도 다음과 같이 변경한다.

function handleInput() {
	while (inputs.length > 0) {
		let current = inputs.pop();
		if (current.isLeft()) moveHorizontal(-1);
		else if (current.isRight()) moveHorizontal(1);
		else if (current.isUp()) moveVertical(-1);
		else if (current.isDown()) moveVertical(1);
	}
}

 

Inputs 인터페이스를 구현하기 위해 Right, Left, Up, Down 클래스에 중복 코드가 많아졌다.

결국 우리가 원하는 것은 유저의 인풋을 처리하는 것이 목적이고, 어떤 인풋인지는 중요하지 않다.

 

Inputs 인터페이스를 다음과 같이 바꿔보자.

interface Inputs {
	handle(): void;
}

Right, Left, Up, Down 클래스도 고쳐보자.

class Right implements Inputs {
	handle(): void {
		moveHorizontal(-1);
	}
}

class Left implements Inputs {
	handle(): void {
		moveHorizontal(1);
	}
}

class Up implements Inputs {
	handle(): void {
		moveVertical(-1);
	}
}

class Down implements Inputs {
	handle(): void {
		moveVertical(1);
	}
}

 

이렇게 코드를 수정하면 handleInput 메서드는 다음과 같이 더욱 짧아진다.

function handleInput() {
	while (inputs.length > 0) {
		let current = inputs.pop();
		current.handle();
	}
}

이렇게 코드를 리팩터링 해보니 커맨드 패턴인 것 같기도??

 

불필요한 메서드의 인라인화

handleInput 메서드를 메서드 추출을 통해 다음과 같이 리팩터링할 수 있다.

function handleInput() {
	while (inputs.length > 0) {
		let current = inputs.pop();
		move(current);
	}
}

function move(current: Inputs) {
	current.handle();
}

하지만 오히려 move메서드를 추출함으로서 가독성이 안좋아지고 '호출 또는 전달, 한 가지만 할 것'이라는 규칙을 위배한다.

다음과 같이 move메서드를 인라인화하여 메서드를 제거하고 current 변수도 제거 할 수 있다.

function handleInput() {
	while (inputs.length > 0) {
		inputs.pop().handle();
	}
}

 

그러면 메서드를 인라인화 할 수 있으면 무조건 하는 것이 좋은가? 라고 물어보면 NO 이다.

- 메서드가 인라인화하기에 너무 복잡하다면 인라인화 했을 때 가독성이 떨어 질 수 있다.