리팩터링 정리 chapter7

7장 캡슐화

모듈을 분리하는 가장 중요한 기준은 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내지 않아야 할 비밀을 얼마나 잘 숨기느냐에 있을 것이다.

7장에 나오는 리팩터링 기법들

  • 대표적 형태의 데이터구조: 레코드 캡슐화, 컬렉션 캡슐화
  • 기본형 데이터의 경우: 기본형을 객체로 바꾸기
  • 정확한 순서로 계산이 필요할 때: 임시 변수를 질의 함수로 바꾸기
  • 추출/인라인하기의 클래스 버전: 클래스 추출하기, 클래스 인라인하기
  • 클래스 연결 관계를 은닉: 위임숨기기
  • 위임 숨기기의 반대기법: 중개자 제거하기
  • 함수도 구현을 캡슐화할 때: 함수 추출하기, 알고리즘 교체하기

7.1 레코드 캡슐화하기

절차

  1. 레코드를 담은 변수를 캡슐화한다.
    -> 레코드를 캡슐화하는 함수의 이름은 검색하기 쉽게 지어준다.
  2. 레코드를 감싼 단순한 클래스로 해당 변수의 내용을 교체한다. 이 클래스에 원본 레코드를 반환하는 접근자도 정의하고, 변수를 캡슐화하는 함수들이 이 접근자를 사용하도록 수정한다.
  3. 테스트한다.
  4. 원본 레코드 대신 새로 정의한 클래스 타입의 객체를 반환하는 함수들을 새로 만든다.
  5. 레코드를 반환하는 예전 함수를 사용하는 코드를 4단계에서 만든 새 함수를 사용하도록 바꾼다.

예시

// before
organization = {name: "애크미 구스베리", country: "GB"}

// after
class Organization {
  constructor(data) {
    this._name = data.name;
    this._country = data.country;
  }
  get name() {return this._name;}
  set name(arg) {this._name = arg;}
  get country() {return this._country;}
  set country(arg) {this._country = arg;}

7.2 컬렉션 캡슐화하기

절차

  1. 아직 컬렉션을 캡슐화하지 않았다면 변수 캡슐화하기부터 한다.
  2. 컬렉션에 원소를 추가/제거하는 함수를 추가한다
    -> 컬렉션 자체를 통째로 바꾸는 세터는 제거한다. 세터를 제거할 수 없다면 인수로 받은 컬렉션을 복제해 저장하도록 한다.
  3. 정적 검사를 수행한다.
  4. 컬렉션을 참조하는 부분을 모두 찾는다. 컬렉션의 변경자를 호출하는 코드가 모두 앞에서 추가한 추가/제거 함수를 호출하도록 수정한다. 하나씩 수정할 때마다 테스트한다.
  5. 컬렉션 게터를 수정해서 원본 내용을 수정할 수 없는 읽기전용 프락시나 복제본을 반환하게 한다.
  6. 테스트한다.

예시

// before
class Person {
  get courses() {return this._courses;}
  set courses(aList) {this._courses = aList;}
}

// after
class Person {
  get courses() {return this._courses.slice()}
  addCourse(aCourse) {}
  removeCourse(aCourse) {}
}

7.3 기본형을 객체로 바꾸기

절차

  1. 아직 변수를 캡슐화하지 않았다면 캡슐화한다.
  2. 단순한 값 클래스를 만든다. 생성자는 기존 값을 인수로 받아서 저장하고, 이 값을 반환하는 게터를 추가한다.
  3. 정적 검사를 수행한다.
  4. 값 클래스의 인스턴스를 새로 만들어서 필드에 저장하도록 세터를 수정한다. 이미 있다면 필드의 타입을 적절히 변경한다.
  5. 새로 만든 클래스의 게터를 호출한 결과를 반환하도록 게터를 수정한다.
  6. 테스트한다.
  7. 함수의 이름을 바꾸면 원본 접근자의 동작을 더 잘 드러낼 수 있는지 검토한다.

예시

// before
orders.filter(o => "high" === o.priority || "rush" === o.priority)

// after
orders.filter(o => o.priority.higherTahn(new Priority("normal")))

7.4 임시 변수를 질의 함수로 바꾸기

절차

  1. 변수가 사용되기 전에 값이 확실히 결정되는지, 변수를 사용할 때마다 계산 로직이 매번 다른 결과를 내지는 않는지 확인한다.
  2. 읽기전용으로 만들 수 있는 변수는 읽기전용으로 만든다.
  3. 테스트한다.
  4. 변수 대입문을 함수로 추출한다.
    -> 변수와 함수가 같은 이름을 가질 수 없다면 함수 이름을 임시로 짓는다. 또한, 추출한 함수가 부수효과를 일으키지 않는지 확인한다. 부수효과가 있다면 질의 함수와 변경 함수 분리하기로 대처한다.
  5. 테스트한다.
  6. 변수 인라인하기로 임시변수를 제거한다.

예시

// before
const basePrice = this._quantity * this._itemPrice;
if (basePrice > 1000) {
  return basePrice * 0.95;
} else {
  return basePrice * 0.98;
}

// after
get basePrice() {this._quantity * this._itemPrice;}
if (this.basePrice > 1000) {
  return this.basePrice * 0.95;
} else {
  return this.basePrice * 0.98;
}

7.5 클래스 추출하기

절차

  1. 클래스의 역할을 분리할 방법을 정한다.
  2. 분리될 역할을 담당할 클래스를 새로 만든다.
  3. 원래 클래스의 생성자에서 새로운 클래스의 인스턴스를 생성하여 필드에 저장해둔다.
  4. 분리될 역할에 필요한 필드들을 새 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
  5. 메서드들도 새 클래스로 옮긴다. 이때 저수준 메서드, 즉 다른 메서드를 호출하기 보다는 호출을 당하는 일이 많은 메서드부터 옮긴다. 하나씩 옮길때마다 테스트한다.
  6. 양쪽 클래스의 인터페이스를 살펴보면서 불필요한 메서드를 제거하고, 이름도 새로운 환경에 맞게 바꾼다.
  7. 새 클래스를 외부로 노출할지 정한다. 노출하려거든 새 클래스에 참조를 값으로 바꾸기를 적용할지 고민해본다.

예시

// before
class Person {
  get officeAreaCode() {return this._officeAreaCode;}
  get officeNumber() {return this._officeNumber;}
}
// after
class Person {
  constructor() {
    this._telephonNumber = new TelephoneNumber()
  }
  
  get officeAreaCode() {return this._telephonNumber.areaCode;}
  get officeNumber() {return this._telephoneNumber.number;}
}
class TelephoneNumber {
  get areaCode() {return this._areacode;}
  get number() {return this._number;}
}

7.6 클래스 인라인하기

절차

  1. 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성한다. 이 메서드들은 단순히 작업을 소스 클래스로 위임해야 한다.
  2. 소스 클래시의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 바꾼다. 하나씩 바꿀 때마다 테스트한다.
  3. 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮긴다. 하나씩 옮길 때마다 테스트한다.
  4. 소스 클래스를 삭제하고 조의를 표한다.

예시

// before
class Person {
  constructor() {
    this._telephonNumber = new TelephoneNumber()
  }
  
  get officeAreaCode() {return this._telephonNumber.areaCode;}
  get officeNumber() {return this._telephoneNumber.number;}
}
class TelephoneNumber {
  get areaCode() {return this._areacode;}
  get number() {return this._number;}
}

  
// after
class Person {
  get officeAreaCode() {return this._officeAreaCode;}
  get officeNumber() {return this._officeNumber;}
}

7.7 위임 숨기기

절차

  1. 위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성한다.
  2. 클라이언트가 위임 객체 대신 서버를 호출하도록 수정한다. 하나씩 바꿀때마다 테스트한다.
  3. 모두 수정했다면, 서버로부터 위임객체를 얻는 접근자를 제거한다.
  4. 테스트한다.

예시

// before
manager = aPerson.department.manager;

// after
manager = aPerson.manager;

class Person {
  get manager() {return this.department.manager;} // 중개자 getter 생성
}

7.8 중개자 제거하기

절차

  1. 위임 객체를 얻는 게터를 만든다.
  2. 위임 메서드를 호출하는 클라이언트가 모두 이 게터를 거치도록 수정한다. 하나씩 바꿀때마다 테스트한다.
  3. 모두 수정했다면 위임 메서드를 삭제한다.
    -> 자동 리팩터링 도구를 사용할 때는 위임 필드를 캡슐화한 다음, 이를 사용하는 모든 메서드를 인라인 한다.

예시

// before
manager = aPerson.manager;

class Person {
  get manager() {return this.department.manager;} // 중개자 getter 생성
}

// after
manager = aPerson.department.manager;

7.9 알고리즘 교체하기

절차

  1. 교체할 코드를 함수 하나에 모은다.
  2. 이 함수만을 이용해 동작을 검증하는 테스트를 마련한다.
  3. 대체할 알고리즘을 준비한다.
  4. 정적 검사를 수행한다.
  5. 기존 알고리즘과 새 알고리즘의 결과를 비교하는 테스트를 수행한다. 두 결과가 같다면 리팩터링이 끝난다. 그렇지 않다면 기존 알고리즘을 참고해서 새 알고리즘을 테스트하고, 디버깅한다.

예시

// before
function foundPerson(people) {
  for(let i = 0; i < people.length; i++) {
    if (people[i] == "Don") {
      return "Don";
    }
    if (people[i] == "John") {
      return "John";
    }
    if (people[i] == "Kay") {
      return "Kay";
    }
    return "";
    
// after
function foundPerson(people) {
  const candidates = ["Don", "John", "Kay"]
  return people.find(p => candidates.includes(p)) || '' ;

좋은 웹페이지 즐겨찾기