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

3장 - 긴 코드 조각내기 for 메서드 다섯 줄 제한(FIVE LINES)

by jeounpar 2024. 1. 31.

메서드 다섯 줄 제한(FIVE LINES)을 지키위 위한 리팩터링 패턴 3가지

1. 메서드 추출(EXTRACT METHOD)

2. 호출 또는 전달, 한 가지만 할 것(EITHER CALL OR PASS)

3. if 문은 메서드의 시작에만 배치

예제 코드 : https://github.com/wikibook/five-lines

 

1. 메서드 추출(EXTRACT METHOD)

function draw() {
	let canvas = document.getElementById("GameCanvas") as HTMLCanvasElement;
	let g = canvas.getContext("2d");

	g.clearRect(0, 0, canvas.width, canvas.height);

	// 맵 그리기
	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);
		}
	}

	// 플레이어 그리기
	g.fillStyle = "#ff0000";
	g.fillRect(playerx * TILE_SIZE, playery * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}

draw 메서드는 맵을 그리는 메서드와 플레이어를 그리는 메서드로 추출할 수 있다.

 

맵을 그리는 메서드를 drawMap, 플레이어를 그리는 메서드를 drawPlayer 메서드로 분리할 수 있는데, 두 메서드의 매개변수로 g값을 넘겨줘야 컴파일 에러를 방지할 수 있다 (리팩터링에 컴파일러를 사용)

코드는 다음과 같아진다.

function draw() {
	let canvas = document.getElementById("GameCanvas") as HTMLCanvasElement;
	let g = canvas.getContext("2d");

	g.clearRect(0, 0, canvas.width, canvas.height);

	drawMap(g);
	drawPlayer(g);
}

function drawMap(g: CanvasRenderingContext2D) {
	...
}

function drawPlayer(g: CanvasRenderingContext2D) {
	...
}

 

draw 메서드는 다섯줄 제한 규칙을 지켰지만, drawMap 메서드는 다섯줄 이상이다. (다섯줄 이하로 바꾸는 방법은 4장에서 살펴볼 예정)

 

2. 호출 또는 전달, 한 가지만 할 것(EITHER CALL OR PASS)

호출 또는 전달, 한 가지만 한다는 것은 '함수 내에서 객체에 있는 메서드를 호출하거나 객체를 인자로 전달할 수 있지만 둘을 섞어 사용해서는 안 된된다는 뜻' 이다.

흠.. 아직 이해하기 힘든데, 아래의 코드를 보자.

// 배열의 평균을 구하는 함수
function average(arr: number[]) {
	return sum(arr) / arr.length;
}

sum 메서드를 사용해 배열의 모든 원소를 더하고 arr 배열의 메서드인 length를 사용하고 있다.

이는 '호출 또는 전달, 한 가지만 할 것' 이라는 규칙에 위배된다.

'호출 또는 전달, 한 가지만 할 것' 다르게 말하면 '하나의 메서드에 모두 같은 수준의 추상화를 사용한다'로 바꿔 말할 수 있고, 위의 예시에서는 sum(arr)이라는 높은 수준의 추상화와 arr.length 낮은 수준의 추상화 섞어서 사용하고있다.

'호출 또는 전달, 한 가지만 할 것' = '메서드 내용은 동일한 추상화 수준에 있어야 한다' 이다.

 

메서드 추출을 통해 얻은 draw메서드를 살펴보자.

function draw() {
	let canvas = document.getElementById("GameCanvas") as HTMLCanvasElement;
	let g = canvas.getContext("2d");

	// g의 메서드를 사용 = 낮은 수준의 추상화
	g.clearRect(0, 0, canvas.width, canvas.height);

	// 높은 수준의 추상화
	drawMap(g);
	drawPlayer(g);
}

draw 메서드는 g의 메서드를 사용하는 낮은 수준의 추상화 수준과 메서드를 추출해서 얻은 높은 수준의 추상화 수준동시에 사용하고 있어서 규칙을 위배한다.

낮은 수준의 추상화 수준을 높은 수준으로 끌어 올리기 위해 메서드 추출을 진행한다.

변수 canvas와 g는 초기 그래픽을 설정하는 코드이므로 해당 코드들을 추출을 하면 코드는 다음과 같아진다.

function draw() {
	let g = createGraphics();
	drawMap(g);
	drawPlayer(g);
}

function createGraphics() {
	let canvas = document.getElementById("GameCanvas") as HTMLCanvasElement;
	let g = canvas.getContext("2d");
	g.clearRect(0, 0, canvas.width, canvas.height);
	return g;
}

 

메서드 추출을 진행하면서 메서드의 이름을 짓게 되는데, 이때 좋은 이름의 메서드는 코드의 가독성을 높인다.

좋은 이름을 가지는 메서드의 속성은 다음과 같다

- 함수의 의도를 설명해야 한다.

- 함수가 하는 모든 것을 담아야 한다.

- 도메인에서 일하는 사람이 이해할 수 있어야 한다.

 

이번엔 update 메서드의 리팩터링을 진행해보자.

function update() {
	while (inputs.length > 0) {
		let current = inputs.pop();
		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);
	}

	for (let y = map.length - 1; y >= 0; y--) {
		for (let x = 0; x < map[y].length; x++) {
			if (
				(map[y][x] === Tile.STONE || map[y][x] === Tile.FALLING_STONE) &&
				map[y + 1][x] === Tile.AIR
			) {
				map[y + 1][x] = Tile.FALLING_STONE;
				map[y][x] = Tile.AIR;
			} else if (
				(map[y][x] === Tile.BOX || map[y][x] === Tile.FALLING_BOX) &&
				map[y + 1][x] === Tile.AIR
			) {
				map[y + 1][x] = Tile.FALLING_BOX;
				map[y][x] = Tile.AIR;
			} else if (map[y][x] === Tile.FALLING_STONE) {
				map[y][x] = Tile.STONE;
			} else if (map[y][x] === Tile.FALLING_BOX) {
				map[y][x] = Tile.BOX;
			}
		}
	}
}

 

위 메서드는 사용자 인풋을 처리하는 메서드와 map을 업데이트하는 메서드로 추출할 수 있다.

메서드 추출 후 update 메서드는 다음과 같다.

function update() {
	handleInput();
	updateMap();
}

function handleInput() {
	...
}

function updateMap() {
	...
}

 

3. if 문은 메서드의 시작에만 배치

잠시, 2에서 n까지 모든 소수를 출력하는 메서드를 살펴보자.

function reportPrimes(n: number) {
	for (let i = 2; i < n; i++) {
		if (isPrime(i)) console.log(`${i} is prime`);
	}
}

위 메서드는

1. 숫자를 반복

2. 숫자가 소수인지 확인

두 가지 일을 한다.

메서드는 하나의 일을 해야하는 규칙을 지키기 위해 메서드를 추출하면 다음과 같아진다.

function reportPrimes(n: number) {
	for (let i = 2; i < n; i++) {
		printIfPrime(i);
	}
}

function printIfPrime(n: number) {
	if (isPrime(i)) console.log(`${i} is prime`);
}

 

updateMap 메서드는

1. 2차원 배열을 순회

2. tile 업데이트

두 가지 일을 하고 있는데, tile 업데이트를 메서드로 분리하면 다음과 같다.

function updateMap() {
	for (let y = map.length - 1; y >= 0; y--) {
		for (let x = 0; x < map[y].length; x++) {
			updateTile(x, y);
		}
	}
}

function updateTile(x: number, y: number) {
	...
}