개요

플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수



function bookConcert(aCustomer, isPreminum){
    if(isPreminum){
        // 프리미엄 예약용 로직
    }else{
        // 일반 예약용 로직
    }
}

 

//호출하는 쪽 예시

bookConcert(aCustomer, true); // 불리언

bookConcert(aCustomer, Customer.PREMINUM); //열거형

bookConcert(aCustomer,"premium"); //리터럴

플래그 인수는 이렇게 호출할 있는 함수들이 무엇이고 어떻게 호출해야 하는지 이해하기 어려워 진다.

 

API 익힐 함수에 플래그 인수가 있으면 함수들의 기능 차이가 드러나지 않는다.

또한 함수 인자로 무엇을 넘겨야하는지 추가 학습이 필요하다.

대상 함수에게 있어서 true, false 의미가 무엇인지 명확하지 않다.

 

플래그 인수는 정확히 말하면 변수 형태가 아닌 리터럴 형태로 값을 건네는 것을 말한다.

 

리터럴이란 자체를 의미한다.

true , "스트링" , 123 등등…

 

 

예시

배송일자를 계산하는 호출



//도대체 이 불리언 플래그가 무슨 의미인지 알 수 있나?
//호출하는 곳 1
aShipment.deliveryDate = deliveryDate(anOrder, true);
//다른 어딘가 호출하는 곳2
aShipment.deliveryDate = deliveryDate(anOrder, false);


function deliveryDate(anOrder, isRush){
    if(isRush){
        let deliveryTime;
        if(["MA","CT"].includes(anOrder.deliveryState)) deliveryTime = 1;
        else if(["NY", "NH"].includes(anOrder.deliveryState)) deliveryTime = 2;
        else deliveryTime = 3;
        return anOrder.placedOn.plusDays(1 + deliveryTime);
    }else{
        let deliveryTime;
        if(["MA","CT","NY"].includes(anOrder.deliveryState)) deliveryTime = 2;
        else if(["ME","NH"].includes(anOrder.deliveryState)) deliveryTime = 3;
        else deliveryTime = 4;
        return anOrder.placedOn.plusDays(2 + deliveryTime);
    }
}




//함수 추출하기로 조건문 분해
function deliveryDate(anOrder, isRush){
    if(isRush) return rushDeliveryDate(anOrder);
    else return regularDeliveryDate(anOrder);
}
function regularDeliveryDate(anOrder) {
    let deliveryTime;
    if (["MA", "CT", "NY"].includes(anOrder.deliveryState))
        deliveryTime = 2;
    else if (["ME", "NH"].includes(anOrder.deliveryState))
        deliveryTime = 3;
    else
        deliveryTime = 4;
    return anOrder.placedOn.plusDays(2 + deliveryTime);
}
function rushDeliveryDate(anOrder) {
    let deliveryTime;
    if (["MA", "CT"].includes(anOrder.deliveryState))
        deliveryTime = 1;
    else if (["NY", "NH"].includes(anOrder.deliveryState))
        deliveryTime = 2;
    else
        deliveryTime = 3;
    return anOrder.placedOn.plusDays(1 + deliveryTime);
}


//목적을 더 잘 드러낸다.
aShipment.deliveryDate = rushDeliveryDate(anOrder);
aShipment.deliveryDate = regularDeliveryDate(anOrder);


function regularDeliveryDate(anOrder) {
    let deliveryTime;
    if (["MA", "CT", "NY"].includes(anOrder.deliveryState))
        deliveryTime = 2;
    else if (["ME", "NH"].includes(anOrder.deliveryState))
        deliveryTime = 3;
    else
        deliveryTime = 4;
    return anOrder.placedOn.plusDays(2 + deliveryTime);
}
function rushDeliveryDate(anOrder) {
    let deliveryTime;
    if (["MA", "CT"].includes(anOrder.deliveryState))
        deliveryTime = 1;
    else if (["NY", "NH"].includes(anOrder.deliveryState))
        deliveryTime = 2;
    else
        deliveryTime = 3;
    return anOrder.placedOn.plusDays(1 + deliveryTime);
}

 

저자가 플래그 인수를 싫어하는 이유

변수가 같은 데이터가 아니라 리터럴로 설정하기 때문이다.

 

const isRush = determineIfRush(anOrder);

aShipment.deliveryDate = deliveryDate(anOrder, isRush);

 

예시:매개변수를 까다로운 방식으로 사용할



function deliveryDate(anOrder, isRush){
    let result;
    let deliveryTime;
    if(anOrder.deliveryState === "MA" || anOrder.deliveryState === "CT")
        deliveryTime = isRush ? 1 : 2;
    else if (anOrder.deliveryState === "NY" || anOrder.deliveryState === "NH"){
        deliveryTime = 2;
        if(anOrder.deliveryState === "NH" && !isRush)
            deliveryTime = 3;
    }
    else if (isRush)
        deliveryTime = 3;
    else if (anOrder.deliveryState === "ME")
        deliveryTime = 3;
    else
        deliveryTime = 4;
    result = anOrder.placeOn.plusDays(2 + deliveryTime);
    if(isRush) result = result.minusDays(1);
    return result;
}


//지나치게 까다로워 고치려면 일이 커질 것 같으면 래핑하는 함수도 고려할만 하다.
function rushDeliveryDate(anOrder) {return deliveryDate(anOrder, true);}
function regularDeliveryDate(anOrder) {return deliveryDate(anOrder, false);}
function deliveryDate(anOrder, isRush){
    let result;
    let deliveryTime;
    if(anOrder.deliveryState === "MA" || anOrder.deliveryState === "CT")
        deliveryTime = isRush ? 1 : 2;
    else if (anOrder.deliveryState === "NY" || anOrder.deliveryState === "NH"){
        deliveryTime = 2;
        if(anOrder.deliveryState === "NH" && !isRush)
            deliveryTime = 3;
    }
    else if (isRush)
        deliveryTime = 3;
    else if (anOrder.deliveryState === "ME")
        deliveryTime = 3;
    else
        deliveryTime = 4;
    result = anOrder.placeOn.plusDays(2 + deliveryTime);
    if(isRush) result = result.minusDays(1);
    return result;
}
//원래플래그 함수를 호출하지 말라는 의미에서 함수 이름 변경
function rushDeliveryDate(anOrder) {return deliveryDateHelperOnly(anOrder, true);}
function regularDeliveryDate(anOrder) {return deliveryDateHelperOnly(anOrder, false);}
function deliveryDateHelperOnly(anOrder, isRush){
    let result;
    let deliveryTime;
   
    if(["MA","CT"].includes(anOrder.deliveryState))
        deliveryTime = isRush ? 1 : 2;
    else if ( ["NY","NH"].includes(anOrder.deliveryState)){
        deliveryTime = 2;
        if(anOrder.deliveryState === "NH" && !isRush)
            deliveryTime = 3;
    }
    else if (isRush || anOrder.deliveryState === "ME")
        deliveryTime = 3;
    else
        deliveryTime = 4;
    result = anOrder.placeOn.plusDays(2 + deliveryTime);
    if(isRush) result = result.minusDays(1);
    return result;
}

 

개요

함수 로직이 거의 같고, 리터럴 값만 다르다면, 다른 부분만 매개변수로 받아 중복을 제거한다.

 

간단한 예시



//가장 간단한 예시
function tenPercentRaise(aPerson){
    aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson){
    aPerson.salary = aPerson.salary.multiply(1.05);
}
//함수 매개변수화
function rise(aPerson, factor){
    aPerson.salary = aPerson.salary.multiply(1+factor);
}

 

간단하지 않은 예제



function baseCharge(usage){
    if(usage < 0) return usd(0);
    const amount = bottomBand(usage) * 0.03
        + middleBand(usage) * 0.05
        + topBand(usage) * 0.07;
    return usd(amount);
}
function bottomBand(usage){
    return Math.min(usage, 100);
}
function middleBand(usage){
    return usage > 100 ? Math.min(usage,200) - 100 : 0;
}
function topBand(usage){
    return usage > 200 ? usage - 200 : 0;
}


function baseCharge(usage){
    if(usage < 0) return usd(0);
    const amount = bottomBand(usage) * 0.03
        + withinBand(usage, 100, 200) * 0.05
        + topBand(usage) * 0.07;
    return usd(amount);
}
function bottomBand(usage){
    return Math.min(usage, 100);
}
//통합할 함수를 선정할 때 보통 중간 함수가 적절
//매개변수화 하여 넣기
function withinBand(usage,bottom,top){
    return usage > bottom ? Math.min(usage,top) - bottom : 0;
}
function topBand(usage){
    return usage > 200 ? usage - 200 : 0;
}


function baseCharge(usage){
    if(usage < 0) return usd(0);
    const amount = withinBand(usage,0,100) * 0.03
        + withinBand(usage, 100, 200) * 0.05
        + topBand(usage) * 0.07;
    return usd(amount);
}
function topBand(usage){
    return usage > 200 ? usage - 200 : 0;
}


function baseCharge(usage){
    if(usage < 0) return usd(0);
    const amount = withinBand(usage,0,100) * 0.03
        + withinBand(usage, 100, 200) * 0.05
        + withinBand(usage, 200, Infinity) * 0.07;
    return usd(amount);
}

 

개요

겉보기 부수효과가 전혀 없는 함수를 추구해야 한다. 이런 함수는 옮기기도 쉽고, 아무 시점에 호출해도 항상 같은 값을 리턴한다.

 

부수효과가 있는 함수와 없는 함수는 구분해야 한다.

 

질의함수는 모두 부수효과가 없어야 한다. 규칙을 명령/질의 분리라 한다.

 

겉보기 부수효과란, 부수효과가 전혀 없는 함수와 똑같은 결과를 리턴하지만, 실제론 상태를 변경하는 것을 의미한다. 예를들어 캐싱 기능을 지원하면, 분명 호출로 상태가 변경된다. 하지만 같은 결과를 리턴하는 사실은 똑같다.

 

예시

악당을 찾는 함수



function alertForMiscreant(people){
    for(const p of people){
        if(p==="조커"){
            setOffAlarms();
            return "조커";
        }
        if(p==="사루만"){
            setOffAlarms();
            return "사루만";
        }
    }
    return "";
}


//함수 복제 후 질의 목적에 맞는 이름 짓기
function findMiscreant(people){
    for(const p of people){
        if(p==="조커"){
            setOffAlarms();
            return "조커";
        }
        if(p==="사루만"){
            setOffAlarms();
            return "사루만";
        }
    }
    return "";
}


//부수효과 제거
function findMiscreant(people){
    for(const p of people){
        if(p==="조커") return "조커";
        if(p==="사루만") return "사루만";
    }
    return "";
}


//원래 함수 호출하던 곳, 새로 만든 질의함수로 바꾸기
// const found = alertForMiscreant(people);
const found = findMiscreant(people); //질의
alertForMiscreant(people);//명령
완료


function alertForMiscreant(people){
    for(const p of people){
        if(p==="조커"){
            setOffAlarms();
            return ;
        }
        if(p==="사루만"){
            setOffAlarms();
            return ;
        }
    }
    return "";
}
function findMiscreant(people){
    for(const p of people){
        if(p==="조커") return "조커";
        if(p==="사루만") return "사루만";
    }
    return "";
}
//원래 함수 호출하던 곳, 새로 만든 질의함수로 바꾸기
// const found = alertForMiscreant(people);
const found = findMiscreant(people); //질의
alertForMiscreant(people);//명령
function alertForMiscreant(people){
    if(findMiscreant(people) !=="") setOffAlarms();
}
function findMiscreant(people){
    for(const p of people){
        if(p==="조커") return "조커";
        if(p==="사루만") return "사루만";
    }
    return "";
}
//원래 함수 호출하던 곳, 새로 만든 질의함수로 바꾸기
// const found = alertForMiscreant(people);
const found = findMiscreant(people); //질의
alertForMiscreant(people);//명령

 

 

개요

제어 플래그란 코드 동작을 변경하는 사용되는 변수

어딘가에서 값을 계산해 다른 어딘가 조건문에 쓰인다.

, 계산과 평가가 곳에서 이루어 지지지 않는다.

 

제어 플래그는 반목문(혹은 재귀) 주로 존재한다.

 

예시



function eaxmple(){
    let found = false;
    for(const p of people){
        if( ! found){
            if(p === "조커"){
                sendAlert();
                found = true;
            }
            if(p === "사루만"){
                sendAlert();
                found = true;
            }
        }
    }
}


function eaxmple(){
    checkForMiscreants(people);
}
//함수로 추출
function checkForMiscreants(people) {
    let found = false;
    for (const p of people) {
        if (!found) {
            if (p === "조커") {
                sendAlert();
                found = true;
            }
            if (p === "사루만") {
                sendAlert();
                found = true;
            }
        }
    }
}


function eaxmple(){
    checkForMiscreants(people);
}
//함수로 추출
function checkForMiscreants(people) {
    let found = false;
    for (const p of people) {
        if (!found) {
            if (p === "조커") {
                sendAlert();
                found = true;
            }
            if (p === "사루만") {
                sendAlert();
                found = true;
            }
        }
    }
}


function checkForMiscreants(people) {
    let found = false;
    for (const p of people) {
        if (!found) {
            if (p === "조커") {
                sendAlert();
                // found = true;
                return;
            }
            if (p === "사루만") {
                sendAlert();
                // found = true;
                return;
            }
        }
    }
}


function checkForMiscreants(people) {
    for (const p of people) {
        if (p === "조커") {
            sendAlert();
            return;
        }
        if (p === "사루만") {
            sendAlert();
            return;
        }
    }
}


//더 가다듬기
function checkForMiscreants(people) {
    if(people.some(p=>["조커","사루만"].includes(p))) sendAlert();
}

 

 

개요

특정 조건이 참일 때만 제대로 동작하는 코드

이런 코드는 조건이 코드에 항상 명시적으로 기술되어 있지 않다.

알고리즘을 보고 알아내야 한다. 주석도 없는 경우도 있다.

좋은 방법은 assertion 코드 추가다.

 

어셔션은 항상 참이라 가정하는 조건부 문장이다.

이셔션은 프로그램에 영향을 주지 않도록 작성돼야 한다.

 

예시

할인 관련 코드



class Customer {
    applyDiscount(aNumber){
        return (this.discountRate)
            ? aNumber - (this.discountRate * aNumber)
            : aNumber;
    }
}
//간단한 구조 개선
class Customer {
    applyDiscount(aNumber){
        if(!this.discountRate) return aNumber;
        else return aNumber - (this.discountRate * aNumber);
    }
}
//어서션 추가
class Customer {
    applyDiscount(aNumber){
        if(!this.discountRate) return aNumber;
        else {
            assert(this.discountRate>=0);
            return aNumber - (this.discountRate * aNumber);
        }
    }
}


class Customer {
    applyDiscount(aNumber){
        if(!this.discountRate) return aNumber;
        else return aNumber - (this.discountRate * aNumber);
    }
    //문제 범위를 좁히기 위해 세터에 옮겼다.
    //applyDiscount에서 통과 못했을 때 추가로 추적을 해야 하기 때문이다.
    set discountRate(aNumber){
        assert(null === aNumber || aNumber >= 0);
        this._discountRate = aNumber;
    }
}

위와 같은 어서션은 오류의 출처를 특정하기에 좋다.

 

어서션은 참이라고 '생각되는' 가정에 붙이는 아니다.

'항상' 참인 것에 붙인다.

 

 

개요

데이터 구조의 특정 값을 확인 똑같은 동작을 수행하는 코드가 중복이 되는 경우는 흔하다.

같은 중복을 곳으로 모으는 것이 좋다

 

특이 케이스 패턴

특수한 경우의 공통 동작을 요소 하나에 모아서 사용하는

 

특이 케이스 패턴을 활용하면 특이 케이스를 확인하는 코드 대부분을 단순한 함수 호출로 바꿀 있다.

 

특이 케이스 객체에서 단순히 데이터만 읽는 다면 리터럴 객체로 만든다.

어떤 동작을 수행해야한다면 일반 객체로 만든다.

 

특이 케이스 객체 가장 유명한게 객체 패턴이다.

예시



//전력 회사 가정, 현장에 인프라를 설치해 서비스를 제공할 것
class Site{
    get customer(){return this._customer;}
}
class Customer{
    get name(){return this._name;}//고객이름
    get buillingPlan(){return this._buillingPlan;}//요금제
    set buillingPlan(arg){this._buillingPlan= arg;}
    get paymentHistory(){return this._paymentHistory;}납부이력
}
//클라리언트1이라고 가정
function client1(){
    const aCustomer = site.customer;
    /**코드 들..*/
    let customerName;
    if(aCustomer === "미확인 고객") customerName = "거주자";
    else customerName =aCustomer.name;
}
//클라이언트2
function client2(){
    const plan = (aCustomer === "미확인 고객")
        ? registry.buillingPlans.basic
        : aCustomer.buillingPlan;
    /**코드 들..*/
}
//클라이언트3
function client3(){
    if(aCustomer !== "미확인 고객") aCustomer.buillingPlan = newPlan;
    /**코드 들..*/
}
//클라이언트4
function client4(){
    const weeksDelinquent = (aCustomer === "미확인 고객")
        ? 0
        : aCustomer.paymentHistory.weeksDelinquentInLastYear;
}
현장에는 고객이 거주하기도 하지만 그렇지 않다
최근 누군가 이사왔거나, 이사 갔을 수도 있다.(특이 케이스)
이런 식별이 안되는 고객을 "미확인 고객" 문자열로 채운다.


class Customer{
    get name(){return this._name;}//고객이름
    get buillingPlan(){return this._buillingPlan;}//요금제
    set buillingPlan(arg){this._buillingPlan= arg;}
    get paymentHistory(){return this._paymentHistory;}//납부이력
    //미확인 고객구분 메서드
    get isUnknown(){return false;}
}
//미확인 고객 전용 클래스, 서브클래스 아님!!!!!
class UnknownCustomer{
    get isUnknown(){return true;}
}
자바스크립트는 동적 언어로 굳이 서브클래스로 만들지 않았다.
//한번에 클래스 사용하도록 하기 부담됨, 임시로 수정할 코드들을 함수로 모아둠
function isUnknown(arg){
    if(!((arg instanceof Customer) || (arg === "미확인 고객")))
        throw new Error(`잘못된 값과 비교 : <${arg}>`);
    return (arg === "미확인 고객");
}
"미확인 고객" 고객을 리턴하는 모든 곳에 전용 클래스를 리턴하게 하고 isUnknown()메서드를 사용하도록 고쳐야 한다.
이를 적용하려면 번에 일괄적으로 변경해야하는 위험부담이 크다.
그래서 일단 수정해야하는 코드를 별도 함수로 추출해 모아놨다.


//클라리언트1이라고 가정
function client1(){
    const aCustomer = site.customer;
    /**코드 들..*/
    let customerName;
    if(isUnknown(aCustomer)) customerName = "거주자";
    else customerName =aCustomer.name;
}
//클라이언트2
function client2(){
    const plan = (isUnknown(aCustomer))
        ? registry.buillingPlans.basic
        : aCustomer.buillingPlan;
    /**코드 들..*/
}
//클라이언트3
function client3(){
    if(!isUnknown(aCustomer)) aCustomer.buillingPlan = newPlan;
    /**코드 들..*/
}
//클라이언트4
function client4(){
    const weeksDelinquent = (isUnknown(aCustomer))
        ? 0
        : aCustomer.paymentHistory.weeksDelinquentInLastYear;
}
//Site에서 특이 케이스일 때 전용 객채 반환하도록 수정
class Site{
    get customer(){return this._customer === "미확인 고객"
        ? new UnknownCustomer()
        : this._customer;}
}
//"미확인 고객" 문자열 확인하는 코드가 사라짐
function isUnknown(arg){
    if (!(arg instanceof Customer || arg instanceof UnknownCustomer))
        throw new Error(`잘못된 값과 비교 <${arg}>`);
    return arg.isUnknown;
}


//미확인 고객 전용 클래스
class UnknownCustomer{
    get isUnknown(){return true;}
    //특이 케이스 검사를 일반적인 기본값으로 대체할 수 있다.
    get name(){return "거주자";}
}




function client1(){
    /*
    const aCustomer = site.customer;
    let customerName;
    if(isUnknown(aCustomer)) customerName = "거주자";
    */
    const aCustomer = aCustomer.name;
}



class UnknownCustomer{
    get isUnknown(){return true;}
    get name(){return "거주자";}
    get buillingPlan(){return registry.buillingPlans.basic;}
    set buillingPlan(arg){/*무시*/}
}




function client2(){//읽는 경우
    const plan = aCustomer.buillingPlan;
}
function client3(){//쓰는 경우
    aCustomer.buillingPlan = newPlan;
}



class Customer{
    get name(){return this._name;}
    get buillingPlan(){return this._buillingPlan;}
    set buillingPlan(arg){this._buillingPlan= arg;}
    get paymentHistory(){return this._paymentHistory;}
    get isUnknown(){return false;}
}


class UnknownCustomer{
    get isUnknown(){return true;}
    get name(){return "거주자";}
    get buillingPlan(){return registry.buillingPlans.basic;}
    set buillingPlan(arg){/*무시*/}
    get paymentHistory(){return new NullPaymentHistory();}
}
class NullPaymentHistory{
    get weeksDelinquentInLastYear(){return 0;}
}




function client4(){
    const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
}

완료


class Site{
    get customer(){return this._customer === "미확인 고객"
        ? new UnknownCustomer()
        : this._customer;}
}
class Customer{
    get name(){return this._name;}
    get buillingPlan(){return this._buillingPlan;}
    set buillingPlan(arg){this._buillingPlan= arg;}
    get paymentHistory(){return this._paymentHistory;}
    get isUnknown(){return false;}
}
class UnknownCustomer{
    get isUnknown(){return true;}
    get name(){return "거주자";}
    get buillingPlan(){return registry.buillingPlans.basic;}
    set buillingPlan(arg){/*무시*/}
    get paymentHistory(){return new NullPaymentHistory();}
}
class NullPaymentHistory{
    get weeksDelinquentInLastYear(){return 0;}
}
function isUnknown(arg){
    if (!(arg instanceof Customer || arg instanceof UnknownCustomer))
        throw new Error(`잘못된 값과 비교 <${arg}>`);
    return arg.isUnknown;
}
function client1(){
    const aCustomer = aCustomer.name;
}
function client2(){
    const plan = aCustomer.buillingPlan;
}
function client3(){
    aCustomer.buillingPlan = newPlan;
}
function client4(){
    const weeksDelinquent = aCustomer.paymentHistory.weeksDelinquentInLastYear;
}

 

"미확인 고객" 아닌 다른 값을 원하는 튀는 클라이언트가 있을 있다.

예를 들어 똑같은 상황이지만, "미확인 거주자" 리턴받고 싶은 클라이언트도 있을 있다. 이때는 그대로 유지한다.

 

 

예시: 객체 리터럴 이용하기

단순한 값을 위해서 클래스까지 만드는 것은 과한 감이 있다.

정보가 갱신될 있다면 클래스가 필요하다. 반대로 데이터 구조를 읽기만 한다면 클래스 대신 리터럴 객체를 사용해도 된다.

 

//고객 정보를 갱신하는 클라이언트가 없음
class Site{
    get customer(){return this._customer;}
}
class Customer{
    get name(){}
    get billingPlan(){}
    set billingPlan(arg){}
    get paymentHistory(){}
}
function client1(){
    const aCustomer = site.customer;
    /*코드 들*/
    let customerName;
    if(aCustomer === "미확인 고객") customerName = "거주자";
    else customerName = aCustomer.name;
}
function client2(){
    const plan = (aCustomer === "미확인 고객")
        ? registry.billingPlans.basic
        : aCustomer.billingPlan;
}
function client3(){
    const weeksDelinquent = (aCustomer === "미확인 고객")
        ? 0
        : aCustomer.paymentHistory.weeksDelinquentInLastYear;
}
// isUnknown 게터 정의 및 리터럴 객체 생성
class Customer{
    get name(){}
    get billingPlan(){}
    set billingPlan(arg){}
    get paymentHistory(){}
    get isUnknown(){return false;}
}
function createUnknownCustomer(){
    return {
        isUnknown : true,
    };
}


//특이 케이스 조건 검사 부분 함수로 추출, 및 사용
function isUnknown(arg){
    return (arg === "미확인 고객");
}
function client1(){
    const aCustomer = site.customer;
    /*코드 들*/
    let customerName;
    if(isUnknown(aCustomer)) customerName = "거주자";
    else customerName = aCustomer.name;
}
function client2(){
    const plan = isUnknown(aCustomer)
        ? registry.billingPlans.basic
        : aCustomer.billingPlan;
}
function client3(){
    const weeksDelinquent = isUnknown(aCustomer)
        ? 0
        : aCustomer.paymentHistory.weeksDelinquentInLastYear;
}


//이제 조건 검사하는 코드를 클래스로 옮긴다.
class Customer{
    get name(){}
    get billingPlan(){}
    set billingPlan(arg){}
    get paymentHistory(){}
    get isUnknown(){return (this._customer === "미확인 고객")?createUnknownCustomer():this._customer;}
}
function createUnknownCustomer(){
    return {
        isUnknown : true,
    };
}
function isUnknown(arg){
    return arg.isUnknown;
}


//표준 응답을 리터럴 값으로 대체
function createUnknownCustomer(){
    return {
        isUnknown : true,
        name: "거주자",
    };
}
function isUnknown(arg){
    return arg.isUnknown;
}
function client1(){
    let customerName = aCustomer.name;
}


//표준 응답을 리터럴 값으로 대체
function createUnknownCustomer(){
    return {
        isUnknown : true,
        name: "거주자",
        billingPlan : registry.billingPlans.basic,
    };
}
function isUnknown(arg){
    return arg.isUnknown;
}
function client1(){
    let customerName = aCustomer.name;
}
function client2(){
    const plan = aCustomer.billingPlan;
}


class Site{
    get customer(){return this._customer;}
}
class Customer{
    get name(){}
    get billingPlan(){}
    set billingPlan(arg){}
    get paymentHistory(){}
    get isUnknown(){return (this._customer === "미확인 고객")?createUnknownCustomer():this._customer;}
}
//표준 응답을 리터럴 값으로 대체
function createUnknownCustomer(){
    return {
        isUnknown : true,
        name: "거주자",
        billingPlan : registry.billingPlans.basic,
        paymentHistory : {
            weeksDelinquentInLastYear : 0,
        },
    };
}
function isUnknown(arg){
    return arg.isUnknown;
}
function client1(){
    let customerName = aCustomer.name;
}
function client2(){
    const plan = aCustomer.billingPlan;
}
function client3(){
    const weeksDelinquent =aCustomer.paymentHistory.weeksDelinquentInLastYear;
}

 

예시: 변환 함수 이용하기

변환 단계를 추가하면 레코드에도 적용할 있다.



const record = {
    name : "애크미 보스턴",
    location : "Malden MA",
    //여러 정보들...
    customer : {
        name: "애크미 산업",
        billingPlan:"plan-451",
        paymentHistory:{
            weeksDeliquentInLastYear: 7,
            //여러 정보들...
        },
        /*....... */
    }
}
const record2 ={
    name : "물류창고 15",
    location : "Malden MA",
    //여러 정보들...
    customer:"미확인 고객",
}
//클라이언트1
function client1(){
    const site = acquireSiteData();
    const aCustomer = site.customer;
    //여러 코드...
    let customerName;
    if(aCustomer === "미확인 고객") customerName = "거주자";
    else customerName = aCustomer.name;
}
function client2(){
    const plan = (aCustomer === "미확인 고객")
        ? registry.billingPlans.basic
        : aCustomer.billingPlan;
}
function client3(){
    const weeksDelinquent = (aCustomer === "미확인 고객")
        ? 0
        : aCustomer.paymentHistory.weeksDeliquentInLastYear;
}


//클라이언트1
function client1(){
    const rawSite = acquireSiteData();
    const site = enrichSite(rawSite);
    const aCustomer = site.customer;
    //여러 코드...
    let customerName;
    if(aCustomer === "미확인 고객") customerName = "거주자";
    else customerName = aCustomer.name;
    function enrichSite(inputSite){
        return _.cloneDeep(inputSite);
    }
}


//클라이언트1
function client1(){
    const rawSite = acquireSiteData();
    const site = enrichSite(rawSite);
    const aCustomer = site.customer;
    //여러 코드...
    let customerName;
    if(isUnknown(aCustomer)) customerName = "거주자";
    else customerName = aCustomer.name;
    function enrichSite(inputSite){
        return _.cloneDeep(inputSite);
    }
}
function isUnknown(aCustomer){
    return aCustomer === "미확인 고객";
}
function client2(){
    const plan = isUnknown(aCustomer)
        ? registry.billingPlans.basic
        : aCustomer.billingPlan;
}
function client3(){
    const weeksDelinquent = isUnknown(aCustomer)
        ? 0
        : aCustomer.paymentHistory.weeksDeliquentInLastYear;
}


    //변환함수 보강
    function enrichSite(inputSite){
        const result = _.cloneDeep(inputSite);
        const unknownCustomer = {
            isUnknown: true,
        };
        if(isUnknown(result.customer)) result.customer = unknownCustomer;
        else result.customer.isUnknown =false;
        return result;
    }


//변환함수 고려한 수정
function isUnknown(aCustomer){
    if (aCustomer === "미확인 고객") return true;
    else return aCustomer.isUnknown;
}


//클라이언트1
function client1(){
    const rawSite = acquireSiteData();
    const site = enrichSite(rawSite);
    const aCustomer = site.customer;
    //여러 코드...
    const customerName = aCustomer.name;
}
function enrichSite(inputSite){
    const result = _.cloneDeep(inputSite);
    const unknownCustomer = {
        isUnknown: true,
        name: "거주자", //이름 옮기기
    };
    if(isUnknown(result.customer)) result.customer = unknownCustomer;
    else result.customer.isUnknown =false;
    return result;
}


function enrichSite(inputSite){
    const result = _.cloneDeep(inputSite);
    const unknownCustomer = {
        isUnknown: true,
        name: "거주자",
        billingPlan :registry.billingPlans.basic, // 요금제 옮기기
    };
    if(isUnknown(result.customer)) result.customer = unknownCustomer;
    else result.customer.isUnknown =false;
    return result;
}


function client2(){
    const plan = aCustomer.billingPlan;
}


const record = {
    name : "애크미 보스턴",
    location : "Malden MA",
    //여러 정보들...
    customer : {
        name: "애크미 산업",
        billingPlan:"plan-451",
        paymentHistory:{
            weeksDeliquentInLastYear: 7,
            //여러 정보들...
        },
        /*....... */
    }
}
const record2 ={
    name : "물류창고 15",
    location : "Malden MA",
    //여러 정보들...
    customer:"미확인 고객",
}
//클라이언트1
function client1(){
    const rawSite = acquireSiteData();
    const site = enrichSite(rawSite);
    const aCustomer = site.customer;
    //여러 코드...
    const customerName = aCustomer.name;
}
function enrichSite(inputSite){
    const result = _.cloneDeep(inputSite);
    const unknownCustomer = {
        isUnknown: true,
        name: "거주자",
        billingPlan :registry.billingPlans.basic,
        paymentHistory : {
            weeksDeliquentInLastYear : 0,
        },
    };
    if(isUnknown(result.customer)) result.customer = unknownCustomer;
    else result.customer.isUnknown =false;
    return result;
}
function isUnknown(aCustomer){
    if (aCustomer === "미확인 고객") return true;
    else return aCustomer.isUnknown;
}
function client2(){
    const plan = aCustomer.billingPlan;
}
function client3(){
    const weeksDelinquent = aCustomer.paymentHistory.weeksDeliquentInLastYear;
}

 

개요

복잡한 조건부 로직은 난해하다. 클래스와 다형성을 이용하면 구조를 직관적으로 구조화할 있다.

 

예시1_ 타입을 여러 만들고 타입에 따라 조건부 로직을 수행하도록 구성한다.

대게 switch 문이 포함된 함수가 여기에 해당한다.

case 별로 클래스를 하나씩 만들어 switch문을 제거할 있다.

 

예시2_ 기본 동작을 위한 case문과 변형 동작으로 구성된 로직

기본 동작은 슈퍼 클래스에 넣고 변형 동작을 서브클래스로 만든다.

 

모든 조건부 로직을 다형성으로 대체하는 것도 좋진 않다. 간단한 조건부 로직 대부분은 if/else switch/case 해결해도 문제없다.

복잡한 조건부 로직에서 다형성이 막강한 도구가 된다.

 

예시



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.numberOfCocounts>2) ? "지쳤다" : "보통이다";
    case "노르웨이 파랑 앵무":
        return (bird.voltage > 100) ? "그을렸다" : "예쁘다" ;
    default:
        return "알 수 없다";
    }
}
function airSpeedVelocity(bird){
    switch (bird.type) {
    case "유럽 제비":
        return 35;
    case "아프리카 제비":
        return 40 -2 * bird.numberOfCocounts;      
    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){
    return new Bird(bird).plumage;
}
function speed(bird){
    return new Bird(bird).airSpeedVelocity;
}
// 여러 함수를 클래스로 묶기 리팩터링
class Bird{
    constructor(birdObject){
        Object.assign(this. birdObject);
    }
    get plumage(){
        switch (this.type) {
        case "유럽 제비":
            return "보통이다";
        case "아프리카 제비":
            return (this.numberOfCocounts>2) ? "지쳤다" : "보통이다";
        case "노르웨이 파랑 앵무":
            return (this.voltage > 100) ? "그을렸다" : "예쁘다" ;
        default:
            return "알 수 없다";
        }
    }
    get airSpeedVelocity(){
        switch (this.type) {
        case "유럽 제비":
            return 35;
        case "아프리카 제비":
            return 40 -2 * this.numberOfCocounts;      
        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){
    return createBird(bird).plumage;
}
function speed(bird){
    return createBird(bird).airSpeedVelocity;
}
class Bird{
    constructor(birdObject){
        Object.assign(this. birdObject);
    }
    get plumage(){
        switch (this.type) {
        case "유럽 제비":
            return "보통이다";
        case "아프리카 제비":
            return (this.numberOfCocounts>2) ? "지쳤다" : "보통이다";
        case "노르웨이 파랑 앵무":
            return (this.voltage > 100) ? "그을렸다" : "예쁘다" ;
        default:
            return "알 수 없다";
        }
    }
    get airSpeedVelocity(){
        switch (this.type) {
        case "유럽 제비":
            return 35;
        case "아프리카 제비":
            return 40 -2 * this.numberOfCocounts;      
        case "노르웨이 파랑 앵무":
            return (this.isNailed) ? 0 : 10 + this.voltage /10;
        default:
            return null;
        }
    }
}
// 팩터리 메서드
function createBird(bird){
    switch (bird.type) {
    case "유럽 제비":
        return new EuropeanSwallow(bird);
    case "아프리카 제비":
        return new AfricanSwallow(bird);
    case "노르웨이 파랑 앵무":
        return new NorwegianBlueParrot(bird);
    default:
        return new Bird(bird);
    }
}
//서브클래스
class EuropeanSwallow extends Bird{
}
class AfricanSwallow extends Bird{
}
class NorwegianBlueParrot extends Bird{
}


class Bird{
    constructor(birdObject){
        Object.assign(this. birdObject);
    }
    get plumage(){
        switch (this.type) {
        case "유럽 제비":
            throw "오류 발생";
        case "아프리카 제비":
            return (this.numberOfCocounts>2) ? "지쳤다" : "보통이다";
        case "노르웨이 파랑 앵무":
            return (this.voltage > 100) ? "그을렸다" : "예쁘다" ;
        default:
            return "알 수 없다";
        }
    }
    get airSpeedVelocity(){
        switch (this.type) {
        case "유럽 제비":
            return 35;
        case "아프리카 제비":
            return 40 -2 * this.numberOfCocounts;      
        case "노르웨이 파랑 앵무":
            return (this.isNailed) ? 0 : 10 + this.voltage /10;
        default:
            return null;
        }
    }
}
function createBird(bird){
    switch (bird.type) {
    case "유럽 제비":
        return new EuropeanSwallow(bird);
    case "아프리카 제비":
        return new AfricanSwallow(bird);
    case "노르웨이 파랑 앵무":
        return new NorwegianBlueParrot(bird);
    default:
        return new Bird(bird);
    }
}
class EuropeanSwallow extends Bird{
    //하나 씩 오버라이드 시작
    get plumage(){
        return "보통이다";
    }
}


class Bird{
    constructor(birdObject){
        Object.assign(this. birdObject);
    }
    //오버라이드한 후 슈퍼클래스 메서드 상태
    get plumage(){
        return "알 수 없다";
    }
    get airSpeedVelocity(){
        switch (this.type) {
        case "유럽 제비":
            return 35;
        case "아프리카 제비":
            return 40 -2 * this.numberOfCocounts;      
        case "노르웨이 파랑 앵무":
            return (this.isNailed) ? 0 : 10 + this.voltage /10;
        default:
            return null;
        }
    }
}
function createBird(bird){
    switch (bird.type) {
    case "유럽 제비":
        return new EuropeanSwallow(bird);
    case "아프리카 제비":
        return new AfricanSwallow(bird);
    case "노르웨이 파랑 앵무":
        return new NorwegianBlueParrot(bird);
    default:
        return new Bird(bird);
    }
}
class EuropeanSwallow extends Bird{
    get plumage(){
        return "보통이다";
    }
}
class AfricanSwallow extends Bird{
    get plumage(){
        return (this.numberOfCocounts>2) ? "지쳤다" : "보통이다";
    }
}
class NorwegianBlueParrot extends Bird{
    get plumage(){
        return (this.voltage > 100) ? "그을렸다" : "예쁘다" ;
    }
}


function plumages(birds){
    return new Map(birds
                    .map(b=> createBird(b))
                    .map(bird => [bird.name, bird.plumage]));
}
function speeds(birds){
    return new Map(birds
                    .map(b=> createBird(b))
                    .map(bird => [bird.name, bird.airSpeedVelocity]));
}
/**제거 해도된다. 자바스크립트는 타입 계층 구조가 없어도
 * 객체들이 같은 메서드 선부만 가지면, 같은 타입으로 인식해 다형성을 구현할 수 있다.
 * 이를 덕타이핑이라한다.
 */
class Bird{
    constructor(birdObject){
        Object.assign(this. birdObject);
    }
    get plumage(){
        return "알 수 없다";
    }
    get airSpeedVelocity(){
        return null;
    }
}


function createBird(bird){
    switch (bird.type) {
    case "유럽 제비":
        return new EuropeanSwallow(bird);
    case "아프리카 제비":
        return new AfricanSwallow(bird);
    case "노르웨이 파랑 앵무":
        return new NorwegianBlueParrot(bird);
    default:
        return new Bird(bird);
    }
}
class EuropeanSwallow extends Bird{
    get plumage(){
        return "보통이다";
    }
    get airSpeedVelocity(){
        return 35;
    }
}
class AfricanSwallow extends Bird{
    get plumage(){
        return (this.numberOfCocounts>2) ? "지쳤다" : "보통이다";
    }
    get airSpeedVelocity(){
        return 40 -2 * this.numberOfCocounts;      
    }
}
class NorwegianBlueParrot extends Bird{
    get plumage(){
        return (this.voltage > 100) ? "그을렸다" : "예쁘다" ;
    }
    get airSpeedVelocity(){
        return (this.isNailed) ? 0 : 10 + this.voltage /10;
    }
}

 

 

예시: 변형 동작을 다형성으로 표현하기

앞의 예는 계층구조와 상속을 정확히 활용한 다형성 활용법이다.

상속은 똑같은 객체지만 다른 부분도 있음을 표현할 때도 사용한다.

 

선박 항해 투자 등급 계산 코드

위험요소, 잠재 수익 다양한 요인을 기초로 'A' / 'B' 등급으로 나눔

 



function rating(voyage, history){//투자 등급
    const vpf = voyageProfitFactor(voyage, history);
    const vr = voyageRisk(voyage);
    const chr = captainHistoryRisk(voyage, history);
    if(vpf * 3 > (vr+chr*2)) return "A";
    else return "B";
}
function voyageRisk(voyage){//항해 경로 위험요소
    let result = 1;
    if(voyage.length > 4) result +=2;
    if(voyage.length > 8) result += voyage.length -8 ;
    if(["중국","동인도"].includes(voyage.zone)) result += 4;
    return Math.max(result,0);
}
function captainHistoryRisk(voyage, history){//선장 향해 이력 위험요소
    let result = 1;
    if(history.length<5) result +=4;
    result += history.filter( v => v.profit < 0).length;
    if(voyage.zone ==="중국" && hasChina(history)) result -= 2;
    return Math.max(result,0);
}
function hasChina(history){ //중국을 경유하나
    return history.some(v=>"중국" === v.zone);
}
function voyageProfitFactor(voyage, history){
    let result = 2;
    if(voyage.zone === "중국") result += 1;
    if(voyage.zone === "동인도") result += 1;
    if(voyage.zone === "중국" && hasChina(history)) {
        result +=3;
        if(history.length>10) result += 1;
        if(voyage.length >12) result += 1;
        if(voyage.length >18) result -= 1;
    }else{
        if(history.length > 8) result +=1;
        if(voyage.length > 14) result -=1;
    }
    return result;
}
//호출 코드
const voyage = {zone:"서인도", length:10};
const history = [
    {zone : "동인도", profit: 5},
    {zone : "서인도", profit: 15},
    {zone : "중국", profit: -2},
    {zone : "서아프리카", profit: 7},
];
const myRating = rating(voyage, history);
console.log(myRating);
중국을 다녀온 있는 선장이 중국을 경유해 향해하는 특수한 상황 로직을 기본 동작에서 분리하기 위해 상속과 다형성을 이용할
특수한 로직 반복으로 기본 동작을 이해하는 방해가 되고 있다.



function rating(voyage, history){//투자 등급
    return new Rating(voyage,history);
}
//다형성 적용을 위해선 클래스가 있어야하니 여러 함수를 클래스로 묶기 적용
class Rating{
    constructor(voyage,history){
        this.voyage = voyage;
        this.history =history;
    }
    get value() {
        const vpf = this.voyageProfitFactor;
        const vr = this.voyageRisk;
        const chr = this.captainHistoryRisk;
        if(vpf * 3 > (vr+chr*2)) return "A";
        else return "B";
    }
    get voyageRisk(){//항해 경로 위험요소
        let result = 1;
        if(this.voyage.length > 4) result +=2;
        if(this.voyage.length > 8) result += this.voyage.length -8 ;
        if(["중국","동인도"].includes(this.voyage.zone)) result += 4;
        return Math.max(result,0);
    }
   
    get captainHistoryRisk(){//선장 향해 이력 위험요소
        let result = 1;
        if(this.history.length<5) result +=4;
        result += this.history.filter( v => v.profit < 0).length;
        if(this.voyage.zone ==="중국" && this.hasChina) result -= 2;
        return Math.max(result,0);
    }
    get hasChina(){ //중국을 경유하나
        return this.history.some(v=>"중국" === v.zone);
    }
    get voyageProfitFactor(){
        let result = 2;
        if(this.voyage.zone === "중국") result += 1;
        if(this.voyage.zone === "동인도") result += 1;
        if(this.voyage.zone === "중국" && this.hasChina) {
            result +=3;
            if(this.history.length>10) result += 1;
            if(this.voyage.length >12) result += 1;
            if(this.voyage.length >18) result -= 1;
        }else{
            if(this.history.length > 8) result +=1;
            if(this.voyage.length > 14) result -=1;
        }
        return result;
    }
}


//호출 코드
const voyage = {zone:"서인도", length:10};
const history = [
    {zone : "동인도", profit: 5},
    {zone : "서인도", profit: 15},
    {zone : "중국", profit: -2},
    {zone : "서아프리카", profit: 7},
];
const myRating = rating(voyage, history).value;
console.log(myRating);


//변형 동작을 담을 빈 서브 클래스
class ExperiencedChinaRating extends Rating{
}
//팩터리 함수
function createRating(voyage, history){
    if(voyage.zone === "중국" && history.some(v=>"중국" === v.zone))
        return new ExperiencedChinaRating(voyage,history);
    else return new Rating(voyage, history);
}


//팩터리 함수 사용하도록 수정
function rating(voyage, history){
    return createRating(voyage,history).value;
}


    //Rating class ...
    get captainHistoryRisk(){
        let result = 1;
        if(this.history.length<5) result +=4;
        result += this.history.filter( v => v.profit < 0).length;
        // if(this.voyage.zone ==="중국" && this.hasChinaHistory) result -= 2;
        return Math.max(result,0);
    }




//변형 동작을 담을 빈 서브 클래스
class ExperiencedChinaRating extends Rating{
    get captainHistoryRisk(){
        const result = super.captainHistoryRisk - 2;
        return Math.max(result,0);
    }
}



    //Rating class ... voyageProfitFactor함수는 추출하기 까다롭다.
    get voyageProfitFactor(){
        let result = 2;
        if(this.voyage.zone === "중국") result += 1;
        if(this.voyage.zone === "동인도") result += 1;
        result += this.vogageAndHistoryLengthFactor;
        return result;
    }
    get vogageAndHistoryLengthFactor {
        let result = 0;
        if (this.voyage.zone === "중국" && this.hasChinaHistory) {
            result += 3;
            if (this.history.length > 10)
                result += 1;
            if (this.voyage.length > 12)
                result += 1;
            if (this.voyage.length > 18)
                result -= 1;
        } else {
            if (this.history.length > 8)
                result += 1;
            if (this.voyage.length > 14)
                result -= 1;
        }
        return result;
    }
    //Rating class ...
    get vogageAndHistoryLengthFactor {
        let result = 0;
        if (this.history.length > 8) result += 1;
        if (this.voyage.length > 14) result -= 1;
        return result;
    }


    //ExperiencedChinaRating ...
    get vogageAndHistoryLengthFactor {
        let result = 0;
        result += 3;
        if (this.history.length > 10) result += 1;
        if (this.voyage.length > 12) result += 1;
        if (this.voyage.length > 18) result -= 1;
        return result;
    }

중간 결과


function rating(voyage, history){
    return createRating(voyage,history).value;
}
//기본 동작을 담당할 클래스
class Rating{
    constructor(voyage,history){
        this.voyage = voyage;
        this.history =history;
    }
    get value() {
        const vpf = this.voyageProfitFactor;
        const vr = this.voyageRisk;
        const chr = this.captainHistoryRisk;
        if(vpf * 3 > (vr+chr*2)) return "A";
        else return "B";
    }
    get voyageRisk(){
        let result = 1;
        if(this.voyage.length > 4) result +=2;
        if(this.voyage.length > 8) result += this.voyage.length -8 ;
        if(["중국","동인도"].includes(this.voyage.zone)) result += 4;
        return Math.max(result,0);
    }
   
    get captainHistoryRisk(){
        let result = 1;
        if(this.history.length<5) result +=4;
        result += this.history.filter( v => v.profit < 0).length;
        return Math.max(result,0);
    }
    get voyageProfitFactor(){
        let result = 2;
        if(this.voyage.zone === "중국") result += 1;
        if(this.voyage.zone === "동인도") result += 1;
        result += this.vogageAndHistoryLengthFactor;
        return result;
    }
    get vogageAndHistoryLengthFactor() {
        let result = 0;
        if (this.history.length > 8) result += 1;
        if (this.voyage.length > 14) result -= 1;
        return result;
    }
    get hasChinaHistory(){
        return this.history.some(v=>"중국" === v.zone);
    }
}
//변형 동작을 담을 빈 서브 클래스
class ExperiencedChinaRating extends Rating{
    get captainHistoryRisk(){
        const result = super.captainHistoryRisk - 2;
        return Math.max(result,0);
    }
    get vogageAndHistoryLengthFactor() {
        let result = 0;
        result += 3;
        if (this.history.length > 10) result += 1;
        if (this.voyage.length > 12) result += 1;
        if (this.voyage.length > 18) result -= 1;
        return result;
    }
}
//팩터리 함수
function createRating(voyage, history){
    if(voyage.zone === "중국" && history.some(v=>"중국" === v.zone))
        return new ExperiencedChinaRating(voyage,history);
    else return new Rating(voyage, history);
}


//호출 코드
const voyage = {zone:"서인도", length:10};
const history = [
    {zone : "동인도", profit: 5},
    {zone : "서인도", profit: 15},
    {zone : "중국", profit: -2},
    {zone : "서아프리카", profit: 7},
];
const myRating = rating(voyage, history);
console.log(myRating);

다듬기

변형 동작을 서브클래스로 뽑아냈다.

슈퍼클래스 로직은 간소화되어 이해하기 쉽고 다루기 쉬워졌다.

변형 동작은 슈퍼클래스와 차이를 표현해야 하는 서브클래스에서만 신경 쓰면 된다.

 

"기본 동작 - 변형 동작"으로 분리하는 과정에서 생긴 vogageAndHistoryLengthFactor 함수를 정리하기

And 붙었다는 것은 가지 독립된 일을 수행하고 있다는

    //Rating class ...
    get vogageAndHistoryLengthFactor() {
        let result = 0;
        result += this.historyLengthFactor;
        if (this.voyage.length > 14) result -= 1;
        return result;
    }
    get historyLengthFactor() {
        return (this.history.length > 8) ? 1 : 0;
    }


    //ExperiencedChinaRating class ...
    get vogageAndHistoryLengthFactor() {
        let result = 0;
        result += 3;
        result = this.historyLengthFactor;
        if (this.voyage.length > 12) result += 1;
        if (this.voyage.length > 18) result -= 1;
        return result;
    }
    get historyLengthFactor() {
        return (this.history.length > 10) ? 1 : 0;
    }



    //Rating class ...
    get voyageProfitFactor(){
        let result = 2;
        if(this.voyage.zone === "중국") result += 1;
        if(this.voyage.zone === "동인도") result += 1;
        result += this.vogageAndHistoryLengthFactor;
        result += this.historyLengthFactor;
        return result;
    }
    get vogageAndHistoryLengthFactor() {
        let result = 0;
        // result = this.historyLengthFactor;
        if (this.voyage.length > 14) result -= 1;
        return result;
    }
    get historyLengthFactor() {
        return (this.history.length > 8) ? 1 : 0;
    }
    get hasChinaHistory(){
        return this.history.some(v=>"중국" === v.zone);
    }
}
//변형 동작을 담을 빈 서브 클래스
class ExperiencedChinaRating extends Rating{
    get captainHistoryRisk(){
        const result = super.captainHistoryRisk - 2;
        return Math.max(result,0);
    }
    get vogageAndHistoryLengthFactor() {
        let result = 0;
        result += 3;
        // result = this.historyLengthFactor;
        if (this.voyage.length > 12) result += 1;
        if (this.voyage.length > 18) result -= 1;
        return result;
    }
    get historyLengthFactor() {
        return (this.history.length > 10) ? 1 : 0;
    }
}


    //And 가 분리됐으니 함수 이름 변경 및 추가 정리
    get vogageLengthFactor() {
        return (this.voyage.length > 14) ? -1 : 0;
    }


    //ExperiencedChinaRating class ...
    get vogageLengthFactor() {
        let result = 0;
        result += 3;
        if (this.voyage.length > 12) result += 1;
        if (this.voyage.length > 18) result -= 1;
        return result;
    }



class ExperiencedChinaRating extends Rating{
    get captainHistoryRisk(){
        const result = super.captainHistoryRisk - 2;
        return Math.max(result,0);
    }
    //ExperiencedChinaRating class ...
    get voyageProfitFactor(){
        return super.voyageProfitFactor + 3;
    }
    get vogageLengthFactor() {
        let result = 0;
        // result += 3; voyageProfitFactor으로 옮기는 것이 좋아보인다.
        if (this.voyage.length > 12) result += 1;
        if (this.voyage.length > 18) result -= 1;
        return result;
    }
    get historyLengthFactor() {
        return (this.history.length > 10) ? 1 : 0;
    }
}
완료


function rating(voyage, history){
    return createRating(voyage,history).value;
}
class Rating{
    constructor(voyage,history){
        this.voyage = voyage;
        this.history =history;
    }
    get value() {
        const vpf = this.voyageProfitFactor;
        const vr = this.voyageRisk;
        const chr = this.captainHistoryRisk;
        if(vpf * 3 > (vr+chr*2)) return "A";
        else return "B";
    }
    get voyageRisk(){
        let result = 1;
        if(this.voyage.length > 4) result +=2;
        if(this.voyage.length > 8) result += this.voyage.length -8 ;
        if(["중국","동인도"].includes(this.voyage.zone)) result += 4;
        return Math.max(result,0);
    }
   
    get captainHistoryRisk(){
        let result = 1;
        if(this.history.length<5) result +=4;
        result += this.history.filter( v => v.profit < 0).length;
        return Math.max(result,0);
    }
    get voyageProfitFactor(){
        let result = 2;
        if(this.voyage.zone === "중국") result += 1;
        if(this.voyage.zone === "동인도") result += 1;
        result += this.vogageLengthFactor;
        result += this.historyLengthFactor;
        return result;
    }
    get vogageLengthFactor() {
        return (this.voyage.length > 14) ? -1 : 0;
    }
    get historyLengthFactor() {
        return (this.history.length > 8) ? 1 : 0;
    }
    get hasChinaHistory(){
        return this.history.some(v=>"중국" === v.zone);
    }
}
class ExperiencedChinaRating extends Rating{
    get captainHistoryRisk(){
        const result = super.captainHistoryRisk - 2;
        return Math.max(result,0);
    }
    get voyageProfitFactor(){
        return super.voyageProfitFactor + 3;
    }
    get vogageLengthFactor() {
        let result = 0;
        if (this.voyage.length > 12) result += 1;
        if (this.voyage.length > 18) result -= 1;
        return result;
    }
    get historyLengthFactor() {
        return (this.history.length > 10) ? 1 : 0;
    }
}
function createRating(voyage, history){
    if(voyage.zone === "중국" && history.some(v=>"중국" === v.zone))
        return new ExperiencedChinaRating(voyage,history);
    else return new Rating(voyage, history);
}


//호출 코드
const voyage = {zone:"서인도", length:10};
const history = [
    {zone : "동인도", profit: 5},
    {zone : "서인도", profit: 15},
    {zone : "중국", profit: -2},
    {zone : "서아프리카", profit: 7},
];
const myRating = rating(voyage, history);
console.log(myRating);

개요

조건 문의 형태는 보통 가지다.

하나는 또는 거짓 분기 모두 정상동작인 경우

다른 하나는 분기 쪽만 정상 동작인 경우다.

 

또는 거짓이 모두 정상 동작인 경우 if/else 쓰자

 

쪽만 정상인 경우 비정상인 분기에서 return문으로 함수를 탈출한다. 이를 보호구문이라 한다.

 

함수는 진입점과 반환점이 하나여야만 한다는 규칙을 반드시 준수할 필요 없다.

반환점이 여러 개일 코드가 명확해진다면 반환점을 여러 두는 것을 피하지 말자

진입점은 어차피 언어 차원에서 강제한다.

 

예시

직원 급여 계산하는 코드

function payAmount(employee){
    let result;
    if(employee.isSeparated){//퇴사한 직원
        result = {amount:0,reasonCode:"SEP"};
    }else{
        if(employee.isRetired){//은퇴한 직원
            result = {amount:0, reasonCode:"RET"};
        }else{//급여 계산 로직
            lorem.ipsum(dolor.sitAmet);
            consectetur(adipiscing).elit();
            sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
            ut.enim.ad(minim.veniam);
            result = someFinalComputation();
        }
    }
}


function payAmount(employee){
    let result;
    //가장 바깥조건부터 보호구문으로 변경
    if(employee.isSeparated){return {amount:0,reasonCode:"SEP"};}
    if(employee.isRetired){
        result = {amount:0, reasonCode:"RET"};
    }else{//급여 계산 로직
        lorem.ipsum(dolor.sitAmet);
        consectetur(adipiscing).elit();
        sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
        ut.enim.ad(minim.veniam);
        result = someFinalComputation();
    }
}


function payAmount(employee){
    let result;
    if(employee.isSeparated){return {amount:0,reasonCode:"SEP"};}
    if(employee.isRetired){  return {amount:0, reasonCode:"RET"};}
    //급여 계산 로직
    lorem.ipsum(dolor.sitAmet);
    consectetur(adipiscing).elit();
    sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
    ut.enim.ad(minim.veniam);
    result = someFinalComputation();
}


function payAmount(employee){
    if(employee.isSeparated){return {amount:0,reasonCode:"SEP"};}
    if(employee.isRetired){  return {amount:0, reasonCode:"RET"};}
    //급여 계산 로직
    lorem.ipsum(dolor.sitAmet);
    consectetur(adipiscing).elit();
    sed.do.eiusmod = tempor.incididunt.ut(labore) && dolore(magna.aliqua);
    ut.enim.ad(minim.veniam);
    return someFinalComputation();
}

 

예시:조건 반대로 만들기



function adjustedCapital(anInstrument){
    let result = 0;
    if(anInstrument.capital > 0){
        if(anInstrument.interestRate > 0 && anInstrument.duration > 0){
            result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
        }
    }
    return result;
}


function adjustedCapital(anInstrument){
    let result = 0;
    //반대로 해서 보호구문으로 만듦
    if(anInstrument.capital <= 0){ return result;}
    if(anInstrument.interestRate > 0 && anInstrument.duration > 0){
        result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
    }
    return result;
}


function adjustedCapital(anInstrument){
    let result = 0;
    if(anInstrument.capital <= 0){ return result;}
    if(!(anInstrument.interestRate > 0 && anInstrument.duration > 0)){
        return result;
    }
    result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
    return result
}


function adjustedCapital(anInstrument){
    let result = 0;
    if(anInstrument.capital <= 0){ return result;}
    //not 조건 제거
    if(anInstrument.interestRate <= 0 || anInstrument.duration <= 0){
        return result;
    }
    result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
    return result
}


function adjustedCapital(anInstrument){
    let result = 0;
    //조건식 통합
    if(anInstrument.capital <= 0
        || anInstrument.interestRate <= 0
        || anInstrument.duration <= 0){ return result;}
    result = (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
    return result
}


function adjustedCapital(anInstrument){
    if(anInstrument.capital <= 0
        || anInstrument.interestRate <= 0
        || anInstrument.duration <= 0){ return 0;}
    return (anInstrument.income / anInstrument.duration) * anInstrument.adjustmentFactor;
}

개요

비교하는 조건은 다르지만 결과로 수행하는 동작은 똑같은 코드들이 있다.

이렇게 조건은 다르지만 목적이 같은 조건식을 묶어 하나의 함수로 만들자

 

조건부 코드를 통합하면, 주로 함수 추출하기로 이어진다. 함수 추출하기는 '무엇' 하는지 기술하던 코드를 ''하는지 말해주는 코드로 바꿔주는 효과적인 도구이다.

 

조건식 결과가 똑같더라도 진짜 독립된 검사들이라고 판단되면 절대 리팩터링하면 안된다.

예시: or 사용하기



//장애 수당 계산
function disabilityAmount(anEmployee){
    if(anEmployee.seniority < 2) return 0;
    if(anEmployee.monthsDisabled > 12) return 0;
    if(anEmployee.isPartTime) return 0;
    //...
}


function disabilityAmount(anEmployee){
    //합친 후 항상 테스트
    if(anEmployee.seniority < 2 || anEmployee.monthsDisabled > 12) return 0;
    if(anEmployee.isPartTime) return 0;
    //...
}


function disabilityAmount(anEmployee){
    //합친 후 항상 테스트
    if(anEmployee.seniority < 2 || anEmployee.monthsDisabled > 12 || anEmployee.isPartTime) return 0;
}


function disabilityAmount(anEmployee){
    if(isNotEligibleForDisability()) return 0;
    //내부 함수로 추출했다. 안에서만 의미있으므로
    function isNotEligibleForDisability() {
        return anEmployee.seniority < 2 || anEmployee.monthsDisabled > 12 || anEmployee.isPartTime;
    }
}

 

예시:and 사용하기



function example(anEmployee){
    if(anEmployee.onVacation)
    if(anEmployee.seniority > 10)
        return 1;
    return 0.5;
}


function example(anEmployee){
    if((anEmployee.onVacation)&&(anEmployee.seniority > 10))
        return 1;
    return 0.5;
}

 

개요

복잡한 조건부 로직은 프로그램을 복잡하게 만든다.

 

조건문은 결과에 따른 동작을 표현하지만, "" 일어나는지를 알려주지 않는다.

거대한 코드 블록을 작은 코드 뭉치로 분해 함수로 캡슐화한 적절한 이름을 붙여준다.

이렇게 하면 무엇을 위해 분기했는지 명백해진다.

예시



//여름철엔 할인율이 달라짐
if(!aDate.isBefore(plan.summerStrat) && !aDate.isAfter(plan.summerEnd)){
    charge = quantity * plan.summerRate;
}else{
    charge = quantity * plan.regularRate + plan.regularServiceCharge;
}


if(summer()){
    charge = quantity * plan.summerRate;
}else{
    charge = quantity * plan.regularRate + plan.regularServiceCharge;
}
//추출
function summer() {
    return !aDate.isBefore(plan.summerStrat) && !aDate.isAfter(plan.summerEnd);
}
//나머지도 정리
if(summer()){
    charge = summerCharge();
}else{
    charge = regularCharge();
}
function regularCharge() {
    return quantity * plan.regularRate + plan.regularServiceCharge;
}
function summerCharge() {
    return quantity * plan.summerRate;
}
function summer() {
    return !aDate.isBefore(plan.summerStrat) && !aDate.isAfter(plan.summerEnd);
}
//조건문을 함수로 정리하니 이렇게 추가로 정리할 기회를 얻었다.
charge = summer() ? summerCharge() : regularCharge();

+ Recent posts