조건부 로직 간소화
- 조건부 로직은 특별한 경우에 어떤 로직이 실행되는 것인데 도메인이 복잡해질수록 조건식도 복잡해질 수 밖에 없다.
- 복잡해진 조건식을 잘 다루지 못하면 코드를 읽는게 어렵고 버그가 발생할 확률이 높다.
- 그래서 잘 다뤄야 한다.
10.1 조건문 분해하기
if(!aDate.isBefore(plan.summerStart) && !aDate.isAfter(plan.summerEnd)) {
charge = quantity * plan.summerRate;
} else {
charge = quantity * plan.summerRate + plan.regularServiceCharge;
}
if(summer()){
charge = summerCharge();
} else {
charge = regularCharge();
}
- 조건식이 복잡할 때는 변수 또는 함수로 추출하는 것
- 코드를 선언적으로 읽기에 훨씬 좋아진다. 왜 실행되어야 하는지 이해하기 쉽다.
10.2 조건식 통합하기
if (anEmployee.seniority < 2) return 0
if (anEmployee.monthsDisabled > 12) return 0
if (anEmployee.isPartTime) return 0
const isNotEligibleForDisability = () => {
return anEmployee.seniority < 2 || anEmployee.monthsDisabled > 12 || anEmployee.isPartTime
}
if (isNotEligibleForDisability()) return 0
- 수행하는 동작이 똑같은 코드를 통합하면 하려는 일이 더 명확해진다.
- 중요한 건 독립된 검사들이면 이 리팩터링을 하면 안된다.
- 리액트에서 공통 컴포넌트를 고려할 때 UI가 같아도 모델이 다르면?
10.3 중첩 조건문을 보호 구문으로 바꾸기
function getPayAmount() {
let result
if (isDead) {
result = deadAmount()
} else {
if (isSeparated) {
result = separatedAmount()
} else {
if (isRetired) {
result = retiredAmount()
} else {
result = normalAmount()
}
}
}
return result
}
function getPayAmount() {
if (isDead) {
return deadAmount()
}
if (isSeparated) {
return separatedAmount()
}
if (isRetired) {
return retiredAmount()
}
return normalAmount()
}
- 조건문이 중첩되면 가독성에 매우 좋지 않다.
- 의도를 부각하는 것이 핵심.
10.4 조건부 로직을 다형성으로 바꾸기
10.5 특이 케이스 추가하기
if(aCustomer === '미확인 고객') customerName = "거주자";
function isUnknown(aCustomer) {
return aCustomer === '미확인 고객'
}
- 특이 케이스를 여러곳에서 확인해야 하는 경우 함수로 분리하고 더 나아가서 클래스로 다룰 수 있다.
- 데이터 구조를 읽기만 한다면 객체 리터럴도 괜찮다.
10.6 어서션 추가하기
assert(this.discountRate >= 0);
if(this.discountRate){
base = base - (this.discountRate * base);
}
- 조건이 참일때 제대로 동작하는 코드인 경우 앞에서 해당 조건이 참이기 위한 검증을 진행한다.
- assert로 검증하는 것 vs if문으로 검증하는 것
10.7 제어 플래그를 탈출문으로 바꾸기
let found = false;
for (name in list) {
if(!found) {
if (name === '기원') {
found = true;
}
}
}
let found = false;
for (name in list) {
if (name === '기원') {
break;
}
}
API 리팩터링
11.1 질의 함수와 변경 함수 분리하기
function getTotalOutstandingAndSendBill() {
const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
sendBill();
return result;
}
function totalOutstanding() {
return customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
emailGateway.send(formatBill(customer));
}
- 명령과 질의를 분리한다.
- 함수가 2가지 일을 하기 때문 + 부수 효과를 더 잘 다루기 위해서
11.2 함수 매개변수화하기
function tenPercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.05);
}
function raise(aPerson, factor) {
aPerson.salary = aPerson.salary.multiply(1 + factor);
}
- 함수를 더욱 확장성 있게 만들 수 있다.
- 좋은 인터페이스를 만들기 위한 방법.
11.3 플래그 인수 제거하기
function setDimension(name, value) {
if (name === "height") {
this._height = value;
return;
}
if (name === "width") {
this._width = value;
return;
}
}
function setHeight(value) {this._height = value;}
function setWidth (value) {this._width = value;}
- 플래그 인수로 함수 내부에서 실행하는 로직을 선택하는 것보다 아예 각각의 로직을 분리한다.
- 하지만, 결국 어딘가에는 조건식이 있어야 되긴 한다.
11.4 객체 통째로 넘기기
const low = aRoom.daysTempRange.low;
const high = aRoom.daysTempRange.high;
if (aPlan.withinRange(low, high))
if (aPlan.withinRange(aRoom.daysTempRange))
- 레코드를 통째로 넘기면 변화에 대응하기 쉬워진다. 값이 추가되어도 매개변수는 수정할 필요가 없음.
- 매개변수를 줄일 수 있다.
11.5 매개변수를 질의 함수로 바꾸기
availableVacation(anEmployee, anEmployee.grade);
function availableVacation(anEmployee, grade) {
// 연휴 계산...
availableVacation(anEmployee)
function availableVacation(anEmployee) {
const grade = anEmployee.grade;
// 연휴 계산...
- 피호출 함수가 쉽게 결정할 수 있는 값을 매개변수로 굳이 건넬 필요가 없다, 중복.
- 값을 결정하는 책임 주체를 어디로 둘 것인가.
11.6 질의 함수를 매개변수로 바꾸기
targetTemperature(aPlan)
function targetTemperature(aPlan) {
currentTemperature = thermostat.currentTemperature;
// 생략
targetTemperature(aPlan, thermostat.currentTemperature)
function targetTemperature(aPlan, currentTemperature) {
// 생략
- 함수안에 두기에 거북한 참조(전역변수, 제거하길 원하는 원소)를 매개변수로 바꿔서 해결한다.
- aPlan값으로 만들어진 값도 아니기 때문에 굳이 내부에 저렇게 둘 필요가 없을 것 같다.
11.7 세터 제거하기
class Person {
get name() {...}
set name(aString) {...}
class Person {
get name() {...}
- 의도치 않게 변경될 가능성을 봉쇄한다.
- setter 대신에 도메인에 관련된 메소드를 사용하자.
11.8 생성자를 팩토리 함수로 바꾸기
leadEngineer = new Employee(document.leadEngineer, 'E');
leadEngineer = createEngineer(document.leadEngineer)
- 플래그 값(E)를 넘겨주는 것 보다 함수명으로 어떤 객체를 생성하는지 드러내는 것이 더 명확하다.
11.9 함수를 명령으로 바꾸기
function score(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
// 생략
}
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute() {
this._result = 0;
this._healthLevel = 0;
// 생략
}
}
- 추가적인 연산을 제공한다면 명령으로 만드는게 괜찮을 것 같다.
- 하지만 예시정도의 코드라면 function score 로 사용해도 전혀 무리가 없을 것 같다.
11.10 명령을 함수로 바꾸기
class ChargeCalculator {
constructor (customer, usage){
this._customer = customer;
this._usage = usage;
}
execute() {
return this._customer.rate * this._usage;
}
}
function charge(customer, usage) {
return customer.rate * usage;
}
- 로직이 크게 복잡하지 않다면 함수로 바꿔주는 것이 간단할 것 같다.
11.11 수정된 값 반환하기
let totalAscent = 0;
calculateAscent();
function calculateAscent() {
for (let i = 1; i < points.length; i++) {
const verticalChange = points[i].elevation - points[i-1].elevation;
totalAscent += (verticalChange > 0) ? verticalChange : 0;
}
}
const totalAscent = calculateAscent();
function calculateAscent() {
let result = 0;
for (let i = 1; i < points.length; i++) {
const verticalChange = points[i].elevation - points[i-1].elevation;
result += (verticalChange > 0) ? verticalChange : 0;
}
return result;
}
- 코드를 보고 이렇게 하는 사람이 요즘 있을까? 라는 생각이 들긴 했다.
- 함수를 제대로 만들기, 의도를 드러내기.
11.12 오류 코드를 예외로 바꾸기
if (data)
return new ShippingRules(data);
else
return -23;
if (data)
return new ShippingRules(data);
else
throw new OrderProcessingError(-23);
- 오류 코드보다 예외를 사용하자.
- ErrorBoundary, try-catch
- 일일이 오류 코드를 체크하는 것 보다 특정 레이어에서 에러를 잡으면 된다.
11.13 예외를 사전확인으로 바꾸기
double getValueForPeriod (int periodNumber) {
try {
return values[periodNumber];
} catch (ArrayIndexOutOfBoundsException e) {
return 0;
}
}
double getValueForPeriod (int periodNumber) {
return (periodNumber >= values.length) ? 0 : values[periodNumber];
}
- 예외는 정말 예외적일때만 사용한다.
- 미리 조건을 검사할 수 있다면 검사하는 것이 더 낫다.
상속 다루기
12.1 메서드 올리기
class Employee {...}
class Salesman extends Employee {
get name() {...}
}
class Engineer extends Employee {
get name() {...}
}
class Employee {
get name() {...}
}
class Salesman extends Employee {...}
class Engineer extends Employee {...}
12.2 필드 올리기
class Employee {...}
class Salesman extends Employee {
private String name;
}
class Engineer extends Employee {
private String name;
}
class Employee {
protected String name;
}
class Salesman extends Employee {...}
class Engineer extends Employee {...}
- 필드가 비슷한 방식으로 쓰이면 슈퍼클래스로 올려준다.
12.3 생성자 본문 올리기
class Party {...}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super();
this._id = id;
this._name = name;
this._monthlyCost = monthlyCost;
}
}
class Party {
constructor(name){
this._name = name;
}
}
class Employee extends Party {
constructor(name, id, monthlyCost) {
super(name);
this._id = id;
this._monthlyCost = monthlyCost;
}
}
- 생성자에 공통 코드가 존재하는 경우 슈퍼클래스로 옮긴다.
12.4 메서드 내리기
class Employee {
get quota {...}
}
class Engineer extends Employee {...}
class Salesman extends Employee {...}
class Employee {...}
class Engineer extends Employee {...}
class Salesman extends Employee {
get quota {...}
}
- 특정 서브클래스와만 관련되어 있는 메소드는 내려야 한다.
- 다른 서브클래스에게 필요없는 메소드가 제공되는 것을 막는다.
12.5 필드 내리기
class Employee {
private String quota;
}
class Engineer extends Employee {...}
class Salesman extends Employee {...}
class Employee {...}
class Engineer extends Employee {...}
class Salesman extends Employee {
protected String quota;
}
- 서브 클래스에서만 필요한 필드는 서브 클래스로 내린다.
12.6 타입 코드를 서브 클래스로 바꾸기
function createEmployee(name, type) {
return new Employee(name, type);
}
function createEmployee(name, type) {
switch (type) {
case "engineer": return new Engineer(name);
case "salesman": return new Salesman(name);
case "manager": return new Manager (name);
}
- 플래그 같은 타입 코드 대신에 서브 클래스로 쪼개는 것, 다형성.
- 로직이 복잡해질수록 서브 클래스로 쪼개는게 좋을 것 같다.
12.7 서브 클래스 제거하기
class Person {
get genderCode() {return "X";}
}
class Male extends Person {
get genderCode() {return "M";}
}
class Female extends Person {
get genderCode() {return "F";}
}
class Person {
get genderCode() {return this._genderCode;}
}
- 굳이 서브 클래스를 사용할 필요가 없어지면 제거하는게 좋다.
12.8 슈퍼 클래스 추출하기
class Department {
get totalAnnualCost() {...}
get name() {...}
get headCount() {...}
}
class Employee {
get annualCost() {...}
get name() {...}
get id() {...}
}
class Party {
get name() {...}
get annualCost() {...}
}
class Department extends Party {
get annualCost() {...}
get headCount() {...}
}
class Employee extends Party {
get annualCost() {...}
get id() {...}
}
- 중복을 제거하기 위한 방법, 중복이 있으면 변경이 여러곳에서 일어나야 한다.
12.9 계층 합치기
class Employee {...}
class Salesman extends Employee {...}
class Employee {...}
- 불필요한 클래스를 제거하는 것, 개발을 하면서 지속적으로 수정하는 경우가 발생한다.
12.10 서브 클래스를 위임으로 바꾸기
class Order {
get daysToShip() {
return this._warehouse.daysToShip;
}
}
class PriorityOrder extends Order {
get daysToShip() {
return this._priorityPlan.daysToShip;
}
}
class Order {
get daysToShip() {
return (this._priorityDelegate)
? this._priorityDelegate.daysToShip
: this._warehouse.daysToShip;
}
}
class PriorityOrderDelegate {
get daysToShip() {
return this._priorityPlan.daysToShip
}
}
- 상속받는 서브 클래스 대신 위임으로 만든다.
- 상속은 한번만 쓸 수 있고 클래스들이 강하게 결합된다, 위임은 이러한 문제를 해결한다.
12.11 슈퍼클래스를 위임으로 바꾸기
class List {...}
class Stack extends List {...}
class Stack {
constructor() {
this._storage = new List();
}
}
class List {...}
- 슈퍼 클래스의 기능들이 서브 클래스에 어울리지 않으면 상속보다 위임을 사용한다.