12장, 상속 다루기

2021.11.15

1. Pull Up Method, 메서드 올리기

  • 메서드 올리기를 적용하기 가장 쉬운 상황은 메서드들의 본문 코드가 똑같을 때다.
  • 메서드 올리기를 적용하기에 가장 이상하고 복잡한 상황은 메서드의 본문에서 참조하는 필드들이 서브클래스에만 있는 경우다.

절차

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

예시

before

class Employee {
  /*...*/
}
class Salesperson extends Employee {
  get name() {
    /*...*/
  }
}
class Engineer extends Employee {
  get name() {
    /*...*/
  }
}

after

class Employee {
  get name() {
    /*...*/
  }
}
class Salesperson extends Employee {
  /*...*/
}
class Engineer extends Employee {
  /*...*/
}

2. Pull up Field, 필드 올리기

  • 서브클래스들이 독립적으로 개발되었거나 뒤늦게 하나의 계층구조로 리팩터링된 경우라면 일부 기능이 중복되어 있을 수 있다.
  • 중복된 필드를 슈퍼클래스로 올린다면, 두가지 중복을 줄일 수 있다.
    • 데이터 중복 선언
    • 해당 필드를 사용하는 동작을 슈퍼클래스로 옮길 수 있다.

절차

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

예시

before

class Employee {/*...*/}
class Salesperson extends Employee {
  private String name;
}
class Engineer extends Employee {
  private String name;
}

after

// after
class Employee {
  private String name;
}
class Salesperson extends Employee {/*...*/}
class Engineer extends Employee {/*...*/}

3. Pull Up Constuctor Body, 생성자 본문 올리기

  • 생성자는 할 수 있는 일과 호출 순서에 제약이 있기 때문에 다루기 까다롭다.

절차

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

예시

before

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

after

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

4. Push Down Method, 메서드 내리기

  • 특정 서브클래스 하나(혹은 소수)와만 관련된 메서드는 해당 서브클래스(들)에 추가하는 편이 깔끔하다.

절차

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

예시

before

class Employee {
  get quota {/*...*/}
}
class Engineer extends Employee {/*...*/}
class Salesperson extends Employee {/*...*/}

after

class Employee {/*...*/}
class Engineer extends Employee {/*...*/}
class Salesperson extends Employee {
  get quota {/*...*/}
}

5. Push Down Field, 필드 내리기

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

절차

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

예시

before

class Employee {
  private String quota;
}
class Engineer extends Employee {/*..*/}
class Salesperson extends Employee {/*..*/}

after

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

6. Replace Type Code with Subclasses, 타입 코드를 서브클래스로 바꾸기

  • 타입코드는 프로그래밍 언어에 따라 열거형, 심볼, 문자열, 숫자등으로 표현하며 외부 서비스가 제공하는 데이터를 다루려 할 때 딸려오는 일이 흔하다.
  • 다형성을 사용하면 타입코드에 따라 동작이 달라져야하는 함수가 여러 개일 때 특히 유용하다.
  • 특정 타입에서만 필요한 필드나 메서드가 있다면, 필요한 서브클래스만 필드를 갖도록 정리하자.
  • 빈 클래스를 제거할 수도 있지만, 다양한 서브클래스 사이의 관계를 명확히 알려주고 있다면 그냥 두는 것도 괜찮다.

절차

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

예시

before

function createEmployee(name, type) {
  return new Employee(name, type);
}

after

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

7. Remove Subclass, 서브클래스 제거하기

  • 더 이상 쓰지 않는 서브클래스가 있다면 슈퍼클래스의 필드로 대체하고, 삭제하자.

절차

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

예시

before

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

after

class Person {
  get genderCode() {
    return this._genderCode;
  }
}

8. Extract Superclass, 슈퍼클래스 추출하기

절차

예시

before

class Department {
  get totalAnnualCost() {
    /*..*/
  }
  get name() {
    /*..*/
  }
  get headCount() {
    /*..*/
  }
}
class Employee {
  get annualCost() {
    /*..*/
  }
  get name() {
    /*..*/
  }
  get id() {
    /*..*/
  }
}

after

class Party {
  get name() {
    /*..*/
  }
  get annualCost() {
    /*..*/
  }
}
class Department extends Party {
  get annualCost() {
    /*..*/
  }
  get headCount() {
    /*..*/
  }
}
class Employee extends Party {
  get annualCost() {
    /*..*/
  }
  get id() {
    /*..*/
  }
}

9. Collapse Hierarchy, 계층 합치기

  • 클래스 계층구조를 리팩터링하다 보면 기능들을 위로 올리거나 아래로 내리는 일은 다반사로 벌어진다.
  • 어떤 클래스가 부모와 너무 비슷해져 독립할 이유가 없다면, 둘을 하나로 합쳐야 할 시점이다.

절차

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

예시

before

class Employee {
  /*..*/
}
class Salesperson extends Employee {
  /*..*/
}

after

class Employee {
  /*..*/
}

10. Replace Subclass with Delegate, 서브클래스를 위임으로 바꾸기

  • 상속에는 단점이 있다.
    • 한 번만 사용이 가능하다.
    • 부모가 수정되면 자식들이 영향을 받기 쉽다.
  • 위임은 이상의 두 문제를 해결해 준다. ( 상속보다 결합도가 약하다 )
  • 상속과 컴포지션을 적절히 혼용하라.

절차

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

예시

before

class Order {
  get daysToShip() {
    return this._warehouse.daysToShip;
  }
}
class PriorityOrder extends Order {
  get daysToShip() {
    return this._priorityPlan_.daysToShip;
  }
}

after

class Order {
  get daysToShip() {
    return this._priorityDelegate ? this._priorityDelegate.daysToShip : this._warehouse.daysToShip;
  }
}
class PriorityOrder extends Order {
  get daysToShip() {
    return this._priorityPlan_.daysToShip;
  }
}

11. Replace Superclass with Delegate, 슈퍼클래스를 위임으로 바꾸기

  • 제대로된 상속이라면 서브클래스가 슈퍼클래스의 모든 기능을 사용함은 물론, 서브클래스의 인스턴스를 슈퍼클래스의 인스턴스로도 취급할 수 있어야 한다.
  • 위임을 이용하면 기능 일부만 빌려올 뿐, 서로 별개의 개념임이 명확해진다.
  • 위임에도 단점은 있다.
    • 위임할 호스트의 함수 모두를 전달 함수로 만들어야 한다는 점이다.
  • 상위 타입의 모든 메서드가 하위 타입에도 적용되고, 하위 타입의 모든 인스턴스가 상위 타입의 인스턴스도 되는 등, 의미상 적합한 조건이라면 상속은 간단하고 효과적인 메커니즘이다.

절차

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

예시

before

class List {
  /*..*/
}
class Stack extends List {
  /*..*/
}

after

class Stack {
  constructor() {
    this._storage = new List();
  }
}
class List {
  /*..*/
}