cleanCode/FiveLinesOfCode

[FiveLinesOfCode] 3장 : 긴 코드 조각내기

Mo_bi!e 2023. 10. 3. 20:32

3장 : 긴 코드 조각내기

의문점

  • 불변속성(invariant), 가정설정문(assertion)
  • 디자인패턴 → 상속보다는 컴포지션, 컴포지션이 컴파일 오류 발생 만듬?

학습목표

  1. 다섯줄 제한(FIVE LINES)으로 지나치게 긴 메서드 식별하기란?
  2. 세부사항을 보지 않고 코드 작업하기란?
  3. 메서드 추출(EXTRACT METHOD)로 긴 메소드 분해하기란?
  4. 호출 또는 전달, 한가지만할것(EITHER CALL OR PASS)으로 추상화 수준에 맞추기란?
  5. if 문은 함수의 시작에만 배치로 if문 분리하기란?

키워드

  • DRY, KISS 지침을 따른경우라도 코드는 여전히 혼란스러움

1. 다섯줄 제한(FIVE LINES)으로 지나치게 긴 메서드 식별하기란?

  • 정의
    • {} 제외 if, for, while, 세미콜론끝나는 모든것은 5줄이상이면 안됨 → 줄수 제한 자체가 중요
    • 도우미 메서드 만들기
  • 스멜
    • 메서드가 길다는것 자체가 스멜
    • 길다는것?
      • 긴논리를 머리속에 담아야함
      • 메서드는 한가지 작업만 해야한다
  • 의도
    • 장점
      1. 각 메서드의 이름으로 코드의 의도를 전달가능
      2. 5줄마다 주석을 넣는것과 같음
      3. 작은메스드 이름을 적절하게 붙이면 → 큰함수 이름 지정에 도움

2. 세부사항을 보지 않고 코드 작업하기란?

  • 형태살피기 → 그룹을 식별하기
    • 그룹을 명확히 하기 위해 그룹이라 생각하는것에 ‘빈줄’을 추가함
    • 추가한 빈줄에다가 ‘주석’을 달아줌

3. 메서드 추출(EXTRACT METHOD)로 긴 메소드 분해하기란?

  • 정의

    • 한 메서드의 일부를 취해서 자체 메서드로 추출함
  • 절차

    • 그룹식별(빈줄 주석 표시)
      → (빈메서드 만들기, 그룹위 빈메서드 호출, 빈메서드에 그룹 넣기, 컴파일)
      →(호출하는 쪽에 오류발생시킴, 반환값 할당, 컴파일)
      → 호출시 인자오류 바로잡기
      → 빈줄 주석 제거
  • 과정

    1. 주석제거 메소드 개선과 추출

      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);
      }
      
      function draw() {
        let canvas = document.getElementById("GameCanvas") as HTMLCanvasElement;
        let g = canvas.getContext("2d");
      
        g.clearRect(0, 0, canvas.width, canvas.height);
      
      // -> 빈줄과 주석 제거 (각 함수화 자체가 역할을 이미 함)
        drwaMap(g);
        drawPlayer(g);
      
      }
      
      //매개변수 에러 발생 -> 'npx tsc' 로 컴파일러로 알수 있음
      function drwaMap(){
        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);
      }
    2. 서로 다른 수준의 추상화가 섞여있는 문제를 찾아냄

  • 말:

    • 고급스러워 보이려고 노력하다가 어려움을 겪을 수 있는데, 보통은 그럴만한 가치가 없습니다
    • 이 절차는 아무것도 손상시키지않습니다.
      • 아무것도 고장내지 않았다는 확신이 완벽한 결과보다 더 가치가 있습니다

4. 호출 또는 전달, 한가지만할것(EITHER CALL OR PASS)으로 추상화 수준에 맞추기란?

  • 정의
    • 함수 내에서 객체(e.g. 배열)에 있는 메서드 호출하거나, 객체를 인자로 전달할 수 있음
    • 즉 코드를 직접 조작하는 낮은 수준의 작업(e.g. 낮은 수준의 arr.length)과 다른 함수에 인자를 전달하는 높은 수준의 호출(e.g. 높은 수준의 추상화 sum(arr)) 공존
      → 메서드 이름사이 불일치
      → 가독성 하락
    • (MO : 추상화 수준 높은것은 값을 미리 예측할수없는 즉 매개변수를 던져주는 방식 → 즉 미리 알수l없음)
  • 스멜
    • 매우 강력한 스멜
    • 식별은 인자로 전달된 변수 옆 .(점)
  • 의도
    • 메서드 내부에 추상화 수준이 항상 동일하게 유지됨
function draw() {
    let canvas = document.getElementById("GameCanvas");

        //추출 했다고 하더라도 여기도 낮은수준 추상화임
    let g = canvas.getContext("2d");

    //현재 상태 -> 추상화 단계가 다름 (각 아래 1,2)

        //1. 직접 조작하는 낮은수준 추상화
    g.clearRect(0, 0, canvas.width, canvas.height); 

        //2. 다른함수에 인자를 전달하는 높은 수준의 추상화
    drwaMap(g); 
    drawPlayer(g);
}

이런 경우 clearRect 를 추출하면되지만 다시 canvas.getContext 여기도 마찬가지임

→ 대신 최초 3줄을 추출하자

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

function draw() {
  let g = createGraphics(); //return 받아주기
  drwaMap(g);
  drawPlayer(g);
}
  • 반복

    //while 은 두개로 덩어리가 보임
    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;
          }
        }
      }
    }
    
  • 좋은 함수 이름의 속성 (도종환)

    • 도 : 도메인에서 일하는 사람이 이해할 수있어야

    • 종 : 정(종)직 해야함 → 함수의 의도를 설명

    • 환 : 환(완)전해야함 → 함수의 모든것을 포괄해야함

    • 구체적 방법?

      • 그룹별 지배적인 단어가 무엇인지?
      • 항상 나중에 함수가 더 작아졌을 때 이름개선여부 평가
//update 메소드 분리
function update() {
//각각은 복잡해서 이해하기 번거롭 -> 지배적인단어가 무엇?
// 각 input, Map -> 함수명 지정
  handleInput();
  updateMap();
}

function handleInput(){
  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);
  }
}

function updateMap(){
  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;
      }
    }
  }
}

5. if 문은 함수의 시작에만 배치로 if문 분리하기란? (너무 많은 일을 하는 함수 분리하기)

  • 정의

    • if문은 함수의 첫번째 항목이어야함
    • If 문은 무언가를 확인하는 것이고 확인은 한가지 일이다 (아래 코드1 참고)
      • 결국 if문은 함수의 첫번째 항목일수밖에 없고, 다른 일은 못하므로 if문은 유일해야함
    • 또한 if문 {} 내 본문추출과 else 문과 분리하면 안됨 → 구조의 일부임
  • 스멜

    • 이 규칙은 함수가 한가지 이상의 작업 수행 막기위함
  • 의도

    • if문은 하나의 작업이다. else if 는 if문과 분리할수 없는 원자 단위임
  • if문은 함수의 시작에만 배치

    function update() {
      while (inputs.length > 0) {
        let current = inputs.pop();
    
    // 코드 1
    // 조건문으로 무언가를 확인하는 것은 한가지의 일임 
    // -> 그러므로 함수의 첫번째 항목이어야하며, 유일한것이어야 함
        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);
      }
    function handleInputs(){
      while (inputs.length > 0) {
        let current = inputs.pop();
        handleInput(current)
      }
    }
    
    // 새로운 이름을 매개변수에 지정해서 가독성을 높일 수있음
    //function handleInput(current: Input){
    function handleInput(input: Input){
      if (input === Input.LEFT)
        moveHorizontal(-1);
      else if (input === Input.RIGHT)
        moveHorizontal(1);
      else if (input === Input.UP)
        moveVertical(-1);
      else if (input === Input.DOWN)
        moveVertical(1);
    }

요약

  1. 다섯줄 제한(FIVE LINES)으로 지나치게 긴 메서드 식별하기란?

    • 두가지 이상 작업 수행하는 메서드 식별 목적
  2. 세부사항을 보지 않고 코드 작업하기란?

  • 덩어리로 보기
  1. 메서드 추출(EXTRACT METHOD)로 긴 메소드 분해하기란?
    • (다섯줄제한, 추상화수준 맞추기, if문 함수시작에만 배치)의 결과 → 매개변수 이름 변경이점도 있음 → 가독성 향상
  2. 호출 또는 전달, 한가지만할것(EITHER CALL OR PASS)으로 추상화 수준에 맞추기란?
    • 여러 수준의 추상화가 있는 메서드 식별에 도움이 됨 → 추상화 수준 맞추기 가능
  3. if 문은 함수의 시작에만 배치로 if문 분리하기란?
    • 조건문 확인이 한가지의 작업임 → 메서드 추출

+: 마틴파울러 리팩터링과 추출하는 구조적 패턴이 비슷함 → 실무에서는 IDE도움을 받음 (단축키 촥촥촥)