개요
상속은 코드 재활용의 손쉬운 수단
상속 잘못 적용 예로 자바 스택 클래스가 대표적
스택은 리스트를 상속하고 있는데, 코드를 재사용하겠다는 취지는 좋았으나 불필요한 모든 기능이 모두 상속됐다.
구성 관계로 리스트를 사용해 필요한 기능만 위임했더라면 스택 API가 이렇게 복잡하지 않았을 것이다.
이렇듯 슈퍼클래스의 기능이 서브클래스에 어울리지 않는다면 상속을 하면 안된다는 신호다.
제대로된 상속은 서브클래스가 슈퍼클래스의 모든 기능을 사용함과 동시에 서브클래스의 인스턴스를 슈퍼클래스의 인스턴스로도 취급할 수 있어야 한다.(리스코프 치환 원칙)
상속은 슈퍼클래스와 서브클래스가 강결합 상태이다. 위임은 일부 기능만 빌려올 뿐 별개이다.
슈퍼 클래스를 수정하면 모든 서브클래스가 영향을 받는다. 반면에 위임은 영향을 안받지만, 단점으로 위임 함수 모두를 전달 함수로 만들어야하는 수고로움이 생긴다.
예시
고서 보관하는 오래된 도서관
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._lastCleaned = dateLastCleaned; } needsCleaning(targetDate){ const threshold = this.hasTag("revered") ? 700 : 1500; return this.daySinceLastCleaning(targetDate) > threshold; } daySinceLastCleaning(targetDate){ return this._lastCleaned.until(targetDate, ChronoUnit.DAYS); } } |
class Scroll extends CatalogItem{ constructor(id, title, tags, dateLastCleaned){ super(id, title, tags); //슈퍼 클래스 참조를 하나 만들고 슈퍼클래스를 생성한다. this._catalogItem = new CatalogItem(id, title, tags); this._lastCleaned = dateLastCleaned; } needsCleaning(targetDate){ const threshold = this.hasTag("revered") ? 700 : 1500; return this.daySinceLastCleaning(targetDate) > threshold; } daySinceLastCleaning(targetDate){ return this._lastCleaned.until(targetDate, ChronoUnit.DAYS); } } |
class Scroll extends CatalogItem{ constructor(id, title, tags, dateLastCleaned){ super(id, title, tags); //슈퍼 클래스 참조를 하나 만들고 슈퍼클래스를 생성한다. this._catalogItem = new CatalogItem(id, title, tags); this._lastCleaned = dateLastCleaned; } //슈퍼 클래스에 대응하는 메서드를 만들고, 전달 메서드화 시킨다. get id(){return this._catalogItem._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.daySinceLastCleaning(targetDate) > threshold; } daySinceLastCleaning(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, title, tags, dateLastCleaned){ //슈퍼 클래스 참조를 하나 만들고 슈퍼클래스를 생성한다. this._catalogItem = new CatalogItem(id, title, tags); this._lastCleaned = dateLastCleaned; } //슈퍼 클래스에 대응하는 메서드를 만들고, 전달 메서드화 시킨다. get id(){return this._catalogItem._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.daySinceLastCleaning(targetDate) > threshold; } daySinceLastCleaning(targetDate){ return this._lastCleaned.until(targetDate, ChronoUnit.DAYS); } } |
더 가다듬기
슈퍼클래스를 위임으로 바꾸는 것은 끝
다만 모든 카탈로그아이템 마다 스크롤 속성을 하나씩 가지게 됐는데, 이를 하나의 참조만 같도록 값을 참조로 바꾸기 리팩터링을 수행한다.
class Scroll{ constructor(id, title, tags, dateLastCleaned){ this._id = id;//먼저 id를 내 속성으로 만들기 this._catalogItem = new CatalogItem(null, title, tags); this._lastCleaned = dateLastCleaned; } 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.daySinceLastCleaning(targetDate) > threshold; } daySinceLastCleaning(targetDate){ return this._lastCleaned.until(targetDate, ChronoUnit.DAYS); } } //스크롤 데이터 읽는 부분 코드 const scrolls = aDocument .map(record => new Scroll(record.id, record.catalogData.title, record.catalogData.tags, LocalDate.parse(record.lastCleaned))); |
class Scroll{//먼저 적절한 저장소 찾기 또는 만들기, 여기선 적절한 저장소가 있다고 가정(catalog) constructor(id, title, tags, dateLastCleaned,catalogID, catalog){ this._id = id; this._catalogItem = catalog.get(catalogID); this._lastCleaned = dateLastCleaned; } 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.daySinceLastCleaning(targetDate) > threshold; } daySinceLastCleaning(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{//먼저 적절한 저장소 찾기 또는 만들기, 여기선 적절한 저장소가 있다고 가정(catalog) constructor(id, dateLastCleaned,catalogID, catalog){ this._id = id; this._catalogItem = catalog.get(catalogID); this._lastCleaned = dateLastCleaned; } 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.daySinceLastCleaning(targetDate) > threshold; } daySinceLastCleaning(targetDate){ return this._lastCleaned.until(targetDate, ChronoUnit.DAYS); } } //스크롤 데이터 읽는 부분 코드 const scrolls = aDocument .map(record => new Scroll(record.id, LocalDate.parse(record.lastCleaned), record.catalogData.id, catalog )); |
'IT책, 강의 > 리팩터링' 카테고리의 다른 글
12 - 상속 다루기 - 서브클래스를 위임으로 바꾸기 (0) | 2023.11.03 |
---|---|
12 - 상속 다루기 - 계층 합치기 (0) | 2023.11.02 |
12 - 상속 다루기 - 슈퍼클래스 추출하기 (1) | 2023.11.01 |
12 - 상속 다루기 - 서브클래스 제거하기 (0) | 2023.10.31 |
12 - 상속 다루기 - 타입 코드를 서브클래스로 바꾸기 (0) | 2023.10.30 |