개발/리팩터링

파이브 라인스 오브 코드 - 긴 코드 조각내기

DinoDev 2023. 7. 3. 11:09
728x90
반응형

규칙 - 왜 다섯 줄인가?

이 책에서는 근본적인 규칙으로 다섯 줄 제한을 소개합니다.

메서드는 {와 }를 제외하고 5줄 이상이 되면 안된다고 이야기 합니다. 

아래 isTrue() 메서드는 4줄 입니다.

function isTrue(bool: boolean) {
  if (bool)
    return true;
  else return false;
}

메서드가 길면 한 번에 긴 메서드의 모든 논리를 머릿속에 담아야하기 때문에 작업하기가 어렵습니다.

또한 메서드 이름만 보고 어떤 역할을 하는지 유추가 되기 때문에 주석을 넣는 것과 같은 효과를 낼 수 있습니다.

 

draw() 메서드는 매우 길지만 내부를 잘 보면 map을 그리는 영역과 player를 그리는 영역이 존재하고 있습니다. 우리는 메서드 추출 이라는 리팩터링을 통해서 함수의 라인 수를 줄일 수 있습니다.

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

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

  // Draw map
  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);
    }
  }

  // Draw player
  g.fillStyle = "#ff0000";
  g.fillRect(playerx * TILE_SIZE, playery * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}

drawMap과 drawPlayer로 메서드를 추출 했습니다.

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) {
  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 drawPlayer(g: CanvasRenderingContext2D) {
  g.fillStyle = "#ff0000";
  g.fillRect(playerx * TILE_SIZE, playery * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}

메서드 추출

이 책에서는 직접 손으로 하는게 안전한 방법이라고 소개하고 있는데 사람의 손을 타면 버그가 발생할 수 있는 여지가 생깁니다. IDE에서 제공하는 기계적인 메서드 추출 하는 것을 추천합니다.

메서드 추출을 하다 보면 인스턴스의 값을 변경하는 케이스가 생기는데 파라미터로 전달한 인스턴스를 변경하는 것 보다 메서드 내에서 새로운 인스턴스를 만드는 deep copy를 진행하고 이 인스턴스를 리턴하는 방식이 안전한 방식입니다.

규칙 - 호출 또는 전달, 한 가지만 할 것

함수 내에서는 객체에 있는 메서드를 호출하거나 객체를 인자로 전달할 수 있지만 둘을 섞어 사용해서는 안 됩니다.

function average(arr: number[]) {
  return sum(arr) / arr.length;
}

average 메서드는 sum() 메서드에 객체를 전달하는 것과 arr.length 처럼 객체에 있는 메서드를 호출하는 두가지 작업을 섞어서 하고 있습니다.

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

메서드 내에서는 동일한 추상화 수준을 유지해야한다고 합니다. 이렇게 arr을 전달하기만 할 것인지, arr객체의 메서드를 호출만 할 것인지 정해야 합니다.

 

이 책 예제에 있는 작업 중에 g를 만드는 작업인 createGraphics()함수를 만들어서 내부 추상화 수준을 동일하게 유지할 수 있습니다.

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

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

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 drawPlayer(g: CanvasRenderingContext2D) {
  g.fillStyle = "#ff0000";
  g.fillRect(playerx * TILE_SIZE, playery * TILE_SIZE, TILE_SIZE, TILE_SIZE);
}

좋은 함수 이름의 속성

좋은 이름이 가져야 할 몇 가지 속성은 아래와 같습니다.

  • 함수의 의도를 잘 설명해야 합니다.
  • 함수가 하는 모든 것을 담아야 합니다.
  • 도메인에서 일하는 사람이 이해할 수 있는 단어를 사용해야 합니다.

규칙 - if 문은 함수의 시작에만 배치

if 문이 있는 경우 해당 if 문은 함수의 첫 번째 항목이어야 합니다.

if는 무언가를 확인하는 한 가지 일을 뜻하는 것으로 함수는 한 가지 일을 해야하기 때문에 함수를 추출하는 것이 맞습니다.

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

reportPrimes()는 1. 숫자를 반복한다 2. 숫자가 소수인지 확인한다 와 같은 두 가지 작업을 진행하고 있습니다.

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

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

이런 if 문의 메서드 추출은 else if와 else까지를 하나의 작업으로 보고 추출을 해야 합니다.

728x90
반응형