Chapter08. 기능 이동

김신영·2023년 7월 17일
0

Refactoring

목록 보기
8/12
post-thumbnail

함수 옮기기 (Move Function)

Move Function
image

  • 대상 함수를 호출하는 함수들은 무엇인지?
  • 대상 함수가 호출하는 함수들은 무엇이 있는지?
  • 대상 함수가 사용하는 데이터는 무엇인지?

절차

  1. 선택한 함수가 현재 컨텍스트에서 사용 중인 모든 프로그램 요소를 살펴본다.
    • 이 요소들 중에서도 함께 옮겨야 할 게 있는지 고민해본다.
    • 얽혀 있는 함수가 여러 개라면, 다른 곳에 미치는 영향이 적은 함수부터 옮기도록 하자.
    • 하위 함수들의 호출자가 고수준 함수 하나 뿐이라면, 먼저 하위 함수들을 고수준 함수에 인라인한 다음, 고수준 함수를 옮기고, 옮긴 위치에서 개별 함수들로 다시 추출하자.
  2. 선택한 함수가 다형 메서드인지 확인한다.
  3. 선택한 함수를 Target 컨텍스트로 복사한다. Target 함수가 새로운 터전에 잘 자리 잡도록 다듬는다.
    • 함수 본문에서 Source 컨텍스트 요소를 사용한다면, 해당 요소들을 매개변수로 넘기거나 Source 컨텍스트 자체를 참조로 넘겨준다.
  4. 정적 분석을 수행한다.
  5. Source 컨텍스트에서 Target 함수를 참조할 방법을 찾아 반영한다.
  6. Source 함수를 Target 함수의 위임 함수가 되도록 수정한다.
  7. 테스트한다.
  8. Source 함수를 인라인할지 고민한다.

예시

  • Before
    class Account {
      get overdraftCharge() {...}
  • After
    class AccountType {
        get overdraftCharge() {...}

필드 옮기기 (Move Field)

Move Field

image

절차

  1. Source 필드가 캡슐화되어 있지 않다면 캡슐화한다.
  2. 테스트한다.
  3. Target 객체에 필드를 생성한다.
  4. 정적 검사를 수행한다.
  5. Source 객체에서 Target 객체를 참조할 수 있는지 확인한다.
  6. 접근자들이 Target 필드를 사용하도록 수정한다.
  7. 테스트한다.
  8. Source 필드를 제거한다.
  9. 테스트한다.

예시

  • Before
    class Customer {
      get plan() {return this._plan;}
      get discountRate() {return this._discountRate;}
  • After
    class Customer {
      get plan() {return this._plan;}
      get discountRate() {return this.plan.discountRate;}

문장을 함수로 옮기기 (Move Statements into Function)

Move Statements into Function

image

반대 리팩터링: 문장을 호출한 곳으로 옮기기

절차

  1. 반복 코드가 함수 호출 부분과 멀리 떨어져 있다면 문장 슬라이드하기를 적용해 근처로 옮긴다.
  2. Target 함수를 호출하는 곳이 한 곳뿐이면, 단순히 소스 위치에서 해당 코드를 잘라내어 피호출 함수로 복사하고 테스트한다.
    • 이 경우라면 나머지 단계는 무시한다.
  3. 호출자가 둘 이상이면 호출자 중 하나에서 Target 함수 호출 부분과 그 함수로 옮기려는 문장들을 함께 다른 함수로 추출한다.
  4. 다른 호출자 모두가 방금 추출한 함수를 사용하도록 수정한다.
    • 하나씩 수정할 때마다 테스트한다.
  5. 모든 호출자가 새로운 함수를 사용하게 되면 원래 함수를 새로운 함수 안으로 인라인한 후 원래 함수를 제거한다.
  6. 새로운 함수의 이름을 원래 함수의 이름으로 바꿔준다.

예시

  • Before
    result.push(`<p>title: ${person.photo.title}</p>`);
    result.concat(photoData(person.photo));
    
    function photoData(aPhoto) {
      return [
        `<p>location: ${aPhoto.location}</p>`,
        `<p>date: ${aPhoto.date.toDateString()}</p>`,
      ];
    }
  • After
    result.concat(photoData(person.photo));
    
    function photoData(aPhoto) {
      return [
        `<p>title: ${aPhoto.title}</p>`,
        `<p>location: ${aPhoto.location}</p>`,
        `<p>date: ${aPhoto.date.toDateString()}</p>`,
      ];
    }

문장을 호출한 곳으로 옮기기 (Move Statements to Callers)

Move Statements to Callers

image

반대 리팩터링: 문장을 함수로 옮기기

절차

  1. 호출자가 한두 개뿐이고 피호출 함수도 간단한 단순한 상황이면, 피호출 함수의 처음(혹은 마지막) 줄들을 잘라내어 호출자로 복사해 넣는다.
    • 테스트만 통과하면, 이후 단계는 필요없다.
  2. 더 복잡한 상황에서는, 이동하지 않길 원하는 모든 문장을 함수로 추출한다.
  3. 원래 함수를 인라인한다.
  4. 추출된 함수의 이름을 원래 함수의 이름으로 변경한다.

예시

  • Before
    emitPhotoData(outStream, person.photo);
    
    function emitPhotoData(outStream, photo) {
      outStream.write(`<p>title: ${photo.title}</p>\n`);
      outStream.write(`<p>location: ${photo.location}</p>\n`);
    }
  • After
    emitPhotoData(outStream, person.photo);
    outStream.write(`<p>location: ${person.photo.location}</p>\n`);
    
    function emitPhotoData(outStream, photo) {
      outStream.write(`<p>title: ${photo.title}</p>\n`);
    }

인라인 코드를 함수 호출로 바꾸기 (Replace Inline Code with Function Call)

Replace Inline Code with Function Call

image

절차

  1. 인라인 코드를 함수 호출로 대체한다.
  2. 테스트 한다.

예시

  • Before
    let appliesToMass = false;
    for(const s of states) {
      if (s === "MA") appliesToMass = true;
    }
  • After
    appliesToMass = states.includes("MA");

문장 슬라이드하기 (Slide Statements)

Slide Statements

image

참고

bliki: CommandQuerySeparation

절차

  1. 코드 조각을 이동할 목표 위치를 찾는다.
    • 코드 조각의 원래 위치와 목표 위치 사이의 코드들을 훑어보면서, 조각을 모으고 나면 동작이 달라지는 코드가 있는지 살핀다.
    • 다음과 같은 간섭이 있다면 이 리팩터링을 포기한다.
      • 코드 조각에서 참조하는 요소를 선언하는 문장 앞으로는 이동할 수 없다.
      • 코드 조각을 참조하는 요소의 뒤로는 이동할 수 없다.
      • 코드 조각에서 참조하는 요소를 수정하는 문장을 건너뛰어 이동할 수 없다.
      • 코드 조각이 수정하는 요소를 참조하는 요소를 건너뛰어 이동할 수 없다.
  2. 코드 조각을 원래 위치에서 잘라내어 목표 위치에 붙여 넣는다.
  3. 테스트한다.

예시

  • Before
    const pricingPlan = retrievePricingPlan();
    const order = retreiveOrder();
    let charge;
    const chargePerUnit = pricingPlan.unit;
  • After
    const pricingPlan = retrievePricingPlan();
    const chargePerUnit = pricingPlan.unit;
    const order = retreiveOrder();
    let charge;

반복문 쪼개기 (Split Loop)

Split Loop

image

절차

  1. 반복문을 복제해 두 개로 만든다.
  2. 반복문을 중복되어 생기는 부수효과를 파악해서 제거한다.
  3. 테스트한다.
  4. 완료됐으면, 각 반복문을 함수로 추출할지 고민해본다.

예시

  • Before
    let averageAge = 0;
    let totalSalary = 0;
    for (const p of people) {
      averageAge += p.age;
      totalSalary += p.salary;
    }
    averageAge = averageAge / people.length;
  • After
    let totalSalary = 0;
    for (const p of people) {
      totalSalary += p.salary;
    }
    
    let averageAge = 0;
    for (const p of people) {
      averageAge += p.age;
    }
    averageAge = averageAge / people.length;

반복문을 파이프라인으로 바꾸기 (Replace Loop with Pipeline)

Replace Loop with Pipeline

image

참고

Refactoring with Loops and Collection Pipelines

절차

  1. 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
  2. 반복문의 첫 줄부터 시작해서, 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다.
    • 이때 컬렉션 파이프라인 연산은 1번에서 만든 반복문 컬렉션 변수에서 시작하여, 이전 연산의 결과를 기초로 연쇄적으로 수행된다.
    • 하나를 대체할 때마다 테스트한다.
  3. 반복문의 모든 동작을 대체했다면 반복문 자체를 지운다.

예시

  • Before
    const names = [];
    for (const i of input) {
      if (i.job === "programmer")
        names.push(i.name);
    }
  • After
    const names = input
      .filter(i => i.job === "programmer")
      .map(i => i.name)
    ;

죽은 코드 제거하기 (Remove Dead Code)

Remove Dead Code

image

절차

  1. 죽은 코드를 외부에서 참조할 수 있는 경우라면, 혹시라도 호출하는 곳이 있는지 확인한다.
  2. 없다면 죽은 코드를 제거한다.
  3. 테스트한다.

예시

  • Before
    if(false) {
      doSomethingThatUsedToMatter();
    }
  • After
profile
Hello velog!

1개의 댓글

comment-user-thumbnail
2023년 7월 18일

너무 좋은 글이네요. 공유해주셔서 감사합니다.

답글 달기