상속 다루기
메서드 올리기
같은 일을 수행하는 서브클래스들이 있다면 수퍼클래스의 메서드로 올린다.
📜 절차
- 메서드 시그니처가 다르다면
함수 선언 바꾸기
로 수퍼클래스에서 사용하고 싶은 형태로 통일한다. - 수퍼클래스에 새로운 메서드를 생성하고, 대상 메서드의 코드를 복사해넣는다.
- 서브클래스의 메서드들을 제거한다.
필드 올리기
서브클래스 필드들이 비슷한 방식으로 쓰인다고 판단되면 수퍼클래스로 끌어올리도록 한다.
📜 절차
- 필드들의 이름이 각기 다르다면 똑같은 이름으로 바꾼다.
- 수퍼클래스에 새로운 필드를 생성한다.
- 서브클래스의 필드들을 제거한다.
생성자 본문 올리기
서브클래스에서 공통되는 생성자 코드가 있다면 수퍼클래스의 생성자로 올린다.
📜 절차
- 수퍼클래스에 생성자가 없다면 하나 정의한다.
- 공통 코드를 수퍼클래스에 추가하고 서브클래스들에서는 제거한다. 생성자 매개변수 중 공통 코드에서 참조하는 값들을 모두 super()로 건넨다.
- 생성자 시작 부분으로 옮길 수 없는 공통 코드에는
함수 추출하기
와메서드 올리기
를 차례로 적용한다.
다음의 예제를 보자.
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;
}
}
메서드 내리기
* 반대 리펙터링: 메서드 올리기
특정 서브클래스 하나(혹은 소수)와만 관련된 메서드는 수퍼클래스에서 제거하고 해당 서브클래스(들)에 추가하도록 한다.
📜 절차
- 대상 메서드를 모든 서브클래스에 복사한다.
- 수퍼클래스에서 그 메서드를 제거한다.
- 이 메서드를 사용하지 않는 모든 서브클래스에서 제거한다.
필드 내리기
* 반대 리펙터링: 필드 올리기
서브클래스 하나(혹은 소수)에서만 사용하는 필드라면, 해당 서브클래스(들)로 필드를 옮긴다.
📜 절차
- 대상 필드를 모든 서브클래스에 정의한다.
- 수퍼클래스에서 그 필드를 제거한다.
- 이 필드를 사용하지 않는 모든 서브클래스에서 제거한다.
타입 코드를 서브클래스로 바꾸기
필드 값에 따라 동작이 달라지는 코드가 있다면, 이런 필드를 서브 클래스로 대체하도록 한다.
📜 절차
- 타입 코드 필드를 자가 캡슐화한다.
- 타입 코드 값에 해당하는 서브클래스를 만든다. 타입 코드 게터 메서드를 오버라이드하여 해당 타입 코드의 리터럴 값을 반환하게 한다.
- 매개변수로 받은 타입 코드와 방금 만든 서브클래스를 매핑하는 선택 로직을 만든다.
- 모든 서브클래스에 대한 생성이 완료되면, 타입 코드 필드를 제거한다.
- 타입 코드 접근자를 이용하는 메서드 모두에
메서드 내리기
와조건부 로직을 다형성으로 바꾸기
를 적용한다.
📚 직접 상속
다음은 직원 코드 예제다.
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
메서드를 이동해 리팩터링을 마무리한다.
서브클래스 제거하기
* 반대 리펙터링: 타입 코드를 서브클래스로 바꾸기
더이상 쓰이지 않는 서브클래스를 발견한다면 제거하도록 한다.
📜 절차
- 서브클래스의 생성자를 팩터리 함수를 바꾼다.
- 서브클래스의 타입을 검사하는 코드가 있다면 그 검사 코드에
함수 추출하기
와함수 옮기기
를 차례로 적용하여 수퍼클래스로 옮긴다. - 서브클래스의 타입을 나타내는 필드를 수퍼클래스에 만든다.
- 서브클래스를 참조하는 메서드가 방금 만든 타입 필드를 이용하도록 수정한다.
- 서브클래스를 지운다.
다음의 예제를 보자.
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 타입 관련 코드를 수퍼클래스로 옮기고, 성별 코드를 사용하고 있는 loadFromInput
와 numberOfMales
함수 또한 이에 맞춰 수정한다.
모든 작업이 끝났다면, 서브클래스들을 제거한다.
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;
};
수퍼클래스 추출하기
비슷한 일을 수행하는 두 클래스가 보이면 비슷한 부분을 공통의 수퍼클래스로 옮기도록 한다.
📜 절차
- 빈 수퍼클래스를 만든다. 원래의 클래스들이 새 클래스를 상속하도록 한다.
생성자 본문 올리기
,메서드 올리기
,필드 올리기
를 차례로 적용하여 공통 원소를 수퍼클래스로 옮긴다.- 서브클래스에 남은 메서드들을 검토한다. 공통되는 부분이 있다면
함수로 추출
한 다음메서드 올리기
를 적용한다.
다음의 예제를 보자.
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
를 수퍼클래스로 올린다. 이때 monthlyCost
와 annualCost
는 각 클래스에서 이름도 다르고 코드도 다르지만 의도는 같기 때문에, 이름을 하나로 통일해서 수퍼클래스로 올린다.
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 메서드 제거
}
계층 합치기
어떤 클래스와 그 부모가 너무 비슷해져서 더는 독립적으로 존재해야할 이유가 사라진다면 그 둘을 하나로 합치도록 한다.
📜 절차
필드 올리기
와메서드 올리기
혹은필드 내리기
와메서드 내리기
를 적용해 모든 요소를 하나의 클래스로 옮긴다.- 제거할 클래스를 참조하던 모든 코드가 남겨질 클래스를 참조하도록 고친다.
- 빈 클래스를 제거한다.
서브클래스를 위임으로 바꾸기
상속이 잘못된 곳에서 사용되거나 환경이 변해 문제가 생긴다면 서브클래스를 위임으로 바꾸도록 한다.
📜 절차
- 생성자를 호출하는 곳이 많다면
생성자를 팩터리 함수
로 바꾼다. - 위임으로 활용할 빈 클래스를 만든다.
- 위임을 저장할 필드를 수퍼클래스에 추가한다.
- 서브클래스 생성 코드를 수정하여 위임 필드를 생성하고 위임 필드에 대입해 초기화한다.
함수 옮기기
를 적용해 서브클래스의 메서드를 위임 클래스로 옮긴다.- 서브 클래스 외부에도 원래 메서드를 호출하는 코드가 있다면 서브클래스의 위임 코드를 수퍼클래스로 옮긴다.
- 모두 옮겼다면 수퍼클래스의 생성자를 사용하도록 수정하고, 서브클래스를 삭제한다.
📚 서브클래스가 하나일 때
다음은 공연 예약에 대한 예제다. 서브클래스로 추가 비용을 다양하게 설정할 수 있는 프리미엄 예약용 클래스가 있다.
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
자체와 미래의 서브클래스들에 남게 되었다.
수퍼클래스를 위임으로 바꾸기
수퍼클래스의 기능들이 서브클래스에는 어울리지 않는다면 그 기능들을 상속을 통해 이용하지 말고 필요한 기능만 위임하도록 한다.
📜 절차
- 수퍼클래스 객체를 참조하는 필드를 서브클래스에 만든다. 위임할 객체를 새로운 수퍼클래스 인스턴스로 초기화한다.
- 수퍼클래스의 동작 각각에 대응하는 전달 함수를 서브클래스에 만든다.
- 수퍼클래스의 동작 모두가 전달 함수로 오버라이드되었다면 상속 관계를 끊는다.
다음의 예시를 보자. 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;
}
}
수퍼클래스를 위임으로 바꿔보자. 먼저 Scroll
에 CatalogItem
을 참조하는 속성을 만들고 수퍼클래스의 인스턴스를 하나 만들어 대입한다.
// ...
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;
}
}
상속이나 위임 어느 하나만 고집하지 말고, 적절히 혼용하도록 한다.
Author And Source
이 문제에 관하여(상속 다루기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@niyu/refactoring-ch12저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)