개요
데이터 구조의 특정 값을 확인 후 똑같은 동작을 수행하는 코드가 중복이 되는 경우는 흔하다.
이 같은 중복을 한 곳으로 모으는 것이 좋다
특이 케이스 패턴
특수한 경우의 공통 동작을 요소 하나에 모아서 사용하는 것
특이 케이스 패턴을 활용하면 특이 케이스를 확인하는 코드 대부분을 단순한 함수 호출로 바꿀 수 있다.
특이 케이스 객체에서 단순히 데이터만 읽는 다면 리터럴 객체로 만든다.
어떤 동작을 수행해야한다면 일반 객체로 만든다.
특이 케이스 객체 중 가장 유명한게 널 객체 패턴이다.
예시
//전력 회사 가정, 현장에 인프라를 설치해 서비스를 제공할 것 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; } |
'IT책, 강의 > 리팩터링' 카테고리의 다른 글
10 - 조건부 로직 간소화 - 제어 플래그를 탈출문으로 바꾸기 (0) | 2023.09.11 |
---|---|
10 - 조건부 로직 간소화 - 어서션 추가하기 (0) | 2023.09.10 |
10 - 조건부 로직 간소화 - 조건부 로직을 다형성으로 바꾸기 (0) | 2023.09.08 |
10 - 조건부 로직 간소화 - 중첩 조건문을 보호 구문으로 바꾸기 (0) | 2023.09.07 |
10 - 조건부 로직 간소화 - 조건식 통합하기 (0) | 2023.09.06 |