[리팩터링 2판] - 상속 다루기

Lee Jeong Min·2022년 9월 26일
1

리팩터링 2판

목록 보기
12/12
post-thumbnail

리팩터링 2판의 Chatper 12를 보고 정리한 글입니다.

이 장에서는 객체 지향 프로그래밍에서 가장 유명한 특성인 상속을 다룬다. 이 상속은 유용한 동시에 오용하기 쉬운데, 이와 관련한 리팩터링 기법들을 알아보자.

메서드 올리기

반대 리팩터링: 메서드 내리기

배경

메서드들의 본문 코드가 똑같을 때, 중복이 발생하게 된다. 이러한 중복은 한쪽의 변경이 다른쪽에는 반영되지 않을 수 있다는 위험을 항상 수반한다. 이를 위해 메서드 올리기를 적용하여 리팩터링을 진행해보자.

절차

  1. 똑같이 동작하는 메서드인지 면밀히 살펴본다.
  2. 메서드 안에서 호출하는 다른 메서드와 참조하는 필드들을 슈퍼클래스에서도 호출하고 참조할 수 있는지 확인한다.
  3. 메서드 시그니처가 다르다면 함수 선언 바꾸기로 슈퍼클래스에서 사용하고 싶은 형태로 통일한다.
  4. 슈퍼클래스에 새로운 메서드를 생성하고, 대상 메서드의 코드를 복사해넣는다.
  5. 정적 검사를 수행한다.
  6. 서브클래스 중 하나의 메서드를 제거한다.
  7. 테스트한다.
  8. 모든 서브클래스의 메서드가 없어질 때까지 다른 서브클래스의 메서드를 하나씩 제거한다.

예시

리팩터링 전

두 서브클래스에서 같은 일을 수행하는 메서드 발견

class Employee extends Party {
  get annualCost() {
    return this.monthlyCost * 12;
  }
}

class Department extends Party {
  get totalAnnualCost() {
    return this.monthlyCost * 12;
  }
}

리팩터링 후

// 슈퍼클래스에 메서드를 붙여 넣자.
class Party {
  get annualCost() {
    return this.monthlyCost * 12;
  }
}

monthlyCost()를 Party에선 구현해 놓지 않았는데, JS가 동적 언어여서 잘 동작한다. 이럴 때는 함정 메서드(monthlyCost 메서드 정의로 에러를 던지는 코드)를 Party 클래스에 구현해 놓고 사용하면 서브클래스가 monthlyCost()를 구현해야 한다는 사실을 알려줄 수 있다.

필드 올리기

반대 리팩터링: 필드 내리기

배경

서브클래스들이 독립적으로 개발되었거나 뒤늦게 하나의 계층구조로 리팩터링된 경우라면 일부 기능이 중복되어 있을 때가 있다. 이러한 경우 분석 결과 필드들이 비슷한 방식으로 쓰인다고 판단되면 슈퍼클래스로 끌어올리자.

이렇게 하면 데이터 중복 선언을 없앨 수 있고, 해당 필드를 사용하는 동작을 서브 클래스에서 슈퍼클래스로 옮길 수 있다.

절차

  1. 후보 필드들을 사용하는 곳 모두가 그 필드들을 똑같은 방식으로 사용하는지 면밀히 살핀다.
  2. 필드들의 이름이 각기 다르다면 똑같은 이름으로 바꾼다.
  3. 슈퍼클래스에 새로운 필드를 생성한다.
  4. 서브클래스의 필드들을 제거한다.
  5. 테스트한다.

예시

리팩터링 전

class Employee {...} // 자바코드

class SalesPerson extends Employee {
  private String name;
}

class Engineer extends Employee {
  private String name;
}

리팩터링 후

class Employee {
  protected String name;  
}

class SalesPerson extends Employee {...}
class Engineer extends Employee {...}

생성자 본문 올리기

배경

생성자는 다루기 까다롭지만, 서브클래스들에게서 기능이 같은 것들을 발견하고, 생성자로 옮길 수 있는 경우 생성자로 옮겨보자.

절차

  1. 슈퍼클래스에 생성자가 없다면 하나 정의한다. 서브클래스의 생성자들에게서 이 생성자가 호출되는지 확인한다.
  2. 문장 슬라이드하기로 공통 문장 모두 super() 호출 직후로 옮긴다.
  3. 공통 코드를 슈퍼클래스에 추가하고 서브클래스들에게서는 제거한다. 생성자 매개변수 중 공통 코드에서 참조하는 값들을 모두 super()로 건넨다.
  4. 테스트한다.
  5. 생성자 시작 부분으로 옮길 수 없는 공통 코드에는 함수 추출하기와 메서드 올리기를 차례로 적용한다.

예시

리팩터링 전

class Party {}

class Employee extends Party {
  constructor(name, id, monthlyCost) {
    super();
    this._id = id;
    this._name = name;
    this._monthlyCost = monthlyCost;
  }

  // 생략
}

class Department extends Party {
  constructor(name, staff) {
    super();
    this._name = name;
    this._staff = staff;
  }

  // 생략
}

여기서 name을 초기화하는 공통 코드를 발견할 수 있다. 이를 생성자로 옮겨보자.

리팩터링 후

class Party {
  constructor(name) {
    this.name = name;
  }
}

class Employee extends Party {
  constructor(name, id, monthlyCost) {
    super(name);
    this._id = id;
    this._monthlyCost = monthlyCost;
  }

  // 생략
}

class Department extends Party {
  constructor(name, staff) {
    super(name);
    this._staff = staff;
  }

  // 생략
}

위 예제와 달리 공통 코드가 나중에 오는 경우, 함수로 추출한뒤 슈퍼클래스로 옮겨서 사용하는 식으로 리팩터링을 진행할 수 있다.

메서드 내리기

반대 리팩터링: 메서드 올리기

배경

특정 서브클래스 하나와만 관련된 메서드는 슈퍼클래스에서 제거하고 해당 서브클래스들에 추가하는 편이 깔끔하다.

해당 기능을 제공하는 서브클래스가 정확히 무엇인지를 호출자가 알고 있을 때만 적용할 수 있다.

절차

  1. 대상 메서드를 모든 서브클래스에 복사한다.
  2. 슈퍼클래스에서 그 메서드를 제거한다.
  3. 테스트한다.
  4. 이 메서드를 사용하지 않는 모든 서브클래스에서 제거한다.
  5. 테스트한다.

예시

리팩터링 전

class Employee {
  get quota() {...}
}

class Engineer extends Employee {...}
class Salesperson extends Employee {...}

리팩터링 후

class Employee {...}

class Engineer extends Employee {...}
class Salesperson extends Employee {
  get quota() {...}
}
                                 

필드 내리기

반대 리팩터링: 필드 올리기

배경

서브클래스 하나에서만 사용하는 필드는 해당 서브클래스로 옮긴다.

절차

  1. 대상 필드를 모든 서브클래스에 정의한다.
  2. 슈퍼클래스에서 그 필드를 제거한다.
  3. 테스트한다.
  4. 이 필드를 사용하지 않는 모든 서브클래스에서 제거한다.
  5. 테스트한다.

예시

리팩터링 전

class Employee { // 자바코드
  private String quota;
}

class Engineer extends Employee {...}
class Salesperson extends Employee {...}

리팩터링 후

class Employee {...}

class Engineer extends Employee {...}
class Salesperson extends Employee {
  protected String quota;
}

타입 코드를 서브클래스로 바꾸기

반대 리팩터링: 서브 클래스 제거하기

배경

소프트웨어 시스템에서는 비슷한 대상들을 특정 특성에 따라 구분해야 할 때가 자주 있는데 이를 다루는 수단으로 타입 코드 필드가 있다.

이러한 타입코드를 사용하는 곳에서 서브클래스는 아래와 같은 2가지 면에서 매력적이다.

  1. 조건에 따라 다르게 동작하도록 해주는 다형성을 제공한다.
  2. 특정 타입에서만 의미가 있는 값을 사용하는 필드나 메서드가 있을 때 좋다.

이 리팩터링은 대상 클래스에 직접 적용하는 경우와, 타입 코드 자체에 적용하는 경우를 고민해야 한다. 예시를 참고하자.

절차

  1. 타입 코드 필드를 자가 캡슐화한다.
  2. 타입 코드 값 하나를 선택하여 그 값에 해당하는 서브클래스를 만든다. 타입 코드 게터 메서드를 오버라이드하여 해당 타입 코드의 리터럴 값을 반환하게 한다.
  3. 매개변수로 받은 타입 코드와 방금 만든 서브클래스를 매핑하는 선택 로직을 만든다.
  4. 테스트한다.
  5. 타입 코드 값 각각에 대해 서브클래스 생성과 선택 로직 추가를 반복한다. 클래스 하나가 완성될 때마다 테스트한다.
  6. 타입 코드 필드를 제거한다.
  7. 테스트한다.
  8. 타입 코드 접근자를 이용하는 메서드 모두에 메서드 내리기와 조건부 로직을 다형성으로 바꾸기를 적용한다.

예시

직접 상속하는 경우

리팩터링 전

class Employee {
  constructor(name, type) {
    this.validateType(type);
    this._name = name;
    this._type = type;
  }

  validateType(arg) {
    if (!['engineer', 'manager', 'salesman'].includes(arg)) {
      throw new Error(`Employee cannot be of type ${arg}`);
    }
  }

  toString() {
    return `${this._name} (${this._type})`;
  }
}

리팩터링 후

클래스에 있는 type을 제거하고, 생성자를 팩터리 함수로 바꿔서 선택 로직을 함수에서 처리하는 모습을 확인할 수 있다.

class Employee {
  constructor(name) {
    this._name = name;
  }

  toString() {
    return `${this._name} (${this.type})`;
  }
}

class Engineer extends Employee {
  get type() {
    return 'engineer';
  }
}
class Salesperson extends Employee {
  get type() {
    return 'salesperson';
  }
}
class Manager extends Employee {
  get type() {
    return 'manager';
  }
}

function createEmployee(name, type) {
  switch (type) {
    case 'engineer':
      return new Engineer(name);
    case 'salesperson':
      return new Salesperson(name);
    case 'manager':
      return new Manager(name);
      default: throw new Error(`Employee cannot be of type ${arg}`);
  }
}

서브클래스들에는 타입 코드 게터가 여전히 남아 있고 이를 제거하고 싶겠지만 이 메서드를 이용하는 코드가 어딘가에 남아 있을 수 있으므로 조건부 로직을 다형성으로 바꾸기와 메서드 내리기로 문제를 해결하자.

간접 상속하는 경우

리팩터링 전

이번에는 직원의 서브클래스로 '아르바이트'와 '정직원'이라는 클래스가 이미 있어서 Employee를 직접 상속하는 방식으로 타입 코드 문제를 대처할 수 없다. 이러한 경우 간접 상속을 이용한다.

class Employee {
  constructor(name, type) {
    this.validateType(type);
    this._name = name;
    this._type = type;
  }

  validateType(arg) {
    if (!['engineer', 'manager', 'salesperson'].includes(arg)) {
      throw new Error(`${arg}라는 직원 유형은 없습니다.`);
    }
  }

  get type() {
    return this._type;
  }

  set type(arg) {
    this._type = arg;
  }

  get capitalizedType() {
    return this._type.charAt(0).toUpperCase() + this._type.substr(1);
  }

  toString() {
    return `${this._name} (${this.capitalizedType})`;
  }
}

리팩터링 후

타입 코드를 객체로 바꾸고, 앞 예시와 같은 방식으로 직원 유형을 리팩터링해보자.

class EmployeeType {
  constructor(aString) {
    this._value = aString;
  }

  toString() {
    return this._value;
  }

  get capitalizedType() {
    return this.toString().charAt(0).toUpperCase() + this.toString().substr(1);
  }
}

class Engineer extends EmployeeType {
  toString() {
    return 'engineer';
  }
}
class Manager extends EmployeeType {
  toString() {
    return 'manager';
  }
}
class Salesperson extends EmployeeType {
  toString() {
    return 'salesperson';
  }
}

class Employee {
  constructor(name, type) {
    this.validateType(type);
    this._name = name;
    this._type = type;
  }

  validateType(arg) {
    if (!['engineer', 'manager', 'salesperson'].includes(arg)) {
      throw new Error(`${arg}라는 직원 유형은 없습니다.`);
    }
  }

  get typeString() {
    return this._type.toString();
  }

  get type() {
    return this._type;
  }

  set type(arg) {
    this._type = new EmployeeType(arg);
  }

  static createEmployeeType(aString) {
    switch(aString) {
      case 'engineer': return new Engineer();
      case 'manager': return new Manager();
      case 'salesperson': return new Salesperson();
      default: throw new Error(`${aString}라는 직원 유형은 없습니다.`);
    }
  }

  toString() {
    return `${this._name} (${this.type.capitalizedType})`;
  }
}

서브클래스 제거하기

반대 리팩터링: 타입 코드를 서브클래스로 바꾸기

배경

서브클래스는 소프트웨어 시스템이 성장함에 따라 그 가치가 바래지기도 한다. 더 이상 쓰이지 않는 서브클래스와 마주하는 프로그래머는 가치 없는 것을 이해하느라 에너지를 낭비할 것이고, 이러한 경우 서브클래스를 슈퍼클래스의 필드로 대체해 제거하는게 최선이다.

절차

  1. 서브클래스의 생성자를 팩터리 함수로 바꾼다.
  2. 서브클래스의 타입을 검사하는 코드가 있다면 그 검사 코드에 함수 추출하기와 함수 옮기기를 차례로 적용하여 슈퍼클래스로 옮긴다. 하나 변경할 때마다 테스트한다.
  3. 서브클래스의 타입을 나타내는 필드를 슈퍼클래스에 만든다.
  4. 서브클래스를 참조하는 메서드가 방금 만든 타입 필드를 이용하도록 수정한다.
  5. 서브클래스를 지운다.
  6. 테스트한다.

예시

리팩터링 전

class Person {
  constructor(name) {
    this._name = name;
  }

  get name() {
    return this._name;
  }

  get genderCoder() {
    return 'X';
  }
  // 생략
}

class Male extends Person {
  get genderCode() {
    return 'M';
  }
}

class Female extends Person {
  get genderCode() {
    return 'F';
  }
}

// 클라이언트
const numberOfMales = people.filter(p => p instanceof Male).length;

다음과 같은 코드에서 서브 클래스가 하는 일이 이게 다라면 굳이 존재할 이유가 없다. 이를 리팩터링 해보자.

리팩터링 후

서브클래스 만들기를 캡슐화하는 방법은 생성자를 팩터리 함수로 바꾸기다. 먼저 이를 진행해보자.

function createPerson(aRecord) {
  let p;
  switch (aRecord.gender) {
    case 'M':
      p = new Male(aRecord.name);
      break;
    case 'F':
      p = new Female(aRecord.name);
      break;
    default:
      p = new Person(aRecord.name);
  }
  return p;
}

function loadFromInput(data) {
  const result = [];
  data.forEach((aRecord) => {
    result.push(createPerson(aRecord));
  });
  return result;
}

현재 이 코드를 깔끔히 청소해보자. 변수 p를 인라인 하고 loadFromInput()의 반복문을 파이프라인으로 바꾼다. 또한 클라이언트 코드에서 instanceOf를 사용하는 타입 검사 코드를 함수로 추출한다.

function createPerson(aRecord) {
  switch (aRecord.gender) {
    case 'M':
      return new Male(aRecord.name);

    case 'F':
      return new Female(aRecord.name);
    default:
      return new Person(aRecord.name);
  }
}

function loadFromInput(data) {
  data.map((aRecord) => createPerson(aRecord));
}

function isMale(aPerson) {return aPerson instanceof Male;}

class Person {
  get isMale() {return this instanceOf Male;}
}

// 클라이언트
const numberOfMales = people.filter(p => p.isMale).length;

마지막으로 매개변수를 이용하여 서브클래스들의 차이를 나타낼 필드를 추가하자.

class Person {
  constructor(name, genderCode) {
    this._name = name;
    this._genderCode = genderCode;
  }

  get name() {
    return this._name;
  }

  get genderCoder() {
    return 'X';
  }

  get isMale() {
    return this._genderCode === 'M';
  }
  // 생략
}

function createPerson(aRecord) {
  switch (aRecord.gender) {
    case 'M':
      return new Person(aRecord.name, 'M');
    case 'F':
      return new Person(aRecord.name, 'F');
    default:
      return new Person(aRecord.name, 'X');
  }
}

슈퍼클래스 추출하기

배경

비슷한 일을 수행하는 두 클래스가 보이면 상속 메커니즘을 이용해서 비슷한 부분을 공통의 슈퍼클래스로 옮겨 담을 수 있다.

절차

  1. 빈 슈퍼클래스를 만든다. 원래의 클래스들이 새 클래스를 상속하도록 한다.
  2. 테스트한다.
  3. 생성자 본문 올리기, 메서드 올리기, 필드 올리기를 차례로 적용하여 공통 원소를 슈퍼클래스로 옮긴다.
  4. 서브클래스에 남은 메서드들을 검토한다. 공통되는 부분이 있다면 함수로 추출한 다음 메서드 올리기를 적용한다.
  5. 원래 클래스들을 사용하는 코드를 검토하여 슈퍼클래스의 인터페이스를 사용하게 할지 고민해본다.

예시

리팩터링 전

class Employee {
  constructor(name, id, monthlyCost) {
    this.name = name;
    this.id = id;
    this.monthlyCost = monthlyCost;
  }

  get monthlyCost() {
    return this._monthlyCost;
  }

  get name() {
    return this._name;
  }

  get id() {
    return this._id;
  }

  get annualCost() {
    return this.monthlyCost * 12;
  }
}

class Department {
  constructor(name, staff) {
    this.name = name;
    this.staff = staff;
  }

  get staff() {
    return this._staff.slice();
  }

  get name() {
    return this._name;
  }

  get totalMonthlyCost() {
    return this.staff.map((e) => e.monthlyCost).reduce((sum, cost) => sum + cost);
  }

  get headCount() {
    return this.staff.length;
  }

  get totalAnnualCost() {
    return this.totalMonthlyCost * 12;
  }
}

연간 비용과 월간 비용부분과 관련한 부분에서 공통된 기능이 눈에 띔을 확인할 수 있다.

리팩터링 후

Party라는 빈 클래스를 만들고 두 클래스가 이를 확장하도록 만들자. 공통된 필드와 메서드를 슈퍼클래스로 옮긴다.

class Party {
  constructor(name) {
    this._name = name;
  }

  get name() {
    return this._name;
  }

  get annualCost() {
    return this.monthlyCost * 12;
  }
}

class Employee extends Party {
  constructor(name, id, monthlyCost) {
    super(name);
    this.name = name;
    this.id = id;
    this.monthlyCost = monthlyCost;
  }

  get monthlyCost() {
    return this._monthlyCost;
  }

  get id() {
    return this._id;
  }
}

class Department extends Party {
  constructor(name, staff) {
    super(name);
    this.name = name;
    this.staff = staff;
  }

  get staff() {
    return this._staff.slice();
  }

  get totalMonthlyCost() {
    return this.staff.map((e) => e.monthlyCost).reduce((sum, cost) => sum + cost);
  }

  get headCount() {
    return this.staff.length;
  }
}

계층 합치기

배경

계층구조도 진화하면서 어떤 클래스와 그 부모가 너무 비슷해져서 더는 독립적으로 존재해야 할 이유가 사라지는 경우가 생기기도 한다.
→ 둘을 하나로 합쳐야 할 시점이다.

절차

  1. 두 클래스 중 제거할 것을 고른다.
  2. 필드 올리기와 메서드 올리기 혹은 필드 내리기와 메서드 내리기를 적용하여 모든 요소를 하나의 클래스로 옮긴다.
  3. 제거할 클래스를 참조하던 모든 코드가 남겨질 클래스를 참조하도록 고친다.
  4. 빈 클래스를 제거한다.
  5. 테스트한다.

예시

리팩터링 전

class Employee {...}
class SalesPerson extends Employee {...}

리팩터링 후

class Employee {...}

서브클래스를 위임으로 바꾸기

배경

상속은 무언가가 달라져야 하는 이유가 여러 개여도 상속에서는 그중 단 하나의 이유만 선택해야 하며, 클래스들의 관계를 아주 긴밀하게 결합한다.

따라서 이 대신 위임을 사용하자. 위임은 객체 사이의 일반적인 관계이므로 상호작용에 필요한 인터페이스를 명확히 정의할 수 있다.

즉, 상속보다 결합도가 훨씬 약하다.

절차

  1. 생성자를 호출하는 곳이 많다면 생성자를 팩터리 함수로 바꾼다.
  2. 위임으로 활용할 빈 클래스를 만든다. 이 클래스의 생성자는 서브클래스에 특화된 데이터를 전부 받아야 하며, 보통은 슈퍼클래스를 가리키는 역참조도 필요하다.
  3. 위임을 저장할 필드를 슈퍼클래스에 추가한다.
  4. 서브클래스 생성 코드를 수정하여 위임 인스턴스를 생성하고 위임 필드에 대입해 초기화한다.
  5. 서브클래스의 메서드 중 위임 클래스로 이동할 것을 고른다.
  6. 함수 옮기기를 적용해 위임 클래스로 옮긴다. 원래 메서드에서 위임하는 코드는 지우지 않는다.
  7. 서브클래스 외부에도 원래 메서드를 호출하는 코드가 있다면 서브클래스에 위임 코드를 슈퍼클래스로 옮긴다. 이때 위임이 존재하는지를 검사하는 보호 코드로 감싸야 한다. 호출하는 외부 코드가 없다면 원래 메서드는 죽은 코드가 되므로 제거한다.
  8. 테스트한다.
  9. 서브클래스의 모든 메서드가 옮겨질 때까지 5~8 과정을 반복한다.
  10. 서브클래스들의 생성자를 호출하는 코드를 찾아서 슈퍼클래스의 생성자를 사용하도록 수정한다.
  11. 테스트한다.
  12. 서브클래스를 삭제한다.

예시

서브 클래스가 하나인 경우(공연 예약 클래스)

리팩터링 전

class Booking {
  constructor(show, date) {
    this._show = show;
    this._date = date;
  }

  get hasTalkback() {
    return this._show.hasOwnProperty('talkback') && !this.isPeakDay;
  }

  get basePrice() {
    let result = this._show.price;
    if (this.isPeakDay) result += Math.round(result * 0.15);
    return result;
  }
}

// 추가 비용을 다양하게 설정할 수 있느 프리미엄 예약용 서브 클래스
class PremiumBooking extends Booking {
  constructor(show, date, extras) {
    super(show, date);
    this._extras = extras;
  }

  get hasTalkback() {
    return this._show.hasOwnProperty('talkback');
  }

  get basePrice() {
    return Math.round(super.basePrice + this._extras.premiumFee);
  }

  // 슈퍼클래스에는 없는 기능을 프리미엄 예약에서 제공하는 예
  get hasDinner() {
    return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
  }
}

// 클라이언트 쪽 코드
// 일반예약
aBooking = new Booking(show, date);

// 프리미엄 예약
aBooking = new PremiumBooking(show, date, extras);

리팩터링 후

우선 생성자를 팩터리 함수로 바꿔서 생성자 호출 부분을 캡슐화한다.

function createBooking(show, date) {
  return new Booking(show, date);
}

function createPremiumBooking(show, date, extras) {
  return new PremiumBooking(show, date, extras);
}

// 클라이언트 쪽 코드
// 일반예약
aBooking = createBooking(show, date);

// 프리미엄 예약
aBooking = createPremiumBooking(show, date, extras);

이후 위임클래스를 만들고 위임을 예약 객체와 연결

// 역참조를 매개변수로 받는 위임 클래스
class PremiumBookingDelegate {
  constructor(hostBooking, extras) {
    this._host = hostBooking;
    this._extras = extras;
  }
}

function createPremiumBooking(show, date, extras) {
  const result = new PremiumBooking(show, date, extras);
  result._bePremium(extras);
  return result;
}

class Booking {
  ...

  _bePremium(extras) {
    this._premiumDelegate = new PremiumBookingDelegate(this, extras);
  }
}

이후 기능을 위임으로 옮겨준다.

class Booking {
  constructor(show, date) {
    this._show = show;
    this._date = date;
  }

  _bePremium(extras) {
    this._premiumDelegate = new PremiumBookingDelegate(this, extras);
  }

  get hasTalkback() {
    return this._premiumDelegate
      ? this._premiumDelegate.hasTalkback
      : this._show.hasOwnProperty('talkback') && !this.isPeakDay;
  }

  // 위임의 메서드를 기반 메서드의 확장 형태로 재호출하는 경우
  get basePrice() {
    let result = this._show.price;
    if (this.isPeakDay) result += Math.round(result * 0.15);

    return this._premiumDelegate ? this._premiumDelegate.extendBasePrice(result) : result;
  }

  get hasDinner() {
    return this._premiumDelegate ? this._premiumDelegate.hasDinner : undefined;
  }
}

// 역참조를 매개변수로 받는 위임 클래스
class PremiumBookingDelegate {
  constructor(hostBooking, extras) {
    this._host = hostBooking;
    this._extras = extras;
  }

  get hasTalkback() {
    return this._host._show.hasOwnProperty('talkback');
  }

  extendBasePrice(base) {
    return Math.round(base + this._extras.premiumFee);
  }

  get hasDinner() {
    return this._extras.hasOwnProperty('dinner') && !this._host.isPeakDay;
  }
}

// 클라이언트 쪽 코드
// 일반예약
aBooking = createBooking(show, date);

// 프리미엄 예약
aBooking = createPremiumBooking(show, date, extras);

function createBooking(show, date) {
  return new Booking(show, date);
}

function createPremiumBooking(show, date, extras) {
  const result = new Booking(show, date, extras);
  result._bePremium(extras);
  return result;
}

슈퍼클래스를 위임으로 바꾸기

배경

상속은 기존 기능을 재활용하는 강력하고 손쉬운 수단이지만, 혼란과 복잡도를 키우는 방식으로 이뤄지기도 한다. 따라서 이러한 상속보다 위임을 사용하여 객체를 분리하고 문제를 피해보자.

그렇다면 '상속은 절대 하지 말아야 할까?' 라는 질문에 저자는 상속을 먼저 적용하고 나중에 문제가 생기면 슈퍼클래스를 위임으로 바꾸는 방식을 조언한다.

절차

  1. 슈퍼클래스 객체를 참조하는 필드를 서브클래스에 만든다. 위임 참조를 새로운 슈퍼클래스 인스턴스로 초기화한다.
  2. 슈퍼클래스의 동작 각각에 대응하는 전달 함수를 서브클래스에 만든다. 서로 관련된 함수끼리 그룹으로 묶어 진행하며, 그룹을 하나씩 만들 때마다 테스트한다.
  3. 슈퍼클래스의 동작 모두가 전달 함수로 오버라이드되었다면 상속 관계를 끊는다.

예시

고대 스크롤 관리 코드 예제

리팩터링 전

class CatalogItem {
  constructor(id, title, tags) {
    this.id = id;
    this.title = title;
    this.tags = tags;
  }

  get id() {
    return this._id;
  }

  get title() {
    return this._title;
  }

  hasTag(arg) {
    return this._tags.includes(arg);
  }
}

// 정기 세척 이력이 필요하여 카탈로그 아이템을 확장하여
// 세척 관련 데이터를 추가
class Scroll extends CatalogItem {
  constructor(id, title, tags, dateLastCleaned) {
    super(id, title, tags);
    this._lastCleaned = dateLastCleaned;
  }

  needsCleaning(targetDate) {
    const threshold = this.hasTag('reversed') ? 700 : 1500;
    return this.daysSinceLastCleaning(targetDate) > threshold;
  }

  datsSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
  }
}

석화병 치료법을 적어 놓은 스크롤은 사본이 여러 개임에도 카탈로그 아이템은 하나뿐이라는 차이가 존재하여 슈퍼-서브 관계라는 모델링이 맞지 않음

리팩터링 후

Scroll에 카탈로그 아이템을 참조하는 속성을 만들고 슈퍼클래스의 인스턴스를 새로 하나 만들어 대입하고, 대응하는 메서드를 만든 후 상속관계를 끊는다.

class CatalogItem {
  constructor(id, title, tags) {
    this.id = id;
    this.title = title;
    this.tags = tags;
  }
}

class Scroll {
  constructor(id, title, tags, dateLastCleaned) {
    this._catalogItem = new CatalogItem(id, title, tags);
    this._lastCleaned = dateLastCleaned;
  }

  needsCleaning(targetDate) {
    const threshold = this.hasTag('reversed') ? 700 : 1500;
    return this.daysSinceLastCleaning(targetDate) > threshold;
  }

  datsSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
  }

  get id() {
    return this._catalogItem.id;
  }

  get title() {
    return this._catalogItem.title;
  }

  hasTag(aString) {
    return this._catalogItem.hasTag(aString);
  }
}
profile
It is possible for ordinary people to choose to be extraordinary.

0개의 댓글