개요

상속은 코드 재활용의 손쉬운 수단

 

상속 잘못 적용 예로 자바 스택 클래스가 대표적

스택은 리스트를 상속하고 있는데, 코드를 재사용하겠다는 취지는 좋았으나 불필요한 모든 기능이 모두 상속됐다.

 

구성 관계로 리스트를 사용해 필요한 기능만 위임했더라면 스택 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 ));

 

 

 

개요

다중 상속 언어가 아니면 일반적으로 상속은 단일 상속만 된다. 바꿔말하면 하나의 기준으로 분류할 밖에 없다. 상속은 부모클래스와 자식클래스가 강하게 묶이게된다. 부모를 수정하면 예외없이 모든 자식이 영향을 받는다.(단점이라는 소리는 아님)

 

위임은 이런 문제가 없다. 상속보다 결합도가 훨씬 낮기 때문이다.

 

디자인 원칙으로 "상속보다 구성을 사용하라"라는 말이 있다.

상속은 컴파일 시점에 행동이 고정된다. 반면에 구성은 런타임 시점에 동적으로 행동을 변화시킬 있다.

 

서브클래스를 위임으로 바꾸기 리팩터링은 디자인 패턴으로 따지면, 서브클래스를 상태나 전략 패턴으로 대체하기로 있다.

예시

class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    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;
    }
}
// 클라이언트 (일반 예약)
aBooking = new Booking(show, date);
// 클라이언트 (프리미엄 예약)
aBooking = new PremiumBooking(show, date, extras);

서브 클래스를 위임으로 바꾸는 이유, 상속은 번만 가능하다. 다른 상속을 사용해야할 이유가 지금 상속 구조보다 크다면, 지금 상속은 구성으로 바꿔야한다.


//생성자를 팩터리 함수로 변경해 캡슐화
function createBooking(show, date){
    return new Booking(show, date);
}
function createPremiumBooking(show, date, extras){
    return new PremiumBooking(show, date, extras);
}




// 클라이언트 (일반 예약)
aBooking = createBooking(show, date);
// 클라이언트 (프리미엄 예약)
aBooking = createPremiumBooking(show, date, extras);


//위임 클래스 만들기
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        //서브클래스는 super 키워드로 부모 클래스 멤버에 쉽게 접근할 수 있지만,
        //위임에선 역참조가 필수다.
        this._host = hostBooking;
        this._extras =extras;
    }
}


//프리미엄 예약을 대체할 새로운 위임을 연결
function createPremiumBooking(show, date, extras){
    const result = new PremiumBooking(show, date, extras);
    result._bePremium(extras);
    return result;
}


class Booking{
    constructor(show, date){
        this._show = show;
        this._date = date;
    }
    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;
    }
    //이메서드가 private이라는 의미로 _ 붙임
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
}


class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
    get hasTalkback(){
        return this._premiumDelegate.hasTalkback;
    }
    get basePrice(){
        return Math.round(super.basePrice + this._extras.premiumFee);
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}
//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
}


class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
    /* 위임 메서드가 잘 동작하면, 이제 제거
    get hasTalkback(){
        return this._premiumDelegate.hasTalkback;
    }*/
    get basePrice(){
        return Math.round(super.basePrice + this._extras.premiumFee);
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}


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 result;
    }
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
}
//문제 발생
class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
    get basePrice(){
        return this._premiumDelegate.basePrice;
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}
//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
    //super로 부모 메서드 호출하는 부분을 방금과 같은 방식으로 리팩터링하면 무한 재귀에 빠진다., 상속을 구성으로 변경할 흔히 발생하는 문제
    get basePrice(){
        return Math.round(this._host.basePrice + this._extras.premiumFee);
    }
}


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;
    }
    //해결방안 1
    //슈퍼 클래스의 계산 로직을 함수로 추출, 가격 계산과 분배 로직 분리
    get basePrice(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.basePrice
            : this._privateBasePrice;
    }
    get _privateBasePrice(){
        let result = this._show.price;
        if(this.isPeakDay) result += Math.round(result * 0.15);
        return result;
    }
    _bePremium(extras){
        this._premiumDelegate = new PremiumBookingDelegate(this, extras);
    }
}


//위임 클래스
class PremiumBookingDelegate{
    constructor(hostBooking, extras){
        this._host = hostBooking;
        this._extras =extras;
    }
    get hasTalkback(){
        return this._host._show.hasOwnProperty('talkback');
    }
    get basePrice(){
        return Math.round(this._host._privateBasePrice + this._extras.premiumFee);
    }
}



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;
    }
    //해결방안 2
    // 위임 메서드를 기반 메서드의 확장 형태로 재호출
    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);
    }
}


//위임 클래스
class PremiumBookingDelegate{
    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);
    }
}


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;
    }
}
class PremiumBooking extends Booking{
    constructor(show, date, extras){
        super(show, date);
        this._extras = extras;
    }
}
//위임 클래스
class PremiumBookingDelegate{
    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);
    }


    get hasDinner(){
        return (this._premiumDelegate)
            ? this._premiumDelegate.hasDinner
            : undefined; //자바스크립트는 에러를 유연하게 처리하는 방식이 주류
            //다른 객체지향동적 언어라면, 예외를 던지는게 더 나은 선택일 수 있다.
    }
}


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;
    }
    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;
    }
}
//위임 클래스
class PremiumBookingDelegate{
    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);
    }
    get hasDinner(){
        return this._extras.hasOwnProperty('dinner') && !this.isPeakDay;
    }
}
// 클라이언트 (일반 예약)
aBooking = createBooking(show, date);
// 클라이언트 (프리미엄 예약)
aBooking = createPremiumBooking(show, date, extras);

상속을 구성으로 바꾸면서, 분배 로직과 양방향 참조 추가 복잡도가 높아졌다. 그만큼 유연성을 증가해 런타임 동적으로 프리미엄 예약으로 바꿀 있다는 이점이 생겼다. 추가로 필요한 곳에 상속을 사용할 있게 됐다.

 

 

예시 : 서브클래스가 여러 개일



function 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 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;
    }
}
야생 조류와 사육 조류를 구분 짓기
이렇게 구분 지을 경우, 기존 종에 따른 분류를 포기해야 한다.


//EuropeanSwallow부터 시작, 빈 위임 클래스 생성
class EuropeanSwallowDelegate{
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    //위임을 처리할 메서드 생성
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){return null;}
}


//위임 메서드로 옮김
class EuropeanSwallowDelegate{
    get airSpeedVelocity(){return 35;}
}
class EuropeanSwallow extends Bird{
    get airSpeedVelocity(){return this._speciesDelegate.airSpeedVelocity;}
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    //분배 메서드 처리
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}


//유럽 제비 클래스 제거, 관련 코드 정리
function createBird(data){
    switch(data.type){
        case '아프리카 제비' :
            return new AfricanSwallow(data);
        case '노르웨이 제비' :
            return new NorwegianBlueParrot(data);
        default : return new Bird(data);
    }
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data);
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}


//아프리카 제비 클래스 위임 처리
class AfricanSwallowDelegate{
    constructor(data){
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
}
class AfricanSwallow extends Bird{
    constructor(data){
        super(data);
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}


//메서드 옮기기
class AfricanSwallowDelegate{
    constructor(data){
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class AfricanSwallow extends Bird{
    constructor(data){
        super(data);
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return this._speciesDelegate.airSpeedVelocity;
    }
}


//아프리카 제비 클래스 제거, 관련 코드 정리
function createBird(data){
    switch(data.type){
        case '노르웨이 제비' :
            return new NorwegianBlueParrot(data);
        default : return new Bird(data);
    }
}
class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data);
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
class EuropeanSwallowDelegate{
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallowDelegate{
    constructor(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;
    }
}


function createBird(data){
    switch(data.type){
        case '노르웨이 제비' :
            return new NorwegianBlueParrot(data);
        default : return new Bird(data);
    }
}
//노르웨이 제비도 같은 작업 수행
class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data);
            case '노르웨이 제비' :
                return new NorwegianBlueParrotDelegate(data);
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
class EuropeanSwallowDelegate{
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallowDelegate{
    constructor(data){
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class NorwegianBlueParrotDelegate{
    constructor(data){
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get airSpeedVelocity(){
        return( this._isNailed) ? 0 : 10 + this._voltage / 10;
    }
}
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._speciesDelegate.airSpeedVelocity;
    }
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate();
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data);
            case '노르웨이 제비' :
                return new NorwegianBlueParrotDelegate(data,this);
            default: return null;
        }
    }
    get name(){return this._name;}
    get plumage(){return this._plumage || "보통이다";}
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
class NorwegianBlueParrotDelegate{
    //plumage() 옮기기, 오버라이드 메서드로 역참조 필요
    constructor(data, bird){
        this._bird = 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 || "예쁘다";
    }
}
class NorwegianBlueParrot extends Bird{
    constructor(data){
        super(data);
        this._voltage = data.voltage;
        this._isNailed = data.isNailed;
    }
    get plumage(){
        return this._speciesDelegate.plumage;
    }
    get airSpeedVelocity(){
        return this._speciesDelegate.airSpeedVelocity;
    }
}
    //plumage() 분배 처리 문제 1
    get plumage(){
        if(this._speciesDelegate)
            return this._speciesDelegate.plumage;
        else
            return this._plumage || "보통이다";
    }
    //plumage() 분배 처리 문제 2
    //instanceof 연산자를 사용해 특정 클래스만 검사하는 것은 악취나는 코드다.
    get plumage(){
        if(this._speciesDelegate instanceof NorwegianBlueParrotDelegate)
            return this._speciesDelegate.plumage;
        else
            return this._plumage || "보통이다";
    }


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(data){
        switch(data.type){
            case "유럽 제비" :
                return new EuropeanSwallowDelegate(this);
            case "아프리카 제비" :
                return new AfricanSwallowDelegate(data,this);
            case '노르웨이 제비' :
                return new NorwegianBlueParrotDelegate(data,this);
            default: return null;
        }
    }
    get name(){return this._name;}
    //NorwegianBlueParrotDelegate만 특별 취급해보기
    get plumage(){
        if(this._speciesDelegate)
            return this._speciesDelegate.plumage;
        else
            return this._plumage || "보통이다";
    }
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
//plumage() 기본 메서드가 중복된다. 그리고 역참조를 추가하는 코드도 중복된다.
class EuropeanSwallowDelegate{
    constructor(bird){
        this._bird = bird;
    }
    get airSpeedVelocity(){return 35;}
    get plumage(){
        return this._bird._plumage || "예쁘다";
    }
}
class AfricanSwallowDelegate{
    constructor(data,bird){
        this._bird = bird;
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
    get plumage(){
        return this._bird._plumage || "예쁘다";
    }
}
class NorwegianBlueParrotDelegate{
    constructor(data, bird){
        this._bird = 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 || "예쁘다";
    }
}


class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(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);
        }
    }
    get name(){return this._name;}
    get plumage(){
        return this._speciesDelegate.plumage;
    }
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
//이 중복을 제거하기 위해 위임 클래스들에서 공통부분을 뽑아 슈퍼클래스로 만든다.
class SpeciesDelegate{
    constructor(data, bird){
        this._bird = bird;
    }
    get plumage(){
        return this._bird._plumage || "예쁘다";
    }
    airSpeedVelocity(){return null;}
}
class EuropeanSwallowDelegate extends SpeciesDelegate{
    constructor(data,bird){
        super(data, bird);
    }
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallowDelegate extends SpeciesDelegate{
    constructor(data,bird){
        super(data, bird);
        this._bird = bird;
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class NorwegianBlueParrotDelegate extends SpeciesDelegate{
    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 || "예쁘다";
    }
}
//완료
function createBird(data){
    return new Bird(data);
}
class Bird{
    constructor(data){
        this._name = data.name;
        this._plumage = data.plumage;
        this._speciesDelegate = this.selectSpeciesDelegate(data);
    }
    selectSpeciesDelegate(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);
        }
    }
    get name(){return this._name;}
    get plumage(){return this._speciesDelegate.plumage; }
    get airSpeedVelocity(){
        return (this._speciesDelegate)
            ? this._speciesDelegate.airSpeedVelocity
            : null;
    }
}
class SpeciesDelegate{
    constructor(data, bird){
        this._bird = bird;
    }
    get plumage(){
        return this._bird._plumage || "예쁘다";
    }
    airSpeedVelocity(){return null;}
}
class EuropeanSwallowDelegate extends SpeciesDelegate{
    constructor(data,bird){
        super(data, bird);
    }
    get airSpeedVelocity(){return 35;}
}
class AfricanSwallowDelegate extends SpeciesDelegate{
    constructor(data,bird){
        super(data, bird);
        this._bird = bird;
        this._numberOfCoconuts = data._numberOfCoconuts;
    }
    get airSpeedVelocity(){
        return 40 - 2 * this._numberOfCoconuts;
    }
}
class NorwegianBlueParrotDelegate extends SpeciesDelegate{
    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 || "예쁘다";
    }
}

 

개요

시간이 지남에 따라 상속 계층 구조가 이상 필요없어지는 경우 합친다.

예시

class Super{
    common(){/**슈퍼클래스 작업... */    }
}
class Sub extends Super{
    specific(){/**서브클래스 작업... */}
}


class Super{
    common(){/**슈퍼클래스 작업... */    }
    specific(){/**서브클래스 작업... */}
}
//위로 합치던가, 아래로 합치던가 판단하여 적용
class Sub{
    common(){/**슈퍼클래스 작업... */    }
    specific(){/**서브클래스 작업... */}
}

 

개요

비슷한 일을 수행하는 클래스가 보이면, 공통 부분을 추상화해 슈퍼클래스로 옮길 있다. 데이터는 필드 올리기, 동작은 메서드 올리기 리팩터링을 수행한다.

 

비슷한 대안으로 클래스 추출하기가 있다. 차이는 구성( 위임) 이용하냐 상속을 이용하냐 차이다.

 

예시

연간 비용, 월간 비용



class Employee{
    constructor(name, id, monthlyCost){
        this._id = id;
        this._name = name;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get name() {return this._name;}
    get id() {return this._id;}
    get annualCost(){ return this._monthlyCost * 12; }
}
class Department{
    constructor(name, staff){
        this._name = name;
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get name(){return this._name;}
    get totalMonthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get totalAnnualCost(){
        return this.totalMonthlyCost * 12;
    }
}


class Party{} //일단 빈 슈퍼클래스를 만들고 확장한다.
class Employee extends Party{
    constructor(name, id, monthlyCost){
        super();
        this._id = id;
        this._name = name;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get name() {return this._name;}
    get id() {return this._id;}
    get annualCost(){ return this._monthlyCost * 12; }
}
class Department extends Party{
    constructor(name, staff){
        super();
        this._name = name;
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get name(){return this._name;}
    get totalMonthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get totalAnnualCost(){
        return this.totalMonthlyCost * 12;
    }
}



class Party{
    constructor(name){//데이터부터 옮기기(생성자)
        this._name = name;
    }
}
class Employee extends Party{
    constructor(name, id, monthlyCost){
        super(name);
        this._id = id;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get name() {return this._name;}
    get id() {return this._id;}
    get annualCost(){ return this._monthlyCost * 12; }
}
class Department extends Party{
    constructor(name, staff){
        super(name);
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get name(){return this._name;}
    get totalMonthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get totalAnnualCost(){
        return this.totalMonthlyCost * 12;
    }
}


class Party{
    constructor(name){
        this._name = name;
    }
    //옮긴 데이터 관련 메서드 올리기
    get name() {return this._name;}
}
class Employee extends Party{
    constructor(name, id, monthlyCost){
        super(name);
        this._id = id;
        this._monthlyCost = monthlyCost;
    }
    get monthlyCost() {return this._monthlyCost;}
    get id() {return this._id;}
    get annualCost(){ return this._monthlyCost * 12; }
}
class Department extends Party{
    constructor(name, staff){
        super(name);
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get totalMonthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get totalAnnualCost(){
        return this.totalMonthlyCost * 12;
    }
}


class Department extends Party{
    constructor(name, staff){
        super(name);
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    //비슷한 로직은 좀 더 추상화된 쪽 이름으로, 혹은 추상화시켜 함수선언바꾸기
    get monthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
    get annualCost(){
        return this.monthlyCost * 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;}
    get id() {return this._id;}
}
class Department extends Party{
    constructor(name, staff){
        super(name);
        this._staff = staff;
    }
    get staff(){return this._staff.slice();}
    get monthlyCost(){
        return this.staff
            .map(e=e.monthlyCost)
            .reduce((sum,cost)=>sum+cost);
    }
    get headCount(){
        return this.staff.length;
    }
}

 

개요

서브클래싱은 원래 데이터 구조와 다른 변종 또는 종류에 따라 다르게 동작하게 있는 유용한 메커니즘이나, 이상 사용이 안된다면 제거하는 것이 최선

 

예시



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";}
}
//클라이언트
const numberOfMales = people.filter(p=>p instanceof Male).length;


//직관적으로 생성자 마다 팩터리 메서드를 만들 경우
function createPerson(name){
    return new Person(name);
}
function createMale(name){
    return new Male(name);
}
function createFemale(name){
    return new Female(name);
}
//결국, 리팩터링하면 큰 의미가 없다.
function 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);
        }
        result.push(p);
    });
    return result;
}


//생성할 클래스를 선택하는 로직을 함수로 추출하는 것이 더 낫다.
function createPerson(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);
    }
    return p;
}
function loadFromInput(data){
    const result = [];
    data.forEach(aRecord=>{
        result.push(createPerson(aRecord));
    });
    return result;
}


//두 함수 다듬기
function 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);
    }
}
//반복문 파이프라인으로 바꾸기
function loadFromInput(data){
    return data.map(aRecord=>createPerson(aRecord));
}


class Person{
    constructor(name){
        this._name = name;
    }
    //타입 검사 코드를 함수로 추출
    get isMale(){return this instanceof Male;}
    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";}
}
//클라이언트
//클라이언트에서 instanceof 연산자를 사용하는 것부터 구린내가 난다.
const numberOfMales = people.filter(p=>p.isMale).length;


class Person{
    //생성자에 젠더코드 추가
    constructor(name, genderCode){
        this._name = name;
        this._genderCode = genderCode || "X";
    }
    get isMale(){return this instanceof Male;}
    get name(){return this._name;}
    get genderCode(){return this._genderCode;}
}


function createPerson(aRecord){
    switch (aRecord.gender){
        //하나씩 서브클래스 제거
        case 'M' : return new Person(aRecord.name, "M");
        case 'F' : return new Female(aRecord.name);
        default : return new Person(aRecord.name);
    }
}
function loadFromInput(data){
    return data.map(aRecord=>createPerson(aRecord));
}
class Person{
    //생성자에 젠더코드 추가
    constructor(name, genderCode){
        this._name = name;
        this._genderCode = genderCode || "X";
    }
    get isMale(){return "M"=== this._genderCode;}
    get name(){return this._name;}
    get genderCode(){return this._genderCode;}
}
class Female 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");
    }
}
function loadFromInput(data){
    return data.map(aRecord=>createPerson(aRecord));
}
class Person{
    constructor(name, genderCode){
        this._name = name;
        this._genderCode = genderCode ;
    }
    get isMale(){return "M"=== this._genderCode;}
    get name(){return this._name;}
    get genderCode(){return this._genderCode;}
}
//클라이언트
const numberOfMales = people.filter(p=>p.isMale).length;

 

배경

비슷한 대상들을 특정 특성에 따라 구분해야 , 구분 기준으로 타입 코드를 사용하는 경우가 있다.

언어에서 열거형을 지원하면 보통 열거형으로 한다.

 

타입 코드 대신 서브클래스로 바꾸면 가지 장점이 있다.

첫째, 조건에 따라 다르게 동작하도록 해주는 다형성을 제공한다.

둘째, 특정 타입에서만 의미가 있는 값을 사용하는 필드나 메서드가 있을 때다.

 

리팩터링은 대상 클래스에 직접 적용할지, 아니면 타입 코드 자체에 적용할지 고민해야 한다.

 

전자는 서브클래스를 만들면 된다.

후자는 직원에게 직원 유형 속성을 부여하고 속성을 클래스로 정의하고 서브클래싱한다.

 

예시: 직접 상속할



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}라는 직운 유형은 없습니다.`);
    }
    toString(){return `${this._name} (${this._type})`;}
}


class Employee{
    constructor(name, type){
        this.validateType(type);
        this._name = name;
        this._type = type;
    }
    //타입 코드 캡슐화
    get tpye(){return this._type;}
    validateType(arg){
        if(!["engineer","manager","salesperson"].includes(arg))
            throw new Error(`${arg}라는 직운 유형은 없습니다.`);
    }
    toString(){return `${this._name} (${this.type})`;}//_ 없음
}


//서브 클래스 오버라이딩
class Engineer extends Employee{
    get type(){return "engineer";}
}


//생성자를 팩터리 함수로 대체
function createEmployee(name, type){
    return new Employee(name, type);
}


//생성자를 팩터리 함수로 대체
function createEmployee(name, type){
    switch(tpye){
        case "engineer" : return new Engineer(name,type);
    }
    return new Employee(name, type);
}


function createEmployee(name, type){
    switch(tpye){
        case "engineer" : return new Engineer(name,type);
        case "salesperson" : return new Salesperson(name,type);
        case "manager" : return new Manager(name,type);
    }
    return new Employee(name, type);
}
class Employee{
    constructor(name, type){
        this.validateType(type);
        this._name = name;
        this._type = type;
    }
    get tpye(){return this._type;}
    validateType(arg){
        if(!["engineer","manager","salesperson"].includes(arg))
            throw new Error(`${arg}라는 직운 유형은 없습니다.`);
    }
    toString(){return `${this._name} (${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";}
}


class Employee{
    //모든 서브클래스에서 재정의한 필드와 게터제거
    constructor(name, type){
        this.validateType(type);
        this._name = name;
    }
    validateType(arg){
        if(!["engineer","manager","salesperson"].includes(arg))
            throw new Error(`${arg}라는 직운 유형은 없습니다.`);
    }
    toString(){return `${this._name} (${this.type})`;}
}


function createEmployee(name, type){
    switch(tpye){
        case "engineer" : return new Engineer(name,type);
        case "salesperson" : return new Salesperson(name,type);
        case "manager" : return new Manager(name,type);
        default : throw new Error(`${arg}라는 직운 유형은 없습니다.`);
    }
}
class Employee{
    //팩터리 메서드에서 검증은 대체가능 하니 제거
    constructor(name, type){
        this._name = name;
    }
    toString(){return `${this._name} (${this.type})`;}
}


//필요없어진 타입 코드 인자 제거
function createEmployee(name){
    switch(tpye){
        case "engineer" : return new Engineer(name);
        case "salesperson" : return new Salesperson(name);
        case "manager" : return new Manager(name);
        default : throw new Error(`${arg}라는 직운 유형은 없습니다.`);
    }
}
class Employee{
    constructor(name){
        this._name = name;
    }
    //서브 클래스에는 아직 type 코드 게터가 있다.
    toString(){return `${this._name} (${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";}
}

 

 

 

예시 : 간접 상속할

이미 서브 클래스로 아르바이트와 정직원이라는 클래스가 존재해 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.substr(1).toLowerCase();
    }
    toString(){
        return `${this._name} (${this.capitalizedType})`;
    }
}


//타입 코드를 클래스로 만들기(기본형을 객체로 만듦)
class EmployeeType{
    constructor(aString){
        this._value = aString;
    }
    toString(){return this._value;}
}
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 typeString() {return this._type.toString();}
    get type(){return this._type;}
    set type(arg){this._type = arg;}
    get capitalizedType(){
        return this.typeString.charAt(0).toUpperCase()
        + this.typeString.substr(1).toLowerCase();
    }
    toString(){
        return `${this._name} (${this.capitalizedType})`;
    }
}


class EmployeeType{
}
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;
        this._type =  this.createEmployeeType(type);
    }
    static createEmployeeType(aString){
        switch(aString){
            case "engineer" : return new Engineer();
            case "salesperson" : return new Salesperson();
            case "manager" : return new Manager();
            default : throw new Error(`${aString}라는 직원 유형은 없습니다.`);
        }
    }
    get typeString() {return this._type.toString();}
    get type(){return this._type;}
    set type(arg){this._type = Employee.createEmployeeType(arg);}
    get capitalizedType(){
        return this.typeString.charAt(0).toUpperCase()
        + this.typeString.substr(1).toLowerCase();
    }
    toString(){
        return `${this._name} (${this.capitalizedType})`;
    }
}


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;
        this._type = this.createEmployeeType(type);
    }
    static createEmployeeType(aString){
        switch(aString){
            case "engineer" : return new Engineer();
            case "salesperson" : return new Salesperson();
            case "manager" : return new Manager();
            default : throw new Error(`${aString}라는 직원 유형은 없습니다.`);
        }
    }
    get typeString() {return this._type.toString();}
    get type(){return this._type;}
    set type(arg){this._type = Employee.createEmployeeType(arg);}


    toString(){
        return `${this._name} (${this.type.capitalizedName})`;
    }
}

상태 패턴과 비슷하다.

개요

서브클래스에서만 사용하는 필드를 내린다.

예시


class Employee{
    protected String name;
}
class Salesperson extends Employee{
}
class Engineer extends Employee{
}


class Employee{
}
class Salesperson extends Employee{
}
class Engineer extends Employee{
    private String name;
}

 

개요

특정 서브클래스에만 관련된 메서드는 슈퍼 클래스에서 제거하고 해당 서브클래스로 옮긴다.

리팩터링은 해당 기능을 제공하는 서브클래스에 대해 정확히 파악한 경우에 적용할 있다. 그렇지 않다면 조건부 로직을 다형성으로 바꿔야 한다.

 


class Employee{
    get quota{}
}
class Engineer extends Employee{}
class Salesperson extends Employee{}
class Employee{
}
class Engineer extends Employee{}
class Salesperson extends Employee{
    get quota{}
}

개요

생성자는 특수한 메서드이기 때문에 리팩터링도 다른 식으로 접근해야 한다.

리팩터링이 간단히 끝날 같지 않다면 생성자를 팩터리 함수로 바꾸기를 고려한다.

 

예시



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


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

 

 

예시 : 공통 코드가 나중에



class Employee{
    constructor(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;
    }
}


class Manager extends Employee{
    constructor(name, grade){
        super(name);
        this._grade = grade;
        this.finishConstruction();
    }
    //먼저 공통 코드를 함수로 추출
    finishConstruction() {
        if (this.isPrivileged)
            this.assignCar();
    }
    get isPrivileged(){
        return this._grade >4;
    }
}


class Employee{
    constructor(name){
    }
    get isPrivileged(){}
    assignCar(){}
    //슈퍼클래스로 옮김
    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;
    }
}

 

개요

서브 클래스들이 독립적으로 개발되다가 하나의 계층구조로 리팩터링된 경우라면 일부 기능이 중복될 있다.

 

예시



public class Refactoring1 {
    static class Employee{
       
    }
    static class Salesperson extends Employee{
        private String name;
    }
    static class Engineer extends Employee{
        private String name;
    }
}


public class Refactoring1 {
    static class Employee{
        protected String name;
    }
    static class Salesperson extends Employee{
    }
    static class Engineer extends Employee{
    }
}

+ Recent posts