메서드 올리기
// before
class Parent {
}
class Child1 extends Parent {
get name() {}
}
class Child2 extends Parent {
get name() {}
}
// after
class Parent {
get name() {}
}
class Child1 extends Parent {
}
class Child2 extends Parent {
}
- 부모에게는 없는데 자식에게만 중복되서 등장하는 필드, 메서드들이 있는 경우 이를 부모로 올려주는 것이 중복을 막을 수 있다.
- 만약 동작이 다를 경우, 템플릿 메서드 만들기를 고려할 수 있다.
필드 올리기
// before
class Parent {
}
class Child1 extends Parent {
private String name;
}
class Child2 extends Parent {
private String name;
}
// after
class Parent {
protected String name;
}
class Child1 extends Parent {
}
class Child2 extends Parent {
}
- 위는 자바 코드로, 필드 또한 자식에게만 중복으로 등장하고, 부모에게는 없는 경우 이를 부모로 올려주는 것이 중복을 제거할 수 있는 방법이다.
생성자 본문 올리기
// before
class Parent {
}
class Child1 extends Parent {
constructor (name, id, cost) {
super();
this.id = id;
this.name = name;
this.cost = cost;
}
}
// after
class Parent {
constructor (name) {
this.name = name;
}
}
class Child1 extends Parent {
constructor (name, id, cost) {
super(name);
this.id = id;
this.cost = cost;
}
}
- 위 두개의 경우가 생성자에 등장하는 경우이다.
- 이 경우, 부모 생성자에서 공통 필드를 초기화하는 코드를 추가하고, 서브 클래스에서 해당 부모 생성자를 호출하게 하는 것이 중복을 막을 수 있다.
메서드 내리기
// before
class Parent {
get name() {}
}
class Child1 extends Parent {}
class Child2 extends Parent {}
// after
class Parent {}
class Child1 extends Parent {}
class Child2 extends Parent {
get name() {}
}
- 위와는 반대로, 특정 자식에게만 존재하는 메서드를 위해 부모가 가지고 있는 경우, 이를 해당 자식에게 내려주는 것이 깔끔하다.
필드 내리기
// before
class Parent {
protected String name;
}
class Child1 extends Parent {}
class Child2 extends Parent {}
// after
class Parent {}
class Child1 extends Parent {}
class Child2 extends Parent {
private String name;
}
- 위와 반대로, 특정 자식에게만 존재할 필드를 위해 부모가 가지고 있는 것보다는 이를 특정 자식에게 내려주는 것이 깔끔하다.
타입 코드를 서브클래스로 바꾸기
// 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);
}
}
- 비슷한 대상을 특정 특성에 따라 구분할 때, 이러한 구분자를 타입 코드 필드라고 한다.
- 만약 타입으로 구분된 각자가 서로 상당히 다른 로직을 구성해야 한다면, 서브클래스로 추출한 뒤 type에 따라 각기 다른 서브 클래스의 객체를 리턴해주는 것이 좋다.
- 생성자에서는 이런게 불가능하므로, 팩터리 메서드를 사용하는 것이 다시 한번 더 좋음을 볼 수 있다.
서브클래스 제거하기
// before
class Person {
get genderCode() {return "X";}
}
class Male extends Person {
get genderCode() {return "M";}
}
class Female extends Person {
get genderCode() {return "Y";}
}
// after
class Person {
get genderCode() {
return this.genderCode;
}
}
- 서브클래스는 원래 데이터 구조와 상당히 다른 동작이나 변종을 다형성으로 만들기 위해 존재한다.
- 그러나 거의 차이가 없는 경우, 코드량과 복잡도만 높일 뿐이다.
- 이 경우, 서브클래스들을 슈퍼클래스에 합치는 게 더 나을 것이다.
슈퍼클래스 추출하기
// before
class Department {
get annualCost() {}
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() {}
}
- 상속 구조는 설계를 철저히 하고, 부모-자식 관계를 신중히 설계해야 한다는 사실은 맞는 얘기다.
- 그러나 상속의 경우 프로그래밍을 수행하면서 공통 부분을 발견했을 때 추출하는 경우가 더 많았다고 한다.
- 슈퍼클래스로 추출하느냐, 단순히 클래스를 하나 추출해서 위임으로 해결하냐 (상속이냐 위임이냐)는 선택의 문제이다.
- 저자의 추천으로는 먼저 슈퍼클래스로 추출하는 것이 더 간단하고, 만약 맞지 않을 경우에는 슈퍼클래스를 위임으로 바꾸는 것 또한 어렵지 않으므로 그렇게 한다고 한다.
계층 합치기
// before
class Employee {}
class Salesperson extends Employee {}
// after
class Employee {}
- 개발을 하다가 어떤 클래스와 부모가 너무 비슷해져 독립적으로 존재해야할 필요성을 못 느낄 때가 있다.
- 이 경우 그 둘을 하나로 합치는 게 낫다.
서브클래스를 위임으로 바꾸기
// 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 PriorityOrderDelegate {
get daysToShip() {
return this.priorityPlan.daysToShip;
}
}
- 공통 데이터와 동작을 슈퍼 클래스로 빼고, 서브 클래스에는 자신에 맞게 기능을 추가하거나 오버라이드하는 것이 상속의 장점이다.
- 그러나 상속은 한번만 쓸수 있다. 다중 상속을 지원하지 않는 언어가 많다.
- 가령 사람 객체의 동작을 나이와 소득 수준에 따라 달리 하고 싶으면 둘다가 안되고, 젊은이 혹은 어르신, 부자 혹은 서민 만 될 수 있다.
- 또한 상속은 클래스간의 관계를 아주 긴밀하게 결합함
- 부모를 수정하면 이미 존재하는 자식들의 기능이 바뀔 수 있다.
- 그래서 상속보다는 컴포지션을 사용하라는 말이 나온다. 컴포지션은 위임과 같은 말이라고 봐도 좋다.
- 하지만 상속이 당장 바꾸기에는 쉽다. 따라서 상속을 먼저 수행하고, 나중에 필요하면 위임으로 바꾸는 것은 쉽다.
- 이 리팩터링은 서브클래스를 상태 패턴, 전략 패턴으로 대체하는 것과 비슷
- 이 리팩터링의 핵심은 생성자를 팩터리 함수로 바꾸는 게 좋다는 것이다.
- 헷갈리면 안되는게, 이 리팩터링은 자식 객체가 부모한테 들어가는 것이다.
슈퍼클래스를 위임으로 바꾸기
// before
class List {}
class Stack extends List {}
// after
class Stack {
constructor() {
this.storage = new List();
}
}
class List {}
- 자바의 스택은 리스트를 상속하고 있는데, 이는 큰 문제가 있다.
- 바로 스택에서는 적용되지 않는 기능들이 스택 인터페이스에 그대로 노출된다.
- 이러한 경우, 슈퍼클래스, 즉 리스트를 위임, 컴포지션으로 받는 것이 자연스럽다.
- 이 리팩터링의 경우는 부모 객체가 자식한테 들어가는 것이다.
- 저자는 상속을 먼저 하고, 나중에 바꿔야할 때 위임으로 바꾸는 걸 추천한다.
https://product.kyobobook.co.kr/detail/S000001810241