7. 서브클래스 제거하기서브클래싱?서브클래싱의 문제점절차예시8. 슈퍼클래스 추출하기저자가 말하는 상속절차예시9. 계층 합치기절차10. 서브클래스를 위임으로 바꾸기저자가 말하는 상속의 단점상속은 위험할까?절차예시정말 서브클래스를 위임으로 바꿔야 하는 구조일까? 🤔11. 슈퍼클래스를 위임으로 바꾸기위임의 단점절차예시
7. 서브클래스 제거하기
서브클래싱?
- 원래 데이터 구조와는 다른 변종을 만들거나 종류에 따라 동작이 달라지게 할 수 있는 유용한 메커니즘이다.
- 다름을 프로그래밍하는 멋진 수단
서브클래싱의 문제점
- 시스템 규모가 커짐에 따라 서브클래스가 다른 모듈로 이동하거나 사라져서 활용되지 않을 수 있다.
- 서브클래스를 필요로 하지 않는 방식으로 만들어진 기능에서만 사용되기도 한다.
절차
- 서브클래스의 생성자를 팩터리 함수로 변경한다.
- 서브클래스의 타입을 검사하는 코드가 있다면, 함수 추출하기와 함수 옮기기를 차례로 적용하여 슈퍼클래스로 옮긴다.
- 서브클래스의 타입을 나태내는 필드를 슈퍼클래스에 만든다.
- 서브클래스를 참조하는 메서드가 방금 만든 타입 필드를 이용하도록 수정한다.
- 서브클래스를 지운다.
- 테스트한다.
예시
class Person { constructor(name) { this._name = name; } get genderCode() { return "X"; } } class Male extends Person { get genderCode() { return "M"; } } class Femail extends Person { get genderCode() { return "F"; } }
리팩터링
function 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"; } }
class Person { constructor(name, genderCode) { this._name = name; this._genderCode = genderCode; // 서브클래스의 타입을 나태내는 필드 생성 } get genderCode() { return this._genderCode; } } // 서브클래스 지우기
8. 슈퍼클래스 추출하기
저자가 말하는 상속
‘현실 세계의 분류 체계를 기초로 하여 부모-자식 관계를 신중하게 설계해야 한다.’ 라는 말처럼 현실 세계의 분류 체계는 상속을 적용하는데 힌트가 될 수 있지만, 상속은 슈퍼클래스로 끌어올리고 싶은 공통 요소를 찾았을 때 수행하는 사례가 잦았다.
- 비슷한 일을 수행하는 두 클래스가 있을 때 비슷한 부분을 슈퍼클래스로 옮겨 상속 관계를 형성할 수 있다.
절차
- 빈 슈퍼클래스를 만든다. 원래의 클래스들이 새 클래스를 상속하도록 한다.
- 테스트한다.
- 생성자 본문 올리기, 메서드 올리기, 필드 올리기를 차례로 적용하여 공통 원소를 슈퍼클래스로 옮긴다.
- 서브클래스에 남은 메서드들을 검토한다. 공통되는 부분은 함수로 추출한 다음 메서드 올리기를 적용한다.
- 원래 클래스를 사용하는 코드를 검토하여 슈퍼 클래스의 인터페이스를 사용하게 할지 고민한다.
예시
class Employee { constructor(name, id, monthlyCost) { this._id = id; this._name = name; this._monthlyCost = monthlyCost; } get name() { return this._name; } get monthlyCost() { return this._monthlyCost; } get annualCost() { return this.monthlyCost * 12; } } class Department { constructor(name, staff) { this._name = name; this._staff = staff; } get name() { return this._name; } get totalMonthlyCost() { return this.staff .map((e) => e.monthlyCost) .reduce((sum, cost) => sum + cost); } get totalAnnualCost() { return this.totalMonthlyCost * 12; } }
리팩터링
class Party { constructor(name) { // 필드 올리기 this._name = name; } get name() { // 메서드 올리기 return this._name; } get annualCost() { // 함수 선언 바꾸기 & 메서드 올리기 return this.monthlyCost * 12; } } class Employee extends Party { constructor(name, id, monthlyCost) { super(name); this._id = id; this._monthlyCost = monthlyCost; } get monthlyCost() { return this._monthlyCost; } } class Department extends Party { constructor(name, staff) { super(name); this._staff = staff; } get monthlyCost() { return this.staff .map((e) => e.monthlyCost) .reduce((sum, cost) => sum + cost); } }
9. 계층 합치기
- 슈퍼클래스와 서브클래스가 비슷해져 독립적으로 존재해야 할 이유가 사라지는 경우에 적용할 수 있다.
절차
- 두 클래스 중 제거할 것을 고른다.
- 필드 올리기와 메서드 올리기 혹은 필드 내리기와 메서드 내리기를 적용하여 모든 요소를 하나의 클래스로 옮긴다.
- 제거할 클래스를 참조하던 모든 코드가 남겨질 클래스를 참조하도록 고친다.
- 빈 클래스를 제거한다.
- 테스트한다.
10. 서브클래스를 위임으로 바꾸기
저자가 말하는 상속의 단점
- 달라져야 하는 이유가 여러 개여도 상속에서는 단 하나의 이유만 선택해 기준으로 삼는다.
- 젊은이 - 어르신
- 부자 - 서민
- 관계가 긴밀하여 부모를 수정했을 때 자식에서 발생할 수 있는 사이드 이펙트를 주의해야 한다.
상속은 위험할까?
클래스의 상속보다 객체 컴포지션을 사용하라!
많은 사람들이 이 원칙을 듣고 ‘상속은 위험하다’라고 받아들여서 상속을 하면 안된다고 주장한다고 한다. 이 원칙은 상속을 쓰지 말라는 말이 아닌, 과용하는데 따른 반작용으로 나온 것이다.
책의 저자는 언제든 서브클래스를 위임으로 바꿀 수 있음을 알기에, 처음에는 상속으로 접근하고 문제가 생기면 위임으로 갈아탄다고 설명한다.
절차
- 생성자를 호출하는 곳이 많다면 생성자를 팩터리 함수로 바꾼다.
- 위임으로 활용할 빈 클래스를 만든다. 이 클래스의 생성자는 서브클래스에 특화된 데이터를 전부 받아야 하며, 보통은 슈퍼클래스를 가리키는 역참조도 필요하다.
- 위임을 저장할 필드를 슈퍼클래스에 추가한다.
- 서브클래스 생성 코드를 수정하여 위임 인스턴스를 생성하고 위임 필드에 대입해 초기화한다.
- 서브클래스의 메서드 중 위임 클래스로 이동할 것을 고른다.
- 함수 옮기기를 적용해 위임 클래스로 옮긴다. 원래 메서드에서 위임하는 코드는 지우지 않는다.
- 서브클래스 외부에도 원래 메서드를 호출하는 코드가 있다면 서브클래스의 위임 코드를 슈퍼클래스로 옮긴다.
- 위임이 존재하는지를 검사하는 보호 코드로 감싸야 한다. 호출하는 외부 코드가 없다면 원래 메서드는 죽은 코드가 되므로 제거한다.
- 테스트한다.
- 서브클래스의 모든 메서드가 옮겨질 때까지 5~8 과정을 반복한다.
- 서브클래스들의 생성자를 호출하는 코드를 찾아서 슈퍼클래스의 생성자를 사용하도록 수정한다.
- 테스트한다.
- 서브클래스를 삭제한다.
예시
class Booking { constructor(show, date) { this._show = show; this._date = date; } get hasTalkback() { return this._show.hasOwnProperty("talback") && !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("talback"); // 오버라이드 } get basePrice() { return Math.round(super.basePrice + this._extras.premiumFee); // 오버라이드 } get hasDinner() { return this._extras.hasOwnProperty("dinner") && !this.isPeakDay; } }
정말 서브클래스를 위임으로 바꿔야 하는 구조일까? 🤔
만약 일반 예약과 프리미엄 예약을 자유롭게 전환해야 한다면 어떨까?
- HTTP 요청을 통해 새로운 데이터를 받아서 처리한다.
- 새로 받아오는 방법을 사용하지 못하고, 데이터 구조를 수정해야 하는 상황 이라면?
- 수 많은 곳에서 참조되는 예약 인스턴스를 어떻게 교체하지?
위 예시가 와닿지 않는다면, 예약을 일반과 프리미엄이라는 기준 대신 다른 기준을 가져가야 한다면 어떨까?
리팩터링
// 생성자를 팩터리 함수로 변경 function createBooking(show, date) { return new Booking(show, date); } // 예약 객체와 위임 클래스를 연결 function createPremiumBooking(show, date, extras) { const result = new Booking(show, date, extras); result._bePremium(extras); return result; }
class Booking { constructor(show, date) { this._show = show; this._date = date; } _bePremium(extras) { this._premiumDelegate = new PremiumBookingDelegate(this, extras); } get hasTalkback() { return this._premiumDelegate ? this.PremiumBookingDelegate.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; } get hasDinner() { return this._premiumDelegate ? this._premiumDelegate.hasDinner : undefined; } } class PremiumBookingDelegate { // 위임 클래스는 서브클래스가 사용하던 매개변수와 예약 객체로의 역참조(this)를 매개변수로 받는다. constructor(hostBooking, extras) { this._host = hostBooking; this._extras = extras; } get hasTalkback() { return this._host._show.hasOwnProperty("talkback"); } extendBasePrice(base) { return Math.round(base + this._extras.premiumFee); } }
11. 슈퍼클래스를 위임으로 바꾸기
위임의 단점
- 위임의 기능을 이용할 호스트의 함수를 모두 전달 함수로 만들어야 한다.
절차
- 슈퍼클래스 객체를 참조하는 필드를 서브클래스에 만든다. 위임 참조를 새로운 슈퍼클래스 인스턴스로 초기화한다.
- 슈퍼클래스의 동작 각각에 대응하는 전달 함수를 서브클래스에 만든다.
- 서로 관련된 함수끼리 그룹으로 묶어 진행하며, 그룹을 하나씩 만들 때마다 테스트한다.
예시
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, dateLastCleaned) { super(id, title, tags); this._dateLastCleaned = dateLastCleaned; } needsCleaning(targetDate) { const threshold = this.hasTag("revered") ? 700 : 1500; return this.daysSinceLastCleaning(targetDate) > threshold; } daysSinceLastCleaning(targetDate) { return this._dateLastCleaned.until(targetDate, ChronoUnit.DAYS); } }