Replace Conditional With Polymorphism, 조건부 로직을 다형성으로 바꾸기를 알아보자.

요약

코드

switch (bird.type) {
    case '유럽 제비':
        return "보통이다";
    case '아프리카 제비':
        return (bird.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
    case '노르웨이 파랑 앵무':
        return (bird.voltage > 100) ? "그을렸다" : "예쁘다";
    default:
        return "알 수 없다";
}
class EuropeanSwallow {
    get plumage() {
        return "보통이다";
    }
}
 
class AfricanSwallow {
    get plumage() {
        return (this.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
    }
}
 
class NorwegianBlueSwallow {
    get plumage() {
        return (this.voltage > 100) ? "그을렸다" : "예쁘다";
    }
}

배경

  • 조건부 로직은 프로그램을 읽기 어렵게 만든다.
  • 어떻게 하면 직관적으로 구조화할 수 있을까?
  • 더 높은 수준의 개념을 도입하면 이를 분리시킬 수 있다.
  • 클래스와 다형성이다.
  • 각 타입이 자신만의 방식으로 처리하도록 구현하면 좋겠다.

절차

  1. 다형적 동작을 표현하는 클래스들이 없다면 하나 만든다. 기왕이면 인스턴스를 알아서 만들어 반환하는 팩토리 함수도 함께 만든다. (사용하는쪽에서 이게 더 편하다.)
  2. 호출하는 쪽에서 팩토리 함수를 사용하도록 수정한다.
  3. 조건부 로직을 일단 슈퍼 클래스로 옮긴다.
  4. 서브 클래스 중 하나를 선택한다. 조건부 로직 메서드를 오버라이드 한다.
  5. 같은 방식으로 다른 서브 클래스에도 적용한다.
  6. 슈퍼 클래스에는 기본 동작만 남긴다.

예시

function plumages(birds) {
    return new Map(birds.map(b => [b.name, plumage(b)]));
}
 
function speeds(birds) {
    return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}
 
function plumage(bird) {
    switch (bird.type) {
        case '유럽 제비':
            return "보통이다";
        case '아프리카 제비':
            return (bird.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
        case '노르웨이 파랑 앵무':
            return (bird.voltage > 100) ? "그을렸다" : "예쁘다";
        default:
            return "알 수 없다";
    }
}
 
function airSpeedVelocity(bird) {
    switch (bird.type) {
        case '유럽 제비':
            return 35;
        case '아프리카 제비':
            return 40 - 2 * bird.numberOfCoconuts;
        case '노르웨이 파랑 앵무':
            return (bird.isNailed) ? 0 : 10 + bird.voltage / 10;
        default:
            return null;
    }
}
  • 새의 종에 따른 비행 속도와 깃털 상태를 반환하는 함수이다.
  • 새 종류에 따라 다르게 동작하는 함수가 몇 개 보인다.
function plumages(birds) {
    return new Map(birds.map(b => [b.name, plumage(b)]));
}
 
function speeds(birds) {
    return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}
 
function plumage(bird) {
    new Bird(bird).plumage;
}
 
function airSpeedVelocity(bird) {
    new Bird(bird).airSpeedVelocity;
}
 
class Bird {
    constructor(birdObject) {
        Object.assign(this, birdObject);
    }
 
    get plumage() {
        switch (this.type) {
            case '유럽 제비':
                return "보통이다";
            case '아프리카 제비':
                return (this.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
            case '노르웨이 파랑 앵무':
                return (this.voltage > 100) ? "그을렸다" : "예쁘다";
            default:
                return "알 수 없다";
        }
    }
 
    get airSpeedVelocity() {
        switch (this.type) {
            case '유럽 제비':
                return 35;
            case '아프리카 제비':
                return 40 - 2 * this.numberOfCoconuts;
            case '노르웨이 파랑 앵무':
                return (this.isNailed) ? 0 : 10 + this.voltage / 10;
            default:
                return null;
        }
    }
}
  • 일단 슈퍼 클래스를 만들었고, 그 안에 서브클래스를 만들자.
  • 아 팩토리 함수도 추가해주자.
function plumages(birds) {
    return new Map(birds.map(b => [b.name, plumage(b)]));
}
 
function speeds(birds) {
    return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}
 
function plumage(bird) {
    createBird(bird).plumage;
}
 
function airSpeedVelocity(bird) {
    createBird(bird).airSpeedVelocity;
}
 
function createBird(bird) {
    switch (bird.type) {
        case '유럽 제비':
            return new EuropeanSwallow(bird);
        case '아프리카 제비':
            return new AfricanSwallow(bird);
        case '노르웨이 파랑 앵무':
            return new NorwegianBlueSwallow(bird);
        default:
            return new Bird(bird);
    }
}
 
class Bird {
    constructor(birdObject) {
        Object.assign(this, birdObject);
    }
 
    get plumage() {
        switch (this.type) {
            case '유럽 제비':
                return "보통이다";
            case '아프리카 제비':
                return (this.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
            case '노르웨이 파랑 앵무':
                return (this.voltage > 100) ? "그을렸다" : "예쁘다";
            default:
                return "알 수 없다";
        }
    }
 
    get airSpeedVelocity() {
        switch (this.type) {
            case '유럽 제비':
                return 35;
            case '아프리카 제비':
                return 40 - 2 * this.numberOfCoconuts;
            case '노르웨이 파랑 앵무':
                return (this.isNailed) ? 0 : 10 + this.voltage / 10;
            default:
                return null;
        }
    }
}
 
class EuropeanSwallow extends Bird {
}
 
class AfricanSwallow extends Bird {
}
 
class NorwegianBlueSwallow extends Bird {
}
  • 팩토리 함수를 하나 만들었고, 이걸 호출하도록 변경했다.
  • 그리고 서브 클래스들을 일단 만들어봤다.
  • 로직이 분리되지는 않았으니 이걸 처리해보자.
function plumages(birds) {
    return new Map(birds.map(b => [b.name, plumage(b)]));
}
 
function speeds(birds) {
    return new Map(birds.map(b => [b.name, airSpeedVelocity(b)]));
}
 
function plumage(bird) {
    createBird(bird).plumage;
}
 
function airSpeedVelocity(bird) {
    createBird(bird).airSpeedVelocity;
}
 
function createBird(bird) {
    switch (bird.type) {
        case '유럽 제비':
            return new EuropeanSwallow(bird);
        case '아프리카 제비':
            return new AfricanSwallow(bird);
        case '노르웨이 파랑 앵무':
            return new NorwegianBlueSwallow(bird);
        default:
            return new Bird(bird);
    }
}
 
class Bird {
    constructor(birdObject) {
        Object.assign(this, birdObject);
    }
 
    get plumage() {
        return "알 수 없다";
    }
 
    get airSpeedVelocity() {
        return null;
    }
}
 
class EuropeanSwallow extends Bird {
    get plumage() {
        return "보통이다";
    }
}
 
class AfricanSwallow extends Bird {
    get plumage() {
        return (this.numberOfCoconuts > 2) ? "지쳤다" : "보통이다";
    }
}
 
class NorwegianBlueSwallow extends Bird {
    get plumage() {
        return (this.voltage > 100) ? "그을렸다" : "예쁘다";
    }
}

Reference