리팩터링 - 1

Junho Bae·2021년 9월 13일
0

1-1. 예제 프로그램

예제 프로그램은 연극을 외주로 받아서 공연하는 극단이다.
명세는 다음과 같다.

  • 공연 요청이 들어오면, 연극의 장르와 관객 규모를 기초로 비용을 책정한다.
  • 장르는 두 가지로, 비극/희극이 존재한다.
  • 공연료와 별개로 포인트를 지급, 다음 의뢰 시 공연료를 할인해준다.
{
  "hamlet" : {"name" : "Hamlet", "type" : "tragedy"},
  "as-like" : {"name" : "As You Like It", "type" : "comedy"},
  "othello" : {"name" : "Othello", "type" : "tragedy"}
}

청구서에 들어갈 데이터도 다음과 같다.

[
   {
    "customer" : "BigCo",
    "performances" : [
      { "playID" : "hamlet",
       	"audience" : 55
      },
      
      {
        "playID" : "as-like",
        "audience" : 35
      },
      {
        "playID" : "othello",
        "audience" : 40
      }
    	]
    }
]

청구서 프로그램은 다음과 같다.

function statement(invoice, plays) {

    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    const format = new Intl.NumberFormat("en-US", {
        style : "currencey", currency: "USD", minimumFractionDigits : 2
    }).format;

    for (let pref of invoice.performances) {
        const play = plays[pref.playID];
        let thisAmount = 0;

        switch(play.type) {
            case "tragedy" : 
            thisAmount = 40000;

            if(pref.audience > 30) {
                thisAmount += 1000*(pref.audience - 30);
            }
            break;

            case "comedy" :
            thisAmount = 30000;
            if(pref.audience>20) {
                thisAmount += 10000 + 500 *(pref.audience-20);
            }
            thisAmount += 300*pref.audience;
            break;

            default:
                throw new Error('알 수 없는 장르 :  ${play.type}');
        }
        
        //포인트 적립
        volumeCredits += Math.max(pref.audience-30,0);
        //희극 관계 5명마다 추가 포인트를 제공
        if("comedy" === play.type) volumeCredits += Math.floor(pref.audience/5);

        //청구 내역을 출력한다.
        result += `${play.name} : ${format(thisAmount/100)} (${pref.audience}석)\n`
    }
}

1-2. 예시 프로그램을 본 소감

내소감

무지성으로 예제 코드를 쳐보면서 느꼈는데.. 사실 길고 복잡한 프로그램이 아니라서 애매하긴 하지만, 굳이, 그리고 클린코드 느낌으로 생각해보자면.. 한 메서드에서 너무 많은 일을 하려는 것은 명확해 보인다. 크게 따져봐도 공연료 계산과 포인트 적립, 더 나아가면 출력 관련해서까지, 너무 많은 일이 한 메서드에 들어 있는 느낌이 든다. 관심사에 따라서 메서드는 여러개로 분리가 가능해 보였다.

개인적으로 switch문을 잘 안썼는데, 일을 하다보면 정말 자주 만나게 되는 것 같은데.. 이부분도 개선이 필요할 수 있지 않을까 생각했다.

본문

물론 이 코드는 긴 코드가 아니고, 그럭저럭 쓸만 하지만, 만약 이런 코드가 수백줄이 된다면 세상 끔찍하다.

저자는, 프로그램이 잘 작동하는 상황에서 그저 코드가 '지저분하다'는 이유로 불평하는 것은 단순히 프로그램을 미적 기준으로 판단하는 것이 아니라고 한다. 결국 협업을 하고, 유지보수를 하기 위해서는 사람이 고쳐야 하기 때문이다. 기존 코드가 깔끔하지 못하면 어떤 버그가 생길지도 모르고.

그래서 저자는,

프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운 형태로 리팩터링 하고 나서 원하는 기능을 추가한다.

그렇군..리팩터링 하라고 코드를 떤져줬을 때 참 막막한데.. 프로그램을 나누고, 기능을 추가하기 쉬운 형태로 바꿔야겠다.

저자가 지적하는 부분은 다음과 같다.

  • 청구 내역을 HTML 태그로 출력하는 기능 분리 필요.
  • 새로운 요구사항에 대처가 어려움. 진짜 새로운 요구사항 많이 들어오더라..

1-3. 리팩터링의 첫 단계

첫 단계는 테스트 코드부터
말해 뭐할까. 테스트 코드 없이 코드를 변경하는 일은 정말 두려운 일인 것 같다. 바꾼 코드가 어떤 영향을 끼칠지 알 수가 없으니. 버그검출기를 만들자. 빨강초록파랑은 요새 테스트 프레임워크가 잘 되어 있으니.. 잘 쓸 수 있지 않을까 싶다.

1-4. statement()함수 쪼개기

코드를 분석해서 얻은 정보든, 휘발성이 강하기 때문에, 잊지 않으려면 코드에 바로 반영해야 한다. 다음에 코드를 볼 때, 코드가 스스로 하는 일을 이야기하도록 해야한다.

함수 추출하기
파악한 정보를 코드에 반영해서, 코드 조각에 코드의 일을 설명하는 이름 지어주기. amountFor(aPerformance)정도로 switch문.

주의할 점은, 지역 변수가 아닌 경우에는 파라미터로 전달, 단 값이 변경되는 경우는 조심히 다루고, 이 변수를 초기화나는 코드로 추출 함수에 포함

function statement(invoice, plays) {

    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    const format = new Intl.NumberFormat("en-US", {
        style : "currencey", currency: "USD", minimumFractionDigits : 2
    }).format;

    for (let pref of invoice.performances) {
        const play = plays[pref.playID];
        let thisAmount = 0;

        thisAmount = amountFor(pref,play);

        //포인트 적립
        volumeCredits += Math.max(pref.audience-30,0);
        //희극 관계 5명마다 추가 포인트를 제공
        if("comedy" === play.type) volumeCredits += Math.floor(pref.audience/5);

        //청구 내역을 출력한다.
        result += `${play.name} : ${format(thisAmount/100)} (${pref.audience}석)\n`
    }
}

function amountFor(pref,play) {
    let thisAmount = 0 ;

    switch(play.type) {
        case "tragedy" : 
        thisAmount = 40000;

        if(pref.audience > 30) {
            thisAmount += 1000*(pref.audience - 30);
        }
        break;

        case "comedy" :
        thisAmount = 30000;
        if(pref.audience>20) {
            thisAmount += 10000 + 500 *(pref.audience-20);
        }
        thisAmount += 300*pref.audience;
        break;

        default:
            throw new Error('알 수 없는 장르 :  ${play.type}');
    }
    return thisAmount;
}
  • 변화하는 변수를 초기화하고, 이걸 반환 (thisAmount)
  • 간단한 수정도 리팩터링 이후에는 바로 테스트하도록.
  • 한번에 너무 많은 수정을 하고 나면 재앙을 불러온다. 얼마전에 실제로 재앙을 맞았다.
  • js니까 중첩함수로 만들어서 매개변수로 꺼내지 않아도 된다.
  • 리팩터링이 끝나면 커밋한다.

리팩터링은 프로그램 수정을 작은 단계로 나눠서 진행한다. 그래야 실수하더라도 중간에 버그를 쉽게 찾을 수 있다.

저자가 언급하는 것 처럼, 자바에서는 함수 추출이 매우 편리하다. 자바라기보다는 인텔리제이같은 에디터라고 해야할까.. 단축키는 opt+cmd+m이었던 것 같은데..

함수 추출 이후에는 코드를 더 명확하게 하기

  • 변수 이름 명확하게 바꾸기. thisAmount -> result
  • perf를 aPerformance로 리팩터링

마틴 파울러는 동적 언어를 사용할 때는 타입이 드러나게 한다고 한다. 파라미터의 접두어로..

play 변수 제거하기
가만 보면, play는 그냥 다시 계산하면 되는 값이다. 불필요한 임시 변수는 복잡하게나 한다. 자바 개발을 할때도, 생각해보면 그냥 함수추출해버리면 막 파라미터가 4개씩 있는 경우도 자주 있었던 것 같다. 그냥 넘어갔는데.. 이제는 바꿔야겠다. 클린코드에서 파라미터 3개 넘어가면 잘못된거라 했는데.. 이를 질의함수로 바꾸자.

function statement(invoice, plays) {

    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    const format = new Intl.NumberFormat("en-US", {
        style : "currencey", currency: "USD", minimumFractionDigits : 2
    }).format;

    for (let pref of invoice.performances) {
        //const play = playFor(pref); // 우변 함수로 추출 -> 제거
        let thisAmount = 0;

        thisAmount = amountFor(pref);

        //포인트 적립
        volumeCredits += Math.max(pref.audience-30,0);
        //희극 관계 5명마다 추가 포인트를 제공
        if("comedy" === playFor(pref)) volumeCredits += Math.floor(pref.audience/5);

        //청구 내역을 출력한다.
        result += `${playFor(pref).name} : ${format(thisAmount/100)} (${pref.audience}석)\n`
    }

    function playFor(aPerformance) {
        return plays[aPerformance.playID];
    }
}

function amountFor(aPerformance) {
    let result = 0 ;

    switch(playFor(aPerformance).type) {
        case "tragedy" : 
        result = 40000;

        if(pref.audience > 30) {
            result += 1000*(pref.audience - 30);
        }
        break;

        case "comedy" :
            result = 30000;
        if(pref.audience>20) {
            result += 10000 + 500 *(pref.audience-20);
        }
        result += 300*pref.audience;
        break;

        default:
            throw new Error('알 수 없는 장르 :  ${playFor(aPerformance)}');
    }
    return result;
}

그냥 함수안에서 어떻게 지역변수만 없앨 줄 알았는데 싹 없애버렸다. 좋은 것 같기도 하고... 이렇게 되면 반복문마다 함수 콜을 할테니까 성능에 이슈가 있어보이기도 한다.

이제 대해서는 후장에서 다룬다고 하고, 설사 느려지더라도 이렇게 두는게 성능 개선에 훨씬 쉽다고 한다.

지역변수가 제거됨에 따라, 추출 작업이 훨씬 쉬워지기 때문에 일단 지역변수 제거부터 하자

이제, statement를 보면, 굳이 thisAmount를 둘 필요 없이 amountFor를 전부 인라인 하면, 이 변수도 지울 수 있다.

function statement(invoice, plays) {

    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    const format = new Intl.NumberFormat("en-US", {
        style : "currencey", currency: "USD", minimumFractionDigits : 2
    }).format;

    for (let pref of invoice.performances) {
        //const play = playFor(pref); // 우변 함수로 추출 -> 제거
        //포인트 적립
        volumeCredits += Math.max(pref.audience-30,0);
        //희극 관계 5명마다 추가 포인트를 제공
        if("comedy" === playFor(pref)) volumeCredits += Math.floor(pref.audience/5);

        //청구 내역을 출력한다.
        result += `${playFor(pref).name} : ${format(amountFor(pref)/100)} (${pref.audience}석)\n`
        totalAmount += amountFor(pref);
    }
    result += `총액 : ${format(totalAmount/100)}\n`;
    result += `적립 포인트 : ${volumeCredits}점\n`; 
    return result;
}

function playFor(aPerformance) {
    return plays[aPerformance.playID];
}

function amountFor(aPerformance) {
    let result = 0 ;

    switch(playFor(aPerformance).type) {
        case "tragedy" : 
        result = 40000;

        if(pref.audience > 30) {
            result += 1000*(pref.audience - 30);
        }
        break;

        case "comedy" :
            result = 30000;
        if(pref.audience>20) {
            result += 10000 + 500 *(pref.audience-20);
        }
        result += 300*pref.audience;
        break;

        default:
            throw new Error('알 수 없는 장르 :  ${playFor(aPerformance)}');
    }
    return result;
}

근데 진짜 성능 이슈를 잘 살펴봐야 할 것 같기는 하다. 학부에서 최적화 배울 때 했던 내용과 반대인 것 같은 느낌이다. 인라인 함수콜을 일부러 빼고 그랬었는데... 뒷장에서 자세히 살펴봐야 할 것 같다.

무튼, 지금 play 변수를 제거하면서 로컬 유효범위가 줄어들어, 적립포인트 계산 부분 추출이 쉬워졌다.

이제 적립 포인트 계산을 빼낸다. 여기서는 perf를 전달만 하면 되는데, volumeCredits가 계속 누적되어야 하기 때문에, 복제본을 초기화한 다음 계산 결과를 반환한다.

function volumeCreditsFor(perf) {
    let result = 0;
    result += Math.max(pref.audience-30,0);
    if("comedy" === playFor(pref)) result += Math.floor(pref.audience/5);
    return result;
}

function statement(invoice, plays) {

    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    const format = new Intl.NumberFormat("en-US", {
        style : "currencey", currency: "USD", minimumFractionDigits : 2
    }).format;

    for (let pref of invoice.performances) {
        volumeCredits += volumeCreditsFor(pref);
  
        result += `${playFor(pref).name} : ${format(amountFor(pref)/100)} (${pref.audience}석)\n`
        totalAmount += amountFor(pref);
    }
    result += `총액 : ${format(totalAmount/100)}\n`;
    result += `적립 포인트 : ${volumeCredits}점\n`; 
    return result;
}

이제 result로 적절하게 변수이름 바꾸는데 그럴것 같았다.

format 변수 제거하기

임시변수는 일단 조지자. 얘가 필요한 부분에서나 의미 있어서, 잘못하면 길고 복잡해지니까. 빼버리자. 지금 임시변수에 함수를 대입한 형태니까, 그냥 함수를 직접 선언해버리자

function format(aNumber) {
    return new Intl.NumberFormat("en-US", {
        style : "currencey", currency: "USD", minimumFractionDigits : 2
    }).format(aNumber);
}

function statement(invoice, plays) {

    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for (let pref of invoice.performances) {
        volumeCredits += volumeCreditsFor(pref);
  
        result += `${playFor(pref).name} : ${format(amountFor(pref)/100)} (${pref.audience}석)\n`
        totalAmount += amountFor(pref);
    }
    result += `총액 : ${format(totalAmount/100)}\n`; //이부분
    result += `적립 포인트 : ${volumeCredits}점\n`; 
    return result;
}

마지막 result 이부분을 함수 호출로 변경해버리자. 함수 변수를 일반 함수로 변경해버리는 것이다.

지금 전체 코드는 점점점점 늘어나고 있지만... statement 함수는 굉장히 짧아지고 있다. 기분이 좋다..

단, 지금 format 함수 자체의 이름이 적절치 못하다. 잘 설명을 못하니까.. 함수선언바꾸기를 진행한다.

function usd(aNumber) {
    return new Intl.NumberFormat("en-US", {
        style : "currencey", currency: "USD", minimumFractionDigits : 2
    }).format(aNumbe/100);
}

function statement(invoice, plays) {

    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for (let pref of invoice.performances) {
        volumeCredits += volumeCreditsFor(pref);
  
        result += `${playFor(pref).name} : ${usd(amountFor(pref))} (${pref.audience}석)\n` //여기
        totalAmount += amountFor(pref);
    }
    result += `총액 : ${usd(totalAmount)}\n`; //여기
    result += `적립 포인트 : ${volumeCredits}점\n`; 
    return result;
}

함수 이름을 바꾸고, 단위 변환로직도 아예 포함해서 직관적인 usd 라는 이름으로 변경한다.
이름이 좋으면, 함수 본문을 읽지 않고도 무슨일을 하는지 알 수 있다.

volumeCredits 변수 제거하기
이 부분은, 반복문 쪼개기로 값이 누적되는 부분을 따로 빼버린다. 분리하기 쉽게. 그리고 문장 슬라이드하기로 여기서 사용되는 변수 선언을 바로 위로 위치시킨다.

성능 걱정이 되긴 한다.

function statement(invoice, plays) {

    let totalAmount = 0;
    //let volumeCredits = 0; 얘를
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for (let pref of invoice.performances) {
        result += `${playFor(pref).name} : ${usd(amountFor(pref))} (${pref.audience}석)\n` 
        totalAmount += amountFor(pref);
    }
	let volumeCredits = 0; //여기로
    for(let pref of invoice.performances){
        volumeCredits += volumeCreditsFor(pref);
    }
    
    result += `총액 : ${usd(totalAmount)}\n`; 
    result += `적립 포인트 : ${volumeCredits}점\n`; 
    return result;
}

딱 먹음직스럽게 위치하고 있다. 이제 느낌이 살짝 오지만,

문장들 모으기 -> 임시변수를 질의 함수로 바꾸기 -> 함수로 추출하기 -> 코드 인라인 하기 의 과정을 조진다.

그럼 반복문이 몇번이여..?


function statement(invoice, plays) {

    let totalAmount = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for (let pref of invoice.performances) {
        result += `${playFor(pref).name} : ${usd(amountFor(pref))} (${pref.audience}석)\n` //여기
        totalAmount += amountFor(pref);
    }

    let volumeCredits = totalVolumeCredits(); // 임시변수 -> 함수추출
    for(let pref of invoice.performances){
        volumeCredits += volumeCreditsFor(pref);
    }
    
    result += `총액 : ${usd(totalAmount)}\n`; 
    result += `적립 포인트 : ${volumeCredits}점\n`; 
    return result;
}

//함수추출
function totalVolumeCredits() {
    let volumeCredits = 0;
    for(let pref of invoice.performances) {
        volumeCredits += volumeCreditsFor(pref);
    }
    return volumeCredits;
}

인라인하기

function statement(invoice, plays) {

    let totalAmount = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for (let pref of invoice.performances) {
        result += `${playFor(pref).name} : ${usd(amountFor(pref))} (${pref.audience}석)\n` //여기
        totalAmount += amountFor(pref);
    }

    for(let pref of invoice.performances){
        volumeCredits += volumeCreditsFor(pref);
    }
    
    result += `총액 : ${usd(totalAmount)}\n`; 
    result += `적립 포인트 : ${totalVolumeCredits()}점\n`; //인라인
    return result;
}

이쯤에서 저자는 성능에 관련된 이야기를 하는데... 결론은 일단은 리팩터링 해라이다. 성능에 영향을 끼치는 경우가 적긴 해도 끼칠 수도 있지만, 그 때 다시 성능을 개선하면 된다. 리팩터링되어있기 때문에 더 수월할거고.

사실 아직은 심정적으로 꺼려지는게 사실이지만... 이게 좋다니까 해야지.

지금까지의 과정

  1. 반복문 쪼개기 : 변수 값 누적시키는 부분 분리
  2. 문장 슬라이드하기 : 코드 모으기
  3. 함수 추출하기
  4. 변수 인라인하기

이렇게 임시변수들을 조져버리고 있다. 깔끔한 것 같기는 하다. 변수 이름도 잘못지어진 임시변수들..temp 이런거 쓰고 두는 경우도 많으니까..

이처럼, totalAmount()도 조져주자. result로 이름 바꾸는 것도 잊지 말고... 아까 포인트 적립할 때도 왜 안바꾸나 했다.

결과이다.

1-5. 난무하는 중첩함수

function statement(invoice, plays) {

    let totalAmount = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for (let pref of invoice.performances) {
        result += `${playFor(pref).name} : ${usd(amountFor(pref))} (${pref.audience}석)\n` //여기
    }

    for(let pref of invoice.performances){
        volumeCredits += volumeCreditsFor(pref);
    }
    
    result += `총액 : ${usd(appleSauce)}\n`; //여기
    result += `적립 포인트 : ${totalVolumeCredits()}점\n`; 
    return result;

    function totalAmount() {
        let result = 0;
        for (let pref of invoice.performances) {
            totalAmount += amountFor(pref);
        }
        return totalAmount;
    }

    function totalVolumeCredits() {
        let result = 0;
        for(let pref of invoice.performances) {
            result += volumeCreditsFor(pref);
        }
        return result;
    }

    function playFor(aPerformance) {
        return plays[aPerformance.playID];
    }
    
    function volumeCreditsFor(perf) {
        let result = 0;
        result += Math.max(pref.audience-30,0);
        if("comedy" === playFor(pref)) result += Math.floor(pref.audience/5);
        return result;
    }
    
    function usd(aNumber) {
        return new Intl.NumberFormat("en-US", {
            style : "currencey", currency: "USD", minimumFractionDigits : 2
        }).format(aNumbe/100);
    }
    
    
    function amountFor(aPerformance) {
        let result = 0 ;
    
        switch(playFor(aPerformance).type) {
            case "tragedy" : 
            result = 40000;
    
            if(pref.audience > 30) {
                result += 1000*(pref.audience - 30);
            }
            break;
    
            case "comedy" :
                result = 30000;
            if(pref.audience>20) {
                result += 10000 + 500 *(pref.audience-20);
            }
            result += 300*pref.audience;
            break;
    
            default:
                throw new Error('알 수 없는 장르 :  ${playFor(aPerformance)}');
        }
        return result;
    }
}

난무하는 중첩함수라길래 바꿀줄 알았는데... 보기 좋아졌다고 한다. 실제로 statement 함수는 어어어어어어엄청 크기가 줄었다.

1-6. 계산 단계와 포맷팅 단계 분리하기

아까 처음에 한 얘기를 기억해보자면, 리팩터링 할 때는 새로운 기능을 추가하기 쉽게 바꾸자고 했다. 이제, 코드를 다 쪼개놨으니, HTML 버전을 만드는 기능을 추가할 것이다. 그런데 지금, 중첩함수로 statement()안에 잔뜩 들어있다. 텍스트 버전과, html 버전 함수 모두가 똑같은 계산 함수를 사용하게 만드는 것이 목표다.

단계 쪼개기
statement()의 로직을 두단계로 분리

  1. statement()에 필요한 데이터를 처리하고
  2. 결과를 텍스트나 HTML로 표현하기

2번이 될 친구들을 함수로 추출하자. 지금 statement() 이 친구는 자체가 텍스트로 청구 내역을 추출하고 있으니까, 얘를 다 뽑아내자.

function statement(invoice, plays) {
    return renderPalinText(invoice,plays);
}

function renderPlainText(invoice,plays) {
    let totalAmount = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for (let pref of invoice.performances) {
        result += `${playFor(pref).name} : ${usd(amountFor(pref))} (${pref.audience}석)\n` //여기
    }

    for(let pref of invoice.performances){
        volumeCredits += volumeCreditsFor(pref);
    }
    
    result += `총액 : ${usd(appleSauce)}\n`; //여기
    result += `적립 포인트 : ${totalVolumeCredits()}점\n`; 
    return result;

    function totalAmount() {
        let result = 0;
        for (let pref of invoice.performances) {
            totalAmount += amountFor(pref);
        }
        return totalAmount;
    }

    function totalVolumeCredits() {
        let result = 0;
        for(let pref of invoice.performances) {
            result += volumeCreditsFor(pref);
        }
        return result;
    }

    function playFor(aPerformance) {
        return plays[aPerformance.playID];
    }
    
    function volumeCreditsFor(perf) {
        let result = 0;
        result += Math.max(pref.audience-30,0);
        if("comedy" === playFor(pref)) result += Math.floor(pref.audience/5);
        return result;
    }
    
    function usd(aNumber) {
        return new Intl.NumberFormat("en-US", {
            style : "currencey", currency: "USD", minimumFractionDigits : 2
        }).format(aNumbe/100);
    }
    
    function amountFor(aPerformance) {
        let result = 0 ;
    
        switch(playFor(aPerformance).type) {
            case "tragedy" : 
            result = 40000;
    
            if(pref.audience > 30) {
                result += 1000*(pref.audience - 30);
            }
            break;
    
            case "comedy" :
                result = 30000;
            if(pref.audience>20) {
                result += 10000 + 500 *(pref.audience-20);
            }
            result += 300*pref.audience;
            break;
    
            default:
                throw new Error('알 수 없는 장르 :  ${playFor(aPerformance)}');
        }
        return result;
    }
}

흠 중첩함수들을 접은 상태에서 복사해도 마크다운에서는 다 펴져서 복사가 되는군. 무튼 맨 위 처럼, 본문을 뽑아낸 상태다.

서술은 안하지만, 이런 단계마다 컴파일-테스트-커밋해야한다.

이제, 두 단계 사이의 중간 데이터 구조 역할을 할 객체를 만들어서 인수로 전달하자.이제 invoice, plays를 통해 전달되는 데이터를 다 이 구조체로 옮기고 나서, 계산 관련 코드를 전부 statement()로 옮겨버릴 수 있다. renderPlainText()는 이제 전달된 구조체 데이터만 처리하면 되는거다.

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    return renderPalinText(statementData,plays);

function enrichPerformance(aPerformance) {
    const result = Object.assign({},aPerformance);
    return result;
}
}
function renderPlainText(data,plays) {
    let totalAmount = 0;
    let result = `청구 내역 (고객명 : ${data.customer})\n`;

    for (let pref of data.performances) {
        result += `${playFor(pref).name} : ${usd(amountFor(pref))} (${pref.audience}석)\n` //여기
    }

    for(let pref of invoice.performances){
        volumeCredits += volumeCreditsFor(pref);
    }
    
    result += `총액 : ${usd(appleSauce)}\n`; //여기
    result += `적립 포인트 : ${totalVolumeCredits()}점\n`; 
    return result;

    function totalAmount() {
        let result = 0;
        for (let pref of invoice.performances) {
            totalAmount += amountFor(pref);
        }
        return totalAmount;
    }

    function totalVolumeCredits() {
        let result = 0;
        for(let pref of invoice.performances) {
            result += volumeCreditsFor(pref);
        }
        return result;
    }

    function playFor(aPerformance) {
        return plays[aPerformance.playID];
    }
    
    function volumeCreditsFor(perf) {
        let result = 0;
        result += Math.max(pref.audience-30,0);
        if("comedy" === playFor(pref)) result += Math.floor(pref.audience/5);
        return result;
    }
    
    function usd(aNumber) {
        return new Intl.NumberFormat("en-US", {
            style : "currencey", currency: "USD", minimumFractionDigits : 2
        }).format(aNumbe/100);
    }
    
    function amountFor(aPerformance) {
        let result = 0 ;
    
        switch(playFor(aPerformance).type) {
            case "tragedy" : 
            result = 40000;
    
            if(pref.audience > 30) {
                result += 1000*(pref.audience - 30);
            }
            break;
    
            case "comedy" :
                result = 30000;
            if(pref.audience>20) {
                result += 10000 + 500 *(pref.audience-20);
            }
            result += 300*pref.audience;
            break;
    
            default:
                throw new Error('알 수 없는 장르 :  ${playFor(aPerformance)}');
        }
        return result;
    }
}

요런식으로, 데이터를 넘겨주면서 변수를 하나씩 지워버린다. 이제 실제 데이터를 담는다.

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return renderPalinText(statementData,plays);

function enrichPerformance(aPerformance) {
    const result = Object.assign({},aPerformance);
    result.play = playFor(result);
    result.amount = amountFor(result);
    result.volumeCredits = volumeCreditsFor(result);

    return result;
}

function playFor(aPerformance) {
    return plays[aPerformance.playID];
}

function amountFor(aPerformance) {
    let result = 0 ;

    switch(aPerformance.play.type) {
        case "tragedy" : 
        result = 40000;

        if(pref.audience > 30) {
            result += 1000*(pref.audience - 30);
        }
        break;

        case "comedy" :
            result = 30000;
        if(pref.audience>20) {
            result += 10000 + 500 *(pref.audience-20);
        }
        result += 300*pref.audience;
        break;

        default:
            throw new Error('알 수 없는 장르 :  ${aPerformance.play.type}');
    }
    return result;
}

function totalAmount() {
    let result = 0;
    for (let pref of invoice.performances) {
        totalAmount += pref.amount;
    }
    return totalAmount;
}

function totalVolumeCredits() {
    let result = 0;
    for(let pref of invoice.performances) {
        result += volumeCreditsFor(pref);
    }
    return result;
}

function volumeCreditsFor(perf) {
    let result = 0;
    result += Math.max(pref.audience-30,0);
    if("comedy" === playFor(pref)) result += Math.floor(pref.audience/5);
    return result;
}

}
function renderPlainText(data,plays) {
    let totalAmount = 0;
    let result = `청구 내역 (고객명 : ${data.customer})\n`;

    for (let pref of data.performances) {
        result += `${pref.play.name} : ${usd(pref.amount)} (${pref.audience}석)\n` //여기
    }

    for(let pref of invoice.performances){
        volumeCredits += pref.volumeCredits;
    }
    
    result += `총액 : ${usd(data.totalAmount)}\n`; //여기
    result += `적립 포인트 : ${data.totalVolumeCredits}점\n`; 
    return result;
  
    function usd(aNumber) {
        return new Intl.NumberFormat("en-US", {
            style : "currencey", currency: "USD", minimumFractionDigits : 2
        }).format(aNumbe/100);
    }
    

}

renderPlainText에서는 data를 쓰도록 변경하고, 기존의 중첩 함수들을 전부 statement로 빼준다.

데이터 객체를 만들어서 전달해버리고, 계산 로직은 아예 statement에서만 처리해버리는 방식... 좋은 것 같다.

이제 반복문을 파이프라인으로 바꾼다.

function totalAmount(data) {
    return data.performances.reduce((total,p) => total + p.amount,0);
}

function totalVolumeCredits(data) {
    return data.performances
    .reduce((total,p) => total +p.volumeCredits,0);
}

그리고, 이제 1번인 'statement()에 필요한 데이터 처리'에 해당하는 코드를 전부 별도 함수로 뺀다.

function statement(invoice, plays) {

    return renderPalinText(createStatementData(invoice,plays));

function createStatementData(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return statementData;
}

두 단계가 명확하게 분리가 되었으니.... 이 파일을 분리해버린다.

profile
SKKU Humanities & Computer Science

0개의 댓글