개요
복잡한 조건부 로직은 난해하다. 클래스와 다형성을 이용하면 구조를 직관적으로 구조화할 수 있다.
예시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);
|