개요

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

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

 

특이 케이스 패턴

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

 

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

 

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

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

 

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

예시



//전력 회사 가정, 현장에 인프라를 설치해 서비스를 제공할 것
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();

개요

매직 리터럴

소스 코드에 여러 곳에 등장하는 일반적인 리터럴

 

자주 등장하지만 의미없는 값을 상수로 선언하여 이름을 부여하는 것이 이롭다.

 

예시

function potetialEnergy(mass, height){
    return mass * 9.81 * height;
}
const STANDARD_GRAVITY = 9.81;
function potetialEnergy(mass, height){
    return mass * STANDARD_GRAVITY * height;
}

 

개요

데이터 구조를 값으로 다루면, 복사된 값을 사용하게 된다. 일반적으로 권장되는 사용 방식이다.

데이터 구조를 참조로 다루면, 같은 데이터를 여러 곳에서 공유하게 된다.

 

보통은 데이터 구조를 값으로 다루는 것을 권장하나, 값을 갱신해야하는 경우는 예외다. 값으로 다룬다면, 복사본을 모두 찾아 수정해줘야한다.

보통 이런 경우 하나의 객체만 존재하게 된다. 이를 관리하는 저장소를 만들어 관리하는 좋다.

이렇게 되면 저장소를 통해서만 객체에 접근이 가능하다.

예시

class Order {
    constructor(data){
        this._number = data.number;
        this._customer = new Customer(data.customer);
        //여러 데이터 data.customer == id
    }
    get customer(){return this._customer;}
}
//불변 객체다.
class Customer{
    constructor(id){
        this._id = id;
    }
    get id(){return this._id;}
}
불변 객체라 같은 고객의 주문이 들어올때 마다 고객 객체가 늘어난다.
만약 고객 데이터가 변경된다면 복사본 모두를 변경해야 한다.


//물리적으로 똑같은 객체를 저장할 곳
let _repositoryData;//저장소 객체
export function initialize(){
    _repositoryData = {};
    _repositoryData.customers = new Map();
}
export function registerCustomer(id){
    if(!_repositoryData.customers.has(id)){
        _repositoryData.customers.set(id, new Customer(id));
    }
    return findCustomer(id);
}
export function findCustomer(id){
    return _repositoryData.customers.get(id);
}
프로젝트에 쓸만한 저장소가 존재한다면 그것을 사용해도 무방하다.
아래는 노드 모듈로 변환


import {findCustomer, initialize,registerCustomer,_repositoryData} from './repository.mjs';
export class Order {
    constructor(data){
        this._number = data.number;
        this._customer = new Customer(data.customer);
        //여러 데이터...
    }
    get customer(){return this._customer;}
}
//불변 객체다.
export class Customer{
    constructor(id){
        this._id = id;
    }
    get id(){return this._id;}
}
initialize();
registerCustomer("100");
registerCustomer("200");
registerCustomer("100");
console.log(findCustomer("100"));
console.log(_repositoryData);
////////////////////////////////////////////////////
import {Order, Customer} from './ex1.mjs'
//물리적으로 똑같은 객체를 저장할 곳
export let _repositoryData;//저장소 객체
export function initialize(){
    _repositoryData = {};
    _repositoryData.customers = new Map();
}
export function registerCustomer(id){
    if(!_repositoryData.customers.has(id)){
        _repositoryData.customers.set(id, new Customer(id));
    }
    return findCustomer(id);
}
export function findCustomer(id){
    return _repositoryData.customers.get(id);
}



완료


/* ex1.mjs */
import {findCustomer, initialize,registerCustomer,_repositoryData} from './repository.mjs';
export class Order {
    constructor(data){
        this._number = data.number;
        this._customer = registerCustomer(data.customer);
        //여러 데이터...
    }
    get customer(){return this._customer;}
}
//불변 객체다.
export class Customer{
    constructor(id){
        this._id = id;
    }
    get id(){return this._id;}
    set id(arg){this._id = arg;}
}
initialize();


const customer1 = new Customer("10");
const orders = [];
orders.push(new Order({number:1 , customer:"10"}));
orders.push(new Order({number:2 , customer:"10"}));
orders.push(new Order({number:3 , customer:"10"}));
orders.push(new Order({number:4 , customer:"10"}));
console.log(_repositoryData);
orders.forEach(o=>console.log(o));
findCustomer("10").id = "20";
orders.forEach(o=>console.log(o));


/*repository.mjs */
import {Order, Customer} from './ex1.mjs'
//물리적으로 똑같은 객체를 저장할 곳
export let _repositoryData;//저장소 객체
export function initialize(){
    _repositoryData = {};
    _repositoryData.customers = new Map();
}
export function registerCustomer(id){
    if(!_repositoryData.customers.has(id)){
        _repositoryData.customers.set(id, new Customer(id));
    }
    return findCustomer(id);
}
export function findCustomer(id){
    return _repositoryData.customers.get(id);
}



 

고객 정보를 갱신하면 같은 고객을 공유하는 모든 주문에서 갱신된다.

다만, 방법에서 저장소가 전역 객체다. 전역 객체가 꺼림직 하다면 생성자 주입으로 저장소 객체를 주입하는 것도 방법이다.

 

 

개요

객체를 다른 객체에 중첩하면 내부 객체를 참조 혹은 값으로 취급할 있다.

참조, 차이는 다음과 같다.

참조로 다루는 경우는 내부 객체는 그대로 객체 속성만 갱신한다.

값으로 다루는 겨우는 새로운 속성을 담은 객체로 기존 객체를 대체한다.

 

필드를 값으로 다루고 싶다면 내부 객체의 클래스를 수정하여 객체로 만들면 된다.

객체는 불변이기에 자유롭게 활용할 있다.

불변이라면 외부로 값을 내줘도 몰래 값이 변경되어 내부에 영향 받을 걱정이 없다.

특징은 특히 분산 시스템과 동시성 시스템에서 유용하다.

 

예시



//아직 전화번호가 올바로 설정되지 못하게 짜여 있다고 가정
class Person{
    constructor(){
        this._telephoneNumber = new TelephoneNumber();
    }
    get officeAreaCode(){return this._telephoneNumber.areaCode;}
    set officeAreaCode(arg){this._telephoneNumber.areaCode = arg;}
    get officeNumber(){return this._telephoneNumber.officeNumber;}
    set officeNumber(arg){this._telephoneNumber.officeNumber = arg;}
}
class TelephoneNumber{
    get areaCode(){return this._areaCode;}
    set areaCode(arg){this._areaCode = arg;}
    get number(){return this._number;}
    set number(arg){this._number = arg;}
}


class TelephoneNumber{
    //불변으로 만들기 위해 세터 제거 후 생성자로만 받는다.
    constructor(areaCode, number){
        this._areaCode = areaCode;
        this._number = number;
    }
    get areaCode(){return this._areaCode;}
    get number(){return this._number;}
}


class Person{
    constructor(areaCode,officeNumber){
        this._telephoneNumber = new TelephoneNumber();
    }
    get officeAreaCode(){return this._telephoneNumber.areaCode;}
    //값을 설정할 때 마다 매번 새로운 객체를 리턴한다.
    set officeAreaCode(arg){this._telephoneNumber = new TelephoneNumber(arg, this.officeNumber);}
    get officeNumber(){return this._telephoneNumber.officeNumber;}
    set officeNumber(arg){this._telephoneNumber = new TelephoneNumber(this.officeAreaCode,arg);}
}
이제 전화번호 클래스는 불변 클래스이다.
이제 논리적 동치성을 위해 기반 비교를 해야한다.
자바스크립트는 참조 기반 동치성을 기반 동치성으로 대체하는 메서드가 없다.
그냥 equals()함수를 직접 만든다.


class TelephoneNumber{
    constructor(areaCode,officeNumber){
        this._areaCode = areaCode;
        this._officeNumber = officeNumber;
    }
    get areaCode(){return this._areaCode;}
    set areaCode(arg){this._areaCode = arg;}
    get number(){return this._number;}
    set number(arg){this._number = arg;}
    //논리적 동치성 검사 메서드
    equals(other){
        if(!(other instanceof TelephoneNumber)) return false;
        return Object.keys(this).every(a=>this[a] === other[a]);
    }
}


class TelephoneNumber{
    constructor(areaCode, number){
        this._areaCode = areaCode;
        this._number = number;
    }
    get areaCode(){return this._areaCode;}
    get number(){return this._number;}
    //논리적 동치성 검사 메서드
    equals(other){
        if(!(other instanceof TelephoneNumber)) return false;
        return Object.keys(this).every(a=>this[a] === other[a]);
    }
}


class Person{
    constructor(){
        this._telephoneNumber = new TelephoneNumber();
    }
    get officeAreaCode(){return this._telephoneNumber.areaCode;}
    set officeAreaCode(arg){this._telephoneNumber = new TelephoneNumber(areaCode, this.officeNumber);}
    get officeNumber(){return this._telephoneNumber.officeNumber;}
    set officeNumber(arg){this._telephoneNumber = new TelephoneNumber(this.officeAreaCode, arg);}
}
class TelephoneNumber{
    constructor(areaCode, number){
        this._areaCode = areaCode;
        this._number = number;
    }
    get areaCode(){return this._areaCode;}
    get number(){return this._number;}
    //논리적 동치성 검사 메서드
    equals(other){
        if(!(other instanceof TelephoneNumber)) return false;
        return Object.keys(this).every(a=>this[a] === other[a]);
    }
}
const t1 = new TelephoneNumber("12345","67890");
const t2 = new TelephoneNumber("12345","67890");
console.log(t1.equals(t2));

 

 

 

개요

가변 데이터는 소프트웨어 문제를 일으키기 쉽다.

 

값을 쉽게 계산해낼 있는 변수들을 모두 제거한다.

계산 과정을 보여주는 코드 자체가 데이터의 의미를 분명히 드러내는 경우도 많다.

 

예외사항, 피연산자가 불변이라면 계산 결과도 일정하므로 그대로 두는 것도 좋다.

 

변형연산

데이터 구조를 감싸 데이터에 기초하여 계산한 결과를 속성으로 제공하는 객체

데이터 구조를 받아 다른 데이터 구조로 변환해 반환하는 함수

 

예시

class ProductionPlan{
    get production() {return this._production;}
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amount;
    }
}


class ProductionPlan{
    get production() {
        assert(this._production === this.calculatedProduction);
        return this._production;
    }
    get calculatedProduction(){
        return this._adjustments
            .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amount;
    }
}


class ProductionPlan{
    get production() {
        //어셔선 실패하지 않으면 변경
        return this.calculatedProduction;
    }
    get calculatedProduction(){
        return this._adjustments
            .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amount;
    }
}


class ProductionPlan{
    get production() {
        //인라인
        return this._adjustments
            .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amount;
    }
}


class ProductionPlan{
    get production() {
        return this._adjustments
            .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
    }
}

예시: 소스가 이상일

예시는 변경하려는 값에 영향을 주는 요소가 하나뿐이였다.

예시는 이상일



class ProductionPlan {
    constructor(production){
        this._production = production;
        this._adjustments = [];
    }
    get production(){return this._production;}
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amout;
    }
}


class ProductionPlan {
    constructor(production){
        this._initialProduction = production;
        this._productionAccumulator = 0;
        this._adjustments = [];
    }
    get production(){return this._initialProduction + this._productionAccumulator;}
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amout;
    }
}


class ProductionPlan {
    constructor(production){
        this._initialProduction = production;
        this._productionAccumulator = 0;
        this._adjustments = [];
    }
    get production(){
        assert(this._productionAccumulator === this.calculatedProductionAccumulator);
        return this._initialProduction + this._productionAccumulator;
    }
    get calculatedProductionAccumulator(){
        return this._adjustments
        .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amout;
    }
}

 

개요

가변 데이터는 소프트웨어 문제를 일으키기 쉽다.

 

값을 쉽게 계산해낼 있는 변수들을 모두 제거한다.

계산 과정을 보여주는 코드 자체가 데이터의 의미를 분명히 드러내는 경우도 많다.

 

예외사항, 피연산자가 불변이라면 계산 결과도 일정하므로 그대로 두는 것도 좋다.

 

변형연산

데이터 구조를 감싸 데이터에 기초하여 계산한 결과를 속성으로 제공하는 객체

데이터 구조를 받아 다른 데이터 구조로 변환해 반환하는 함수

 

예시

class ProductionPlan{
    get production() {return this._production;}
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amount;
    }
}


class ProductionPlan{
    get production() {
        assert(this._production === this.calculatedProduction);
        return this._production;
    }
    get calculatedProduction(){
        return this._adjustments
            .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amount;
    }
}


class ProductionPlan{
    get production() {
        //어셔선 실패하지 않으면 변경
        return this.calculatedProduction;
    }
    get calculatedProduction(){
        return this._adjustments
            .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amount;
    }
}


class ProductionPlan{
    get production() {
        //인라인
        return this._adjustments
            .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amount;
    }
}


class ProductionPlan{
    get production() {
        return this._adjustments
            .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
    }
}

 

 

 

예시: 소스가 이상일

예시는 변경하려는 값에 영향을 주는 요소가 하나뿐이였다.

예시는 이상일



class ProductionPlan {
    constructor(production){
        this._production = production;
        this._adjustments = [];
    }
    get production(){return this._production;}
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amout;
    }
}


class ProductionPlan {
    constructor(production){
        this._initialProduction = production;
        this._productionAccumulator = 0;
        this._adjustments = [];
    }
    get production(){return this._initialProduction + this._productionAccumulator;}
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amout;
    }
}


class ProductionPlan {
    constructor(production){
        this._initialProduction = production;
        this._productionAccumulator = 0;
        this._adjustments = [];
    }
    get production(){
        assert(this._productionAccumulator === this.calculatedProductionAccumulator);
        return this._initialProduction + this._productionAccumulator;
    }
    get calculatedProductionAccumulator(){
        return this._adjustments
        .reduce((sum, a)=>sum+a.amount,0);
    }
    applyAdjustment(anAdjustment){
        this._adjustments.push(anAdjustment);
        this._production += anAdjustment.amout;
    }
}

 

+ Recent posts