[리팩터링] 8. 상속 다루기

안광의·2022년 10월 24일
0
post-thumbnail

상속 다루기

메서드 올리기

//리팩토링 전
class Employee {}

class Salesperson extends Employee {
  get name() {}
}

class Engineer extends Employee {
  get name() {}
}
//리팩토링 후
class Employee {
  get name() {}
}

class Salesperson extends Employee {}

class Engineer extends Employee {}

무언가 중복되었다는 것은 한쪽의 변경이 다른 쪽에는 반영되지 않을 수 있다는 위험을 항상 수반하기 때문에 메서드들의 본문 코드가 똑같을 때는 메서드 올리기를 적용할 수 있다.



필드 올리기

//리팩토링 전
class Employee {}

class Salesperson extends Employee {
  #name;
}

class Engineer extends Employee {
  #name;
}
//리팩토링 후
class Employee {
  #name;
}

class Salesperson extends Employee {}

class Engineer extends Employee {}

서브클래스들이 독립적으로 개발되었거나 뒤늦게 하나의 계층 구조로 리팩터링된 경우라면 필드가 중복되기 쉬운데 필드들이 어떻게 이용되는지 분석해보고 비슷한 방식으로 쓰인다면 슈퍼클래스로 끌어올리자.



생성자 본문 올리기

//리팩토링 전
class Party {}

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

class Department extends Party {
  #name;
  #staff;
  constructor(name, staff) {
    super();
    this.#name = name;
    this.#staff = staff;
  }
}
//리팩토링 후
class Party {
  #name;
  constructor(name) {
    this.#name = name;
  }
}

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

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

서브클래스들에서 기능이 같은 메서드들을 발견하면 함수 추출하기와 메서드 올리기를 차례로 적용하여 말끔히 슈퍼클래스로 옮기는 것이 좋다.



메서드 내리기

//리팩토링 전
class Employee {
  get quota() {}
}

class Engineer extends Employee {}
class Salesperson extends Employee {}
//리팩토링 후
class Employee {}

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

특정 서브클래스 하나와만 관련된 메서드는 슈퍼클래스에서 제거하고 해당 서브클래스에 추가하는 편이 깔끔하다. 이 리팩터링은 해당 기능을 제공하는 서브클래스가 정확히 무엇인지 호출자가 알고 있을 때만 적용할 수 있다.



필드 내리기

//리팩토링 전
class Employee {
  #quota;
}

class Engineer extends Employee {}
class Salesperson extends Employee {}
//리팩토링 후
class Employee {}

class Engineer extends Employee {}
class Salesperson extends Employee {
  #quota;
}

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



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

//리팩토링 전
class Employee {
  #name;
  #type;
  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;
  }

  toString() {
    return `${this.#name} (${this.type})`;
  }
}
//리팩토링 후
class Employee {
  #name;
  constructor(name) {
    this.#name = name;
  }

  get type() {
    return 'employee';
  }

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

  static createEmployee(name, type) {
    switch (type) {
      case 'engineer':
        return new Engineer(name);
      case 'manager':
        return new Manager(name);
      case 'salesperson':
        return new Salesperson(name);
      default:
        throw new Error(`There is no ${type}`);
    }
  }
}

class Engineer extends Employee {
  get type() {
    return 'engineer';
  }
}

class Manager extends Employee {
  get type() {
    return 'manager';
  }
}

class Salesperson extends Employee {
  get type() {
    return 'salesperson';
  }
}

소프트웨어 시스템에서는 비슷한 대상들을 특정 특성에 따라 구분하는 타입 코드 필드가 있을 때가 있다. 타입 코드 필드 보다는 조건에 따라 다르게 동작하도록 해주는 다형성을 제공하고 특정 타입에서만 의미가 있는 값을 사용하는 필드나 메서드를 정의할 수 있는 서브클래스를 사용하는 것이 바람직하다.



서브클래스 제거하기

//리팩토링 전
class Person {
  #name;
  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';
  }
}

function loadFromInput(data) {
  const result = [];
  data.forEach((record) => {
    let person;
    switch (record.gender) {
      case 'M':
        person = new Male(record.name);
        break;
      case 'F':
        person = new Female(record.name);
        break;
      default:
        person = new Person(record.name);
    }
    result.push(person);
  });
  return result;
}
//리팩토링 후
class Person {
  #name;
  #genderCode
  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';
  }

  static create(record) {
    switch (record.gender) {
      case 'M':
        return new Person(record.name, 'M');
      case 'F':
        return new Person(record.name, 'F');
      default:
        return new Person(record.name, 'X');
    }
  }
}


function loadFromInput(data) {
  return data.map((record) => Person.create(record));
}

서브클래싱은 데이터 구조와는 다른 변종을 만들거나 종류에 따라 동작이 달라지게 할 수 있는 유용한 메커니즘이지만, 한 번도 활용되지 않거나 서브클래스를 필요로 하지 않는 방식으로 만들어진 기능에서만 쓰일 때는 제거해야 한다.



슈퍼클래스 추출하기

//리팩토링 전
class Department {
  get totalAnnualCost() {}
  get name() {}
  get headCount() {}
}

class Employee {
  get annualCost() {}
  get name() {}
  get id() {}
}
//리팩토링 후
class Party {
  get name() {}
  get annualCost() {}
}

class Department extends Party {
  get headCount() {}
}

class Employee extends Party {
  get id() {}
}

비슷한 일을 수행하는 두 클래스가 보이면 상속 메커니즘을 이용해서 비슷한 부분을 공통의 슈퍼클래스로 옮겨 담을 수 있다. 공통된 부분이 데이터라면 필드 올리기를 활용하고, 동작이라면 메서드 올리기를 활용하면 된다.



계층 합치기

//리팩토링 전
class Employee {
  ...
}

class Salesperson extends Employee {
  ...
}
//리팩토링 후
class Employee {
  ...
}

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



//리팩토링 전
//리팩토링 후


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

//리팩토링 전
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;
  }
}
//리팩토링 후
class Booking {
  #show;
  #date;
  #premiumDelegate;
  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() {
    return this.#premiumDelegate
    ? this.#premiumDelegate.basePrice
    : this.#privateBasePrice;
  }
  
  get #privateBasePrice() {
    let result = this.#show.price;
    
    if (this.isPeakDay) {
      result += Math.round(result * 0.15);
    }
    
    return result;
  }
  
  get hasDinner() {
    return this.#premiumDelegate
    ? this.#premiumDelegate.hasDinner
    : undefined;
  }
  
  #bePrimium(extras) {
    this.#premiumDelegate = new PremiumBookingDelegate(this, extras);
  }
  
  static createBooking(show, date) {
    return new Booking(show, date);
  }
  
  static createPremiumBooking (show, date, extras) {
    const result = new Booking(show, date, extras);
    result.#bePrimium(extras);
    return result;
  }
}
class PremiumBookingDelegate {
  #host
  #extras
  #show
  #privateBasePrice
  constructor(hostBooking, extras) {
    this.#host = hostBooking;
    this.#extras = extras;
  }

  get hasTalkback() {
    return this.#host.#show.hasOwnProperty('talkback');
  }

  get basePrice() {
    return Math.round(this.#host.#privateBasePrice + this.#extras.PremiumFee);
  }

  get hasDinner() {
    return this.#extras.hasOwnProperty('dinner') && !this.#host.isPeakDay;
  }
}

속한 갈래에 따라 종작이 달라지는 객체들은 상속을 표현하는 게 자연스럽다. 공통 데이터와 동작은 모두 슈퍼클래스에 두고 서브클래스는 자신에 맞게 기능을 추가하고나 오버라이드하면 된다. 상속은 한 번만 할 수 있고 부모를 수정하면 이미 존재하는 자식들의 기능을 해치기 쉽다는 단점이 있지만 위임은 이 두 문제를 해결해준다.



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

//리팩토링 전
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;
  }

  needsCleaning(targetDate) {
    const threshold = this.hasTag('revered') ? 700 : 1500;

    return this.daysSinceLastCleaning(targetDate) > threshold;
  }

  daysSinceLastCleaning(targetDate) {
    return this._lastCleaned.until(targetDate, ChronoUnit.DAYS);
  }
}
//리팩토링 후
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 {
  constructor(id, dataLastCleaned, catalogId, catalog) {
    this._id = id;
    this._catalogItem = catalog.get(catalogId);
    this._lastCleaned = dataLastCleaned;
  }

  get id() {
    return this._id;
  }

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

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

  needsCleaning(targetDate) {
    const threshold = this.hasTag('revered') ? 700 : 1500;

    return this.daysSinceLastCleaning(targetDate) > threshold;
  }

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

슈퍼클래스의 기능들이 서브클래스에는 어울리지 않는다면 그 기능들을 상속을 통해 이요하면 안된다는 신호이다. 제대로된 상속이라면 서브클래스가 슈퍼클래스의 모든 기능을 사용함은 물론, 서브클래스의 인스턴스를 슈퍼클래스의 인스턴스로도 취급할 수 있어야 한다. 이 경우가 아니라면 위임을 통해 일부 기능만 빌려올 수 있다.

profile
개발자로 성장하기

0개의 댓글