상속 다루기

메서드 올리기

같은 일을 수행하는 서브클래스들이 있다면 수퍼클래스의 메서드로 올린다.

📜 절차

  1. 메서드 시그니처가 다르다면 함수 선언 바꾸기로 수퍼클래스에서 사용하고 싶은 형태로 통일한다.
  2. 수퍼클래스에 새로운 메서드를 생성하고, 대상 메서드의 코드를 복사해넣는다.
  3. 서브클래스의 메서드들을 제거한다.

필드 올리기

서브클래스 필드들이 비슷한 방식으로 쓰인다고 판단되면 수퍼클래스로 끌어올리도록 한다.

📜 절차

  1. 필드들의 이름이 각기 다르다면 똑같은 이름으로 바꾼다.
  2. 수퍼클래스에 새로운 필드를 생성한다.
  3. 서브클래스의 필드들을 제거한다.

생성자 본문 올리기

서브클래스에서 공통되는 생성자 코드가 있다면 수퍼클래스의 생성자로 올린다.

📜 절차

  1. 수퍼클래스에 생성자가 없다면 하나 정의한다.
  2. 공통 코드를 수퍼클래스에 추가하고 서브클래스들에서는 제거한다. 생성자 매개변수 중 공통 코드에서 참조하는 값들을 모두 super()로 건넨다.
  3. 생성자 시작 부분으로 옮길 수 없는 공통 코드에는 함수 추출하기메서드 올리기를 차례로 적용한다.

다음의 예제를 보자.

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

  get isPrivileged() { ... }

  assignCar() { ... }
}

class Manager extends Employee {
  constructor(name, grade) {
    super(name);
    this._grade = grade;
    // 공통 코드가 나중에 올 때
    if (this.isPrivileged) {
      this.assignCar();
    }
  }

  get isPrivileged() {
    return this._grade > 4;
  }
}

isPrivileged 함수는 grade 필드에 값이 대입된 후에야 호출될 수 있고, 서브클래스만이 이 필드에 값을 대입할 수 있다.

이런 경우라면, 먼저 공통 코드를 함수로 추출하고 추출한 메서드를 수퍼클래스로 옮긴다.

class Employee {
  // ...

  // 공통 코드를 함수로 추출 후 수퍼클래스로 이동
  finishConstruction() {
    if (this.isPrivileged) {
      this.assignCar();
    }
  }
}

class Manager extends Employee {
  constructor(name, grade) {
    super(name);
    this._grade = grade;
    this.finishConstruction(); // 추출한 함수를 호출
  }

  get isPrivileged() {
    return this._grade > 4;
  }
}

💻 생성자 본문 올리기 코드


메서드 내리기

* 반대 리펙터링: 메서드 올리기

특정 서브클래스 하나(혹은 소수)와만 관련된 메서드는 수퍼클래스에서 제거하고 해당 서브클래스(들)에 추가하도록 한다.

📜 절차

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

필드 내리기

* 반대 리펙터링: 필드 올리기

서브클래스 하나(혹은 소수)에서만 사용하는 필드라면, 해당 서브클래스(들)로 필드를 옮긴다.

📜 절차

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

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

필드 값에 따라 동작이 달라지는 코드가 있다면, 이런 필드를 서브 클래스로 대체하도록 한다.

📜 절차

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

📚 직접 상속

다음은 직원 코드 예제다.

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}라는 직원 유형은 없습니다.`);
  }
}

위 예제를 이 기법을 사용해서 리팩터링을 해보자. 먼저 타입 코드 변수를 캡슐화한다.

class Employee {
  
  // ...
  
  get type() {
    return 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"; }
}

생성자를 팩터리 함수로 바꾸기 기법을 사용해서 선택 로직을 담을 별도 장소를 마련한다.

// ...

const createEmployee = (name, type) => {
  switch (type) {
    case 'engineer':
      return new Engineer(name, type);
    case 'salesperson':
      return new Salesperson(name, type);
    case 'manager':
      return new Manager(name, type);
  }
  return new Manager(name, type);

이제 타입 코드 필드와 수퍼클래스의 게터를 제거한다. 또한 validateType 함수에서 타입 검증하는 역할을 createEmployee 에서 해주도록 한다.

class Employee {
  // type 제거
  constructor(name) {
    this._name = name;
  }
  // validateType 함수 제거
}

// ...

const 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(`${type}라는 직원 유형은 없습니다.`);
  }
};

📚 간접 상속

이제는 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.slice(1).toLowerCase());
  }
}

타입에 대한 클래스를 생성하고 각 타입별 서브클래스를 생성하여 관련된 팩터리 함수를 생성한다. 이후에 타입 코드 필드를 제거한다.

class EmployeeType {
  get capitalizedName() { // 함수 이동 및 함수 이름 바꾸기
    return this.toString().charAt(0).toUpperCase() + this.toString().substr(1).toLowerCase();
  }
}

class Engineer extends EmployeeType {
  toString() { return "engineer"; }
}

class Salesperson extends EmployeeType {
  toString() { return "salesperson"; }
}

class Manager extends EmployeeType {
  toString() { return "manager";  }
}

class Employee {
  constructor(name) { // type 필드 제거
    this._name = name;
  }
  
  get type() { return this._type; }

  set type(arg) { // 팩터리 함수 호출
    this._type = Employee.createEmployee(arg); 
  }
 
  static createEmployee(aString) { // 팩터리 함수 생성
    switch (aString) {
      case "engineer":
        return new Engineer();
      case "salesperson":
        return new Salesperson();
      case "manager":
        return new Manager();
      default:
        throw new Error(`${aString}라는 직원 유형은 없습니다.`);
    }
  }
}

EmployeeType 클래스에 capitalizedType 메서드를 이동해 리팩터링을 마무리한다.


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


서브클래스 제거하기

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

더이상 쓰이지 않는 서브클래스를 발견한다면 제거하도록 한다.

📜 절차

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

다음의 예제를 보자.

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

  get name() { return this._name; }

  get genderCode() { return "X"; }
}

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

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

// gender 타입 사용 함수 1
const loadFromInput = (data) => {
  const result = [];
  data.forEach((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);
        break;
    }
    result.push(p);
  });
  return result;
};

// gender 타입 사용 함수 2
const numberOfMales = (people) => {
  return people.filter((p) => p instanceof Male).length;
};

서브클래스가 하는 일이 이렇게 단순하다면 굳이 존재할 이유가 없기 때문에 제거해주도록 한다.

먼저 팩터리 함수를 추가한다.

const 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);
  }
};

수퍼클래스에 성별에 대한 필드를 추가하고, 서브클래스를 참조하는 메서드가 방금 만든 타입 필드를 이용하도록 수정한다.

class Person {
  constructor(name, genderCode) {
    this._name = name;
    this._genderCode = genderCode; // 필드 추가
  }

  get name() {
    return this._name;
  }

  get genderCode() { // 게터 추가
    return this._genderCode;
  }
}

const createPerson = (aRecord) => {
  switch (aRecord.gender) {
    case "M":
      return new Male(aRecord.name, "M"); // genderCode 추가
    case "F":
      return new Female(aRecord.name, "F"); // genderCode 추가
    default:
      return new Person(aRecord.name, "X"); // genderCode 추가
  }
};

// ...

이제 각 서브클래스에서 gender 타입 관련 코드를 수퍼클래스로 옮기고, 성별 코드를 사용하고 있는 loadFromInputnumberOfMales 함수 또한 이에 맞춰 수정한다.

모든 작업이 끝났다면, 서브클래스들을 제거한다.

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

  get name() { return this._name; }

  get genderCode() { return this._genderCode; }

  get isMale() { return this._genderCode === "M"; } // 성별 검사 코드 추가
}

// 서브클래스 제거

const 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");
  }
};

const loadFromInput = (data) => {
  // createPerson 함수를 호출하도록 변경 및 파이프라인으로 바꾸기
  return data.map((aRecord) => createPerson(aRecord));
};

const numberOfMales = (people) => {
  // instanceof -> isMale
  return people.filter((p) => p.isMale).length;
};

💻 서브클래스 제거하기 코드


수퍼클래스 추출하기

비슷한 일을 수행하는 두 클래스가 보이면 비슷한 부분을 공통의 수퍼클래스로 옮기도록 한다.

📜 절차

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

다음의 예제를 보자.

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

  get name() { return this._name; }
  
  get monthlyCost() { return this._monthlyCost; }

  get id() { return this._id; }

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

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

  get name() { return this._name; }

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

  get headCount() { return this.staff.length; }
  
  get totalMonthlyCost() {
    return this._staff.map((e) => e.monthlyCost).reduce((sum, cost) => sum + cost);
  }
  
  get totalAnnualCost() { return this.totalMonthlyCost * 12; }
}

각 클래스에서 연간 비용과 월간 비용, 이름이 공통되고 있다. 두 클래스로부터 수퍼클래스를 추출해보자.
먼저 빈 수퍼클래스를 생성하고 두 클래스가 이를 확장하도록 한다.

class Party {} // 빈 수퍼클래스 생성

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

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

이제 공통되는 속성인 name, monthlyCost, annualCost 를 수퍼클래스로 올린다. 이때 monthlyCostannualCost는 각 클래스에서 이름도 다르고 코드도 다르지만 의도는 같기 때문에, 이름을 하나로 통일해서 수퍼클래스로 올린다.

class Party {
  constructor(name, monthlyCost) {
    this._name = name;
    this._monthlyCost = monthlyCost; // 필드 추가
  }

  get name() { // 메서드 올리기
    return this._name;
  }

  get monthlyCost() { // 메서드 올리기
    return this._monthlyCost;
  }

  get annualCost() { // 메서드 올리기
    return this.monthlyCost * 12;
  }
}

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

  get id() {
    return this._id;
  }
  
  // name, monthlyCost, annualCost 메서드 제거
}

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

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

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

  get totalMonthlyCost() {
    return this.staff
      .map((e) => e.monthlyCost)
      .reduce((sum, cost) => sum + cost);
  }
  // name, totalAnnualCost 메서드 제거
}

💻 수퍼클래스 추출하기 코드


계층 합치기

어떤 클래스와 그 부모가 너무 비슷해져서 더는 독립적으로 존재해야할 이유가 사라진다면 그 둘을 하나로 합치도록 한다.

📜 절차

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

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

상속이 잘못된 곳에서 사용되거나 환경이 변해 문제가 생긴다면 서브클래스를 위임으로 바꾸도록 한다.

📜 절차

  1. 생성자를 호출하는 곳이 많다면 생성자를 팩터리 함수로 바꾼다.
  2. 위임으로 활용할 빈 클래스를 만든다.
  3. 위임을 저장할 필드를 수퍼클래스에 추가한다.
  4. 서브클래스 생성 코드를 수정하여 위임 필드를 생성하고 위임 필드에 대입해 초기화한다.
  5. 함수 옮기기를 적용해 서브클래스의 메서드를 위임 클래스로 옮긴다.
  6. 서브 클래스 외부에도 원래 메서드를 호출하는 코드가 있다면 서브클래스의 위임 코드를 수퍼클래스로 옮긴다.
  7. 모두 옮겼다면 수퍼클래스의 생성자를 사용하도록 수정하고, 서브클래스를 삭제한다.

📚 서브클래스가 하나일 때

다음은 공연 예약에 대한 예제다. 서브클래스로 추가 비용을 다양하게 설정할 수 있는 프리미엄 예약용 클래스가 있다.

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

  get date() { return this._date; }

  get show() { return this._show; }

  get isPeakDay() { ... }

  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;
  }
}

// 클라이언트
const normalClient = (show, date) => {
  return new Booking(show, date);
};

const premiumClient = (show, date, extras) => {
  return new PremiumBooking(show, date, extras);
};

이 클래스를 위임으로 바꿔보자. 먼저 생성자를 팩터리함수로 바꿔 생성자 호출 부분을 캡슐화한다.

// ...

const createBooking = (show, date) => { // 팩터리 함수 생성
  return new Booking(show, date);
};

const createPremiumBooking = (show, date, extras) => { // 팩터리 함수 생성
  return new PremiumBooking(show, date, extras);
};

const normalClient = (show, date) => {
  return new createBooking(show, date); // 팩터리 함수 호출
};

const premiumClient = (show, date, extras) => {
  return new createPremiumBooking(show, date, extras); // 팩터리 함수 호출
};

위임 클래스를 생성한다. 위임 클래스의 생성자는 서브클래스가 사용하던 매개변수와 Booking 객체를 매개변수로 받는다.

class Booking {
  // ...
  
  _bePremium(extras) { // 위임을 Booking과 연결
    this._premiumDelegate = new PremiumBookingDelegate(this, extras);
  }
}

// ...

class PremiumBookingDelegate { // 위임 클래스 생성
  constructor(hostBooking, extras) {
    this._host = hostBooking;
    this._extras = extras;
  }
}

// ...

const createPremiumBooking = (show, date, extras) => { // 팩터리 함수 수정
  const result = new PremiumBooking(show, date, extras);
  result._bePremium(extras);
  return result;
};

// ...

이제 서브클래스의 메서드들을 위임으로 옮긴다. 수퍼클래스의 데이터를 사용하는 부분은 _host를 통하도록 고친다. 또한 위임을 사용하는 분배로직을 수퍼클래스 메서드에 추가한다.

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

  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;
  }

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

  get hasDinner() { // 위임을 사용하는 분배 로직 추가
    return this._premiumDelegate ? this._premiumDelegate.hasDinner : undefined;
  }
}

// PremiumBooking 클래스 제거

class PremiumBookingDelegate {
  constructor(hostBooking, extras) {
    this._host = hostBooking;
    this._extras = extras;
  }

  get hasTalkback() { // host로 연결
    return this._host.show.hasOwnProperty("talkback");
  }

  extendBasePrice(base) { // basePrice 메서드의 확장 형태로 추가
    return Math.round(base + this._extras.premiumFee);
  }

  get hasDinner() { // host로 연결
    return this._extras.hasOwnProperty("dinner") && !this._host.isPeakDay;
  }
}

// ...

const createPremiumBooking = (show, date, extras) => {
  const result = new Booking(show, date, extras); // 수퍼클래스를 반환하도록 변경
  result._bePremium(extras);
  return result;
};

// ...

이렇게 위임으로 바꿔 동적으로 프리미엄 예약으로 바꿀 수 있고, 상속은 다른 목적으로 사용할 수 있다.


📚 서브클래스가 여러개일 때

다음으로는 서브클래스가 여러 개일 때의 경우다.

class Bird {
  constructor(data) {
    this._name = data.name;
    this._plumage = data.plumage;
  }

  get name() { return this._name; }

  get plumage() { return this._plumage || "보통이다"; }

  get airSpeedVelocity() { return null; }
}

class EuropeanSwallow extends Bird {
  get airSpeedVelocity() { return 35; }
}

class AfricanSwallow extends Bird {
  constructor(data) {
    super(data);
    this._numberOfCoconuts = data.numberOfCoconuts;
  }

  get airSpeedVelocity() { return 40 - 2 * this._numberOfCoconuts; }
}

class NorwegianBlueParrot extends Bird {
  constructor(data) {
    super(data);
    this._voltage = data.voltage;
    this._isNailed = data.isNailed;
  }

  get plumage() {
    if (this._voltage > 100) {
      return "그을렸다";
    } else {
      return this._plumage || "예쁘다";
    }
  }

  get airSpeedVelocity() {
    return this._isNailed ? 0 : 10 + this._voltage / 10;
  }
}

const createBird = (data) => {
  switch (data.type) {
    case "유럽 제비":
      return new EuropeanSwallow(data);
    case "아프리카 제비":
      return new AfricanSwallow(data);
    case "노르웨이 파랑 앵무":
      return new NorwegianBlueParrot(data);
    default:
      return new Bird(data);
  }
};

서브클래스드들을 하나씩 위임으로 바꿔보자. 먼저 빈 위임 클래스를 생성한다.

class EuropeanSwallowDelegate { }

Bird 클래스의 생성자에서 위임 필드를 생성하고 초기화한다. 또한 타입 코드를 기준으로 위임을 선택하는 메서드를 생성한다.

class Bird {
  constructor(data) {
    this._name = data.name;
    this._plumage = data.plumage;
    this._specialDelegate = this.selectSpecialDelegate(data); // 위임 필드 생성 및 초기화
  }

  selectSpecialDelegate(data) { // 위임 선택 메서드 추가
    switch (data.type) {
      case "유럽 제비": 
        return new EuropeanSwallowDelegate();
      default: 
        return null;
    }
  }
}

이제 유럽 제비 클래스의 메서드를 위임 클래스로 옮긴다.

class EuropeanSwallow extends Bird {
  get airSpeedVelocity() {
    return this._specialDelegate.airSpeedVelocity; // 위임 클래스의 메서드를 사용하도록 수정
  }
}

class EuropeanSwallowDelegate {
  get airSpeedVelocity() { // 메서드 이동
    return 35;
  }
}

수퍼클래스의 airSpeedVelocity 함수를 수정해 위임이 존재하면 위임의 메서드를 호출하도록 하고 EuropeanSwallow 클래스를 제거한다.

class Bird {
  // ...
  get airSpeedVelocity() { 
    return this._specialDelegate ? this._specialDelegate.airSpeedVelocity : null;
  }
}

// EuropeanSwallow 클래스 제거

// ...

const createBird = (data) => {
  switch (data.type) { // 유럽 제비인 경우 제거
    case "아프리카 제비":
      return new AfricanSwallow(data);
    case "노르웨이 파랑 앵무":
      return new NorwegianBlueParrot(data);
    default:
      return new Bird(data);
  }
};

이제 다른 서브클래스들을 옮기면 다음과 같이 수정된다.

class Bird {
  // ...
  
  get airSpeedVelocity() {
    return this._specialDelegate ? this._specialDelegate.airSpeedVelocity : null;
  }
  
  get plumage() { // 위임 여부 선택 로직 추가
    if(this._specialDelegate instanceof NorwegianBlueParrot) {
      return this._specialDelegate.plumage;
    }
    return this._plumage || "보통이다";
  }
  
  selectSpecialDelegate(data) { // 새로 생성한 클래스로 호출 변경
    switch (data.type) {
      case "유럽 제비":
        return new EuropeanSwallowDelegate();
      case "아프리카 제비":
        return new AfricanSwallowDelegate(data);
      case "노르웨이 파랑 앵무":
        return new NorwegianBlueParrotDelegate(data);
      default:
        return null;
    }
  }
}

class EuropeanSwallowDelegate { 
  get airSpeedVelocity() {
    return 35;
  }
}

class AfricanSwallowDelegate { // 클래스 생성
  constructor(data, bird) {
    super(data, bird);
    this._numberOfCoconuts = data.numberOfCoconuts;
  }

  get airSpeedVelocity() {
    return 40 - 2 * this._numberOfCoconuts;
  }
}

class NorwegianBlueParrotDelegate { // 클래스 생성
  constructor(data, bird) {
    super(data, bird);
    this._voltage = data.voltage;
    this._isNailed = data.isNailed;
  }

  get airSpeedVelocity() {
    return this._isNailed ? 0 : 10 + this._voltage / 10;
  }

  get plumage() {
    if (this._voltage > 100) {
      return "그을렸다";
    } else {
      return this._bird_plumage || "예쁘다"; // bird 참조
    }
  }
}

const createBird = (data) => {
  return new Bird(data); // switch문 제거
};

Bird의 기본 메서드들에서 각 위임 클래스일 때와 아닐 때를 선택하는 로직을 리팩터링해보자.

위임 클래스들에서 수퍼클래스를 추출하고, 각 위임클래스들은 해당 수퍼클래스를 상속받는다.

class SpeciesDelegate {
  constructor(data, bird) {
    this._bird = bird;
  }

  get plumage() {
    return this._bird._plumage || "보통이다";
  }

  get airSpeedVelocity() {
    return null;
  }
}

class EuropeanSwallowDelegate extends SpeciesDelegate { ... }
class AfricanSwallowDelegate extends SpeciesDelegate { ... }
class NorwegianBlueParrotDelegate extends SpeciesDelegate { ... }

Bird의 기본 동작들을 SpeciesDelegate 클래스로 옮기는 것으로 리팩터링을 마무리한다.

class Bird {
  // ...
  get plumage() { return this._specialDelegate.plumage; }

  get airSpeedVelocity() { return this._speciesDelegate.airSpeedVelocity; }

  selectSpecialDelegate(data) {
    switch (data.type) {
      case "유럽 제비":
        return new EuropeanSwallowDelegate(data, this);
      case "아프리카 제비":
        return new AfricanSwallowDelegate(data, this);
      case "노르웨이 파랑 앵무":
        return new NorwegianBlueParrotDelegate(data, this);
      default:
        return new SpeciesDelegate(data, this); // 수퍼클래스로 변경
    }
  }
}

이렇게 위임 클래스들은 종에 따라 달라지는 데이터와 메서드만을 담게 되고 종과 상관없는 공통 코드는 Bird 자체와 미래의 서브클래스들에 남게 되었다.


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


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

수퍼클래스의 기능들이 서브클래스에는 어울리지 않는다면 그 기능들을 상속을 통해 이용하지 말고 필요한 기능만 위임하도록 한다.

📜 절차

  1. 수퍼클래스 객체를 참조하는 필드를 서브클래스에 만든다. 위임할 객체를 새로운 수퍼클래스 인스턴스로 초기화한다.
  2. 수퍼클래스의 동작 각각에 대응하는 전달 함수를 서브클래스에 만든다.
  3. 수퍼클래스의 동작 모두가 전달 함수로 오버라이드되었다면 상속 관계를 끊는다.

다음의 예시를 보자. Scroll 클래스는 CatalogItem을 확장하여 세척 관련 데이터를 추가해 사용했다.

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, dataLastCleaned) {
    super(id, title, tags);
    this._lastCleaned = dataLastCleaned;
  }

  daysSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, "d");
  }

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

수퍼클래스를 위임으로 바꿔보자. 먼저 ScrollCatalogItem을 참조하는 속성을 만들고 수퍼클래스의 인스턴스를 하나 만들어 대입한다.

// ...

class Scroll extends CatalogItem {
  constructor(id, title, tags, dataLastCleaned) {
    super(id, title, tags);
    this._catalogItem = new CatalogItem(id, title, tags); // 필드 생성
    this._lastCleaned = dataLastCleaned;
  }
  // ...
}

서브클래스에서 사용하는 수퍼클래스의 동작 각각에 대응하는 전달 메서드를 만든다. 이후에 CatalogItem과의 상속 관계를 끊는다.

// ...

class Scroll { // 상속 관계 끊기
  constructor(id, title, tags, dataLastCleaned) {
    // super 제거
    this._catalogItem = new CatalogItem(id, title, tags);
    this._lastCleaned = dataLastCleaned;
  }

  get id() { // 메서드 생성
    return this._catalogItem.id;
  }

  get title() { // 메서드 생성
    return this._catalogItem.title;
  }

  hasTag(aString) { // 메서드 생성
    return this._catalogItem.hasTag(aString);
  }
  
  daysSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, "d");
  }

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

💻 수퍼클래스를 위임으로 바꾸기 코드


상속이나 위임 어느 하나만 고집하지 말고, 적절히 혼용하도록 한다.

좋은 웹페이지 즐겨찾기