Chapter 08. 기능 이동
✏️ 함수 옮기기
배경
- 좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐이다.
- 이를 모듈성(modularity)라고 한다.
모듈성: 프로그램의 어딘가를 수정하려 할 때 해당 기능과 깊이 관련된 작은 일부만 이해해도 가능하게 해주는 능력.
- 모듈성을 높이려면 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다.
- 독립적으로 고유한 가치가 있는 함수는 분리하고, 관련이 있는 함수끼리는 묶어준다.
- 이를 결정하기 위해서는 대상 함수의 컨택스트(해당 함수가 호출하는 함수는 무엇이 있는지, 사용하는 데이터가 무엇인지)를 살펴본다.
절차
- 선택한 함수가 현재 컨택스트에서 사용중인 모든 요소를 살펴본다. 그리고 이 중 함께 옮길만한 게 있는지 확인한다.
- 호출되는 함수 중 함께 옮길 게 있다면 대체로 그 함수를 먼저 옮기는 게 낫다.
- 얽혀 있는 함수가 여러 개면 영향이 적은 함수부터 옮긴다.
- 선택한 함수가 다형 메서드인지 확인한다.
- 해당 함수를 타깃 컨택스트(옮기려는 위치)에 복사한다.
- 기존의 함수는 소스 함수(source function), 새로 복사한 함수를 타깃 함수(target function)라 한다.
- 새로운 컨택스트에 어울리는 새로운 이름으로 변경해주고 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.
예시
Before
// 현재 상태 : trackSummary 내부에 연산 함수들을 포함하고 있음.
// 목표 : calculateDistance 함수를 최상위로 옮겨 독립적으로 계산
function trackSummary(points) { // 추적 거리의 총 계산
const totalTime = calculateTime();
const totalDistance = calculateDistance();
const pace = totalTime / 60 / totalDistance;
return {
time: totalTime,
distance: totalDistance,
pace: pace,
};
function calculateDistance() { // 총 거리 계산 ⭐️
let result = 0;
for (let i = 1; i < points.length; i++) {
result += distance(points[i - 1], points[i]);
}
return result;
}
function distance(p1, p2) { ... } // 두 지점의 거리 계산
function radians(degrees) { ... } // 라디안 값으로 변환
function calculateTime() { ... } // 총 시간
}
After
- trackSummary 함수 외부로 복사한다.
- 새로운 (임시) 이름을 지어준다. (top_calculateDistance)
- calculateDistance에서만 사용중인 distance, radians 함수를 calculateDistance 함수로 옮긴다.
- 정적 분석(lint)과 테스트를 활용해 검증 후 top_calculateDistance에 동일하게 복사해준다.
- 이름을 totalDistance로 변경해준다.
- 소스 함수를 제거하고 totalDistance 함수에 의존적이지 않은 distance, radians 함수도 최상위로 옮겨준다.
function trackSummary(points) {
const totalTime = calculateTime();
const pace = totalTime / 60 / totalDistance(points); // 변수 인라인하기
return {
time: totalTime,
distance: totalDistance(points),
pace: pace,
};
}
function totalDistance(points) { // ⭐️
let result = 0;
for (let i = 1; i < points.length; i++) {
result += distance(points[i - 1], points[i]);
}
return result;
}
function distance(p1, p2) { ... }
function radians(degrees) { ... }
Deep dive
- ‘데이터끼리 상호 의존하기 쉬우니 중첩 함수는 되도록 만들지 말자’ 는 내용이 있다. 하지만 때론 위와 같이 함수를 최상위로 분리하기보다는 중첩으로 사용하는 것이 더 나을 때도 있을 것이다. 어떤 경우가 있을까?
→ ex) 재사용성은 낮으나 함수의 변수화로 코드의 이해도를 높이고자 하는 경우
✏️ 필드 옮기기
배경
- 프로그램의 진짜 힘은 데이터 구조에서 나온다.
- 적합한 데이터 구조는 동작 코드를 자연스럽게 단순하고 직관적으로 짤 수 있게 한다.
- 반면 잘못 선택한 데이터 구조는 코드를 이해하기 어렵게 만들고, 더 나아가 프로그램의 역할을 흐릿하게 한다.
- 필드를 옮겨야 하는 상황은 다음과 같다.
- 함수에 어떤 레코드를 넘길 때 마다 또 다른 레코드의 필드도 함께 넘기고 있을 때
- 한 레코드를 변경하는데 다른 레코드의 필드까지 변경해야 할 때
- 구조체 여러 개에 정의된 똑같은 필드들을 갱신해야 할 때
절차
- 소스 필드가 캡슐화되어 있지 않다면 캡슐화한다.
- 타깃 객체에 필드(와 접근자 메서드들)을 생성한다.
- 소스 객체에서 타깃 객체를 참조할 수 있는지 확인한다.
- 접근자들이 타깃 필드를 사용하도록 수정한다.
- 소스 필드를 제거한다.
예시
Before
// 목표 : 할인율을 뜻하는 discountRate 필드를 CustomerContract로 옮기기
class Customer { // 고객 클래스
constructor(name, discountRate) {
this._name = name;
this._discountRate = discountRate;
this._contract = new CustomerContract(dateToday());
}
get discountRate() {
return this._discountRate;
}
becomePreferred() {
this._discountRate += 0.03;
...
}
applyDiscount(amount) {
return amount.subtract(amount.multiply(this._discountRate));
}
dateToday() {
return new Date();
}
}
class CustomerContract { // 계약 클래스
constructor(startDate) {
this._startDate = startDate;
}
}
After
- 필드를 캡슐화한다.
- this._setDiscountRate(discountRate)
- CustomerContract 클래스에 필드 하나와 접근자들을 추가한다.
- Customer의 접근자들이 새로운 필드를 사용하도록 수정한다.
class Customer {
constructor(name, discountRate) {
this._name = name;
this._contract = new CustomerContract(this.dateToday());
this._setDiscountRate(discountRate); // contract 클래스 호출 뒤 실행
}
get discountRate() {
return this._contract.discountRate;
}
setDiscountRate(number) {
return this._contract.discountRate = number;
}
becomePreferred() {
this._contract.discountRate += 0.03;
// 다른 코드들이 있음...
}
applyDiscount(amount) {
return amount.subtract(amount.multiply(this._contract.discountRate));
}
dateToday() {
return new Date();
}
}
class CustomerContract {
constructor(startDate, discountRate) {
this._startDate = startDate;
this._discountRate = discountRate; // Customer 클래스에서 setDiscountRate로 값 생성
}
get discountRate() {
return this._discountRate;
}
set discountRate(arg) {
this._discountRate = arg;
}
}
Deep dive
- 이를 함수에서 응용한다면 어떤 예가 있을까?
- custom hook으로의 상태 이동?
✏️ 문장을 함수로 옮기기
배경
- 중복 제거는 코드를 건강하게 관리하는 가장 효과적인 방법 중 하나다.
- 특정 함수를 호출하는 코드가 나올 때마다 그 앞, 뒤에서 똑같은 코드가 추가로 실행될 때
- 반복되는 부분을 피호출 함수로 합치는 방법에 대해 고민해볼 필요가 있다.
- 이렇게 하면 수정할 일이 생겼을 때 단 한 곳만 수정하면 된다.
절차
- 반복 코드가 함수 호출 부분과 멀리 떨어져 있다면 문장 슬라이스하기를 적용해 근처로 옮긴다.
- 타깃 함수를 호출하는 곳이 한 곳뿐이면, 단순히 소스 위치에서 해당 코드를 잘라내어 피호출 함수로 복사한다.
- 호출자가 둘 이상이면 호출자 중 하나에서 '타깃 함수 호출부와 그 함수로 옮기려는 문장 등을 함께' 다른 함수로 추출한다. 추출한 함수에 기억하기 쉬운 임시 이름을 지어준다.
- 다른 호출자 모두가 방금 추출한 함수를 사용하도록 수정한다.
- 모든 호출자가 새로운 함수를 사용하게 되면 원래 함수를 새로운 함수 안으로 인라인한 후 원래 함수를 제거한다.
- 새로운 함수의 이름을 원래 함수의 이름으로 바꿔준다. (더 나은 이름이 있다면 그 이름을 쓴다)
예시
Before
// 함수 내용 : 사진 관련 데이터를 HTML로 내보내는 코드
// 목표 : 제목을 출력하는 코드를 emitPhotoData로 옮겨 중복 제거
function renderPerson(person) { // result 배열에 이름, 사진 데이터, 정보 HTML을 넣는 역할
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(`<p>제목: ${person.photo.title}</p>`); // 제목 출력 ⭐️
result.push(emitPhotoData(person.photo)); // ⭐️
return result.join('\n');
}
function photoDiv(aPhoto) {
return [
'<div>',
`<p>제목: ${p.title}</p>`, // 제목 출력 ⭐️
emitPhotoData(aPhoto), // ⭐️
'</div>'].join('\n');
}
function emitPhotoData(aPhoto) {
const result = [];
result.push(`<p>위치: ${aPhoto.location}</p>`);
result.push(`<p>날짜: ${aPhoto.date.toDateString()}</p>`);
return result.join('\n');
}
function renderPhoto(aPhoto) {
return '';
}
After
- 제목과 emitPhotoData를 호출하는 새 함수를 생성한다.
- 새 함수를 호출하도록 수정한다.
- emitPhotoData 함수를 인라인한다.
- 기존 함수를 제거하고 새 함수의 이름을 기존 함수로 변경한다.
function renderPerson(person) {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(emitPhotoData(person.photo)); // ⭐️
return result.join('\n');
}
function photoDiv(aPhoto) {
return [
'<div>',
emitPhotoData(aPhoto), // ⭐️
'</div>'].join('\n');
}
function emitPhotoData(aPhoto) {
return [
`<p>제목: ${aPhoto.title}</p>`, // ⭐️
`<p>위치: ${aPhoto.location}</p>`
`<p>날짜: ${aPhoto.date.toDateString()}</p>`
].join('\n');
}
✏️ 문장을 호출한 곳으로 옮기기
배경
- 함수는 프로그래머가 쌓아 올리는 추상화의 기본 블록이다.
- 하지만 초기에는 응집도 높고 한 가지 일만 수행하던 함수가 어느새 둘 이상의 다른 일을 수행하게 된다.
- 이를 발견한다면 달라진 동작을 유발하는 코드를 적절한 위치로 옮겨줘 그 경계를 명확히 해야 한다.
절차
- 호출자가 한두 개뿐이고 피호출 함수도 간단한 단순한 상황이면, 피호출 함수의 처음(혹은 마지막)줄을 잘라내어 호출자로 복사해 넣는다.
- 더 복잡한 상황에서는, 이동하지 '않길' 원하는 모든 문장을 함수로 추출한 다음 검색하기 쉬운 임시 이름을 지어준다.
- 원래 함수를 인라인한다.
- 추출된 함수의 이름을 원래 함수의 이름으로 변경한다. (더 나은 이름이 있다면 그 이름을 쓴다)
예시
Before
// 현재 상황 : emitPhotoData를 통해 제목, 날짜, 위치가 div 태그 내에 들어가도록 설정돼있음
// 목표: listRecentPhotos가 위치 정보를 외부로 빼 다르게 렌더링하도록 수정하기
// 이름, 사진, 정보를 생성하는 함수
function renderPerson(outStream, person) {
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream, person.photo);
emitPhotoData(outStream, person.photo);
}
// 기간 필터 후 각 사진별로 div 태그 안에 emitPhotoData(사진 정보) 태그 추가
function listRecentPhotos(outStream, photos) {
photos
.filter((p) => p.date > recentDateCutoff())
.forEach((p) => {
outStream.write('<div>\n');
emitPhotoData(outStream, p);
outStream.write('</div>\n');
});
}
// 제목, 날짜, 위치 생성
function emitPhotoData(outStream, photo) {
outStream.write(`<p>제목: ${photo.title}</p>\n`);
outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
outStream.write(`<p>위치: ${photo.location}</p>\n`); // ⭐️
}
// renderPerson 렌더링 결과
<p>{person.name}</p>
<img />
<div>
<p>제목: {photo.title}</p>
<p>날짜: {photo.date.toDataString()}</p>
<p>위치: {photo.location}</p> // ⭐️
</div>
After
function renderPerson(outStream, person) {
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream, person.photo);
emitPhotoData(outStream, person.photo); // ⭐️
outStream.write(`<p>위치: ${person.photo.location}</p>\n`)
}
function listRecentPhotos(outStream, photos) {
photos
.filter((p) => p.date > recentDateCutoff())
.forEach((p) => {
outStream.write('<div>\n');
emitPhotoData(outStream, p); // ⭐️
outStream.write(`<p>위치: ${p.location}</p>\n`);
outStream.write('</div>\n');
});
}
function emitPhotoData(outStream, photo) { // ⭐️
outStream.write(`<p>제목: ${photo.title}</p>\n`);
outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
}
// renderPerson 렌더링 결과
<p>{person.name}</p>
<img />
<div>
<p>제목: {photo.title}</p>
<p>날짜: {photo.date.toDataString()}</p>
</div>
<p>위치: {photo.location}</p> // ⭐️
✏️ 인라인 코드를 함수 호출로 바꾸기
배경
- 함수는 여러 동작을 하나로 묶어준다.
- 똑같은 코드를 반복하는 대신 함수를 호출하면 된다.
- 이미 존재하는 함수와 똑같은 일을 하는 인라인 코드가 있다면
- 함수 이름을 확인하고 적절하다면 해당 함수로 교체한다.
- 기능만 (우연히) 같을 뿐 사용 목적이 다르다면 함수 추출하기를 사용한다.
절차
- 인라인 코드를 함수 호출로 대체한다.
예시
Before
let appliesToMass = false
for (const s of states) {
if (s === 'MA') {
appliesToMass = true
}
}
After
appliesToMass = states.includes('MA')
✏️ 문장 슬라이드하기
배경
- 관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉽다.
- 관련된 코드끼리 모으는 작업은 다른 리팩터링의 준비 단계로 자주 행해진다.
- 가장 흔한 사례는 변수를 선언하고 사용할 때 관련 있는 변수끼리 모아주는 작업이 있다.
절차
- 코드 조각(문장들)을 이동할 목표 위치를 찾는다.
- 원래 위치와 목표 위치 사이의 코드를 확인하고 이동 가능 범위가 어디까지인지 확인한다.
- 원래 위치에서 잘라내 목표 위치에 붙여넣는다.
예시
Before
// 예제 1
// 목표 : 연관 있는 변수끼리 묶을 수 있도록 위치 이동하기
const pricingPlan = retrievePricingPlan();
const order = retreiveOrder();
let charge;
const chargePerUnit = pricingPlan.unit; // ⭐️
// 예제 2
function someFunc() {
let result;
if (availableResources.length === 0) {
result = createResource();
allocatedResources.push(result); // ⭐️
} else {
result = availableResources.pop();
allocatedResources.push(result); // ⭐️
}
return result;
}
After
// 예제 1
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit; // ⭐️
const order = retreiveOrder();
let charge;
// 예제 2
function someFunc() {
let result;
if (availableResources.length === 0) {
result = createResource();
} else {
result = availableResources.pop();
}
allocatedResources.push(result); // ⭐️
return result;
}
Deep dive
- 이 파트에서는 코드 이동에 있어 부수효과가 없음을 확인하는 방식으로 ‘명령-질의 분리 원칙(Command-Query Separation Principle)을 지켜가며 코딩’하는 습관에 대한 소개가 있었다. 그렇다면 이 원칙은 무엇이며, 어떻게 이 원칙을 지키며 작성을 할 수 있을까?
✏️ 반복문 쪼개기
배경
- 종종 반복문 하나에서 두 가지 일을 수행하는 모습을 보게 된다.
- 이 때 각각의 반복문으로 분리해두면 문제 발생 시 수정할 동작 하나만 이해하고 고칠 수 있다.
절차
- 반복문을 복제해 두 개로 만든다.
- 반복문이 중복돼 생기는 부수효과를 파악해 제거한다.
- 각 반복문을 함수로 추출할지 고민한다.
예시
Before
// 코드 내용 : 전체 급여와 최연소 나이를 계산하는 코드
// 목표 : 반복문 내 두 가지 계산을 처리하는 역할을 분리하기
let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
for (const p of people) {
if (p.age < youngest) youngest = p.age; // ⭐️
totalSalary += p.salary; // ⭐️
}
return `최연소: ${youngest}, 총 급여: ${totalSalary}`;
After
let youngest = people[0] ? people[0].age : Infinity;
let totalSalary = 0;
for (const p of people) { // ⭐️
totalSalary += p.salary;
}
for (const p of people) { // ⭐️
if (p.age < youngest) youngest = p.age;
}
return `youngestAge: ${youngest}, totalSalary: ${totalSalary}`;
// 더 가다듬기
// 반복문을 함수로 호출하기 + 내장 메서드 활용하기
return `youngestAge: ${youngestAge()}, totalSalary: ${totalSalary()}`;
function totalSalary() {
return people.reduce((total, p) => total + p.salary, 0);
}
function youngestAge() {
return Math.min(...people.map(p => p.age));
}
✏️ 반복문을 파이프라인으로 바꾸기
배경
- 컬렉션 파이프라인(Collection Pipeline)을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다.
- 대표적인 연산은 map과 filter다.
- map은 함수를 사용해 입력 컬렉션의 각 원소를 반환하고, filter는 또 다른 함수를 사용해 입력 컬렉션을 필터링해 부분집합을 만든다.
절차
- 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
- 반복문 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다.
- 모든 동작을 대체했다면 반복문 자체를 지운다.
예시
Before
// 함수 역할 : 인도에 자리한 사무실을 찾아 도시명과 전화번호를 반환
// 목표 : 반복문을 컬렉션 파이프라인으로 바꾸기
function acquireData(input) {
const lines = input.split('\n'); // 컬렉션
let firstLine = true;
const result = [];
for (const line of lines) { // 반복문
if (firstLine) { // 첫 줄 건너뛰기
firstLine = false;
continue;
}
if (line.trim() === '') continue; // 빈 줄 지우기
const record = line.split(','); // , 를 기준으로 문장 나누기
if (record[1].trim() === 'India') { // 인도가 확인되면 해당 사무실 레코드를 추출
result.push({ city: record[0].trim(), phone: record[2].trim() });
}
}
return result;
}
After
function acquireData(input) {
const lines = input.split('\n');
return lines
.slice (1)
.filter (line => line.trim() != '')
.map (line => line.split(','))
.filter (fields => fields[1].trim() === 'India')
.map (fields => ({city: fields[0].trim(), phone: fields[2].trim()}));
}
✏️ 죽은 코드 제거하기
배경
- 사용하지 않는 코드들은 최신 컴파일러에 의해 자동으로 제거된다.
- 그렇더라도 이런 코드들은 동작 이해에 커다란 걸림돌이 될 수 있다.
- 그러므로 코드가 더 이상 사용되지 않게 됐다면 지워야 한다.
- 후에 필요할 지도 모른다는 걱정이 든다면 버전 관리 시스템을 활용하자!
절차
- 죽은 코드를 외부에서 참조할 수 있는 경우라면 호출하는 곳이 있는지 확인한다.
- 없다면 제거한다.
Chapter 09. 데이터 조직화
✏️ 변수 쪼개기
배경
- 변수의 대입이 두 번 이상 이루어진다면 여러 가지 역할을 수행한다는 신호다.
- 이 경우 예외 없이 해당 변수를 역할별로 쪼개야 한다.
- 여러 용도로 쓰인 변수는 코드를 읽는 이에게 커다란 혼란을 주기 때문이다.
절차
- 변수를 선언한 곳과 처음 대입하는 곳에서 변수 이름을 바꿔준다.
- 이 때 가능하면 불변으로 선언한다.
- 모든 참조에 새로운 변수명으로 변경한다.
예시
Before
// 예제 1
let temp = 2 * (height + width);
console.log(temp);
temp = height * width; // ⭐️
console.log(temp);
// 예제 2
// 현재 상황: 매개변수 값을 조건에 따라 변경하고 반환하는 상황
function discount(inputValue, quantity) {
if (inputValue > 50) inputValue = inputValue - 2;
if (quantity > 100) inputValue = inputValue - 1;
return inputValue;
}
After
// 예제 1
const perimeter = 2 * (height + width);
console.log(perimeter);
const area = height * width; // ⭐️
console.log(area);
// 예제 2
// 변수 생성 후 해당 변수에 연산값을 적용한 후 반환
function discount(inputValue, quantity) {
let result = inputValue; // ⭐️
if (inputValue > 50) result = inputValue - 2;
if (quantity > 100) result = inputValue - 1;
return result;
}
✏️ 필드 이름 바꾸기
배경
- 데이터 구조는 무슨 일이 벌어지는지를 이해하는 열쇠다.
- 중요한만큼 반드시 깔끔하게 관리해야 한다.
절차
- 레코드의 유효 범위가 제한적이면 필드에 접근하는 모든 코드를 수정하고 끝낸다.
- 범위가 크면 다음 단계로 넘어간다.
- 우선 레코드를 캡슐화한다.
- 캡슐화된 객체 안의 private 필드명을 변경하고, 그에 맞게 내부 메서드들을 수정한다.
- 매개변수 중 필드명과 겹치는 이름이 있다면 함수 선언 바꾸기로 변경한다.
예시
Before
// 목표: "name"을 "title"로 변경하기
class Organization {
constructor(data) {
this._name = data.name; // ⭐️
this._country = data.country;
}
get name() {return this._name} // ⭐️
set name(aString) {this._name = aString;} // ⭐️
get country() {return this._country;}
set country(aCountryCode) {this._country = aCountryCode;}
}
const organization = new Organization({name: "애크미 구스베리", county: "GB"});
After
- 생성자, 접근자 데이터의 name을 title로 변경해준다.
- 생성자에서 title도 받아들일 수 있도록 조치한다.
- this._title = (data._title !== undefined) ? data.title : data.name;
- 생성자를 호출하는 쪽을 새로운 이름으로 변경해준다.
- const organization = new Organization({title: …
- 수정 후 name을 사용할 수 있게 하던 코드를 제거한다.
- 접근자의 이름을 title로 변경해준다.
class Organization {
constructor(data) {
this._title = data.name; // ⭐️
this._country = data.country;
}
get title() {return this._title;} // ⭐️
set title(aString) {this._title = aString;} // ⭐️
get country() {return this._country;}
set country(aCountryCode) {this._country = aCountryCode;}
}
const organization = new Organization({title: "애크미 구스베리", county: "GB"});
✏️ 파생 변수를 질의 함수로 바꾸기
배경
- 가변 데이터는 소프트웨어에 문제를 일으키는 가장 큰 골칫거리에 속한다.
- 예컨데 한 쪽 코드에서 수정한 값이 연쇄 효과를 일으켜 다른 쪽 코드에 원인을 찾기 어려운 문제를 야기하기도 한다.
- 효과가 좋은 방법으로, 값을 쉽게 계산해낼 수 있는 변수들을 모두 제거할 수 있다.
절차
- 변수 값이 갱신되는 지점을 모두 찾는다.
- 해당 변수의 값을 계산해주는 함수를 만든다.
- 해당 변수가 사용되는 모든 곳에 어서션(assetion)을 추가해 함수의 계산 결과가 같은지 확인한다.
- 변수를 읽는 코드를 모두 함수 호출로 대체한다.
- 변수를 선언하고 갱신하는 코드를 죽은 코드 제거하기로 없앤다.
예시
Before
// 현재 상황: adjustment 값을 적용하면서 production까지 갱신
class ProductionPlan {
get production() {
return this._production;
}
applyAdjustment(adjustment) {
this._adjustments.push(adjustment);
this._production += adjustment.amount; // ⭐️
}
}
After
- 어서션을 추가해 테스트해본다.
- assert(this._production === this.calculatedProduction);
- get calculatedProduction() {…}
- 메서드를 인라인하고 옛 변수를 참조하는 코드를 제거한다.
class ProductionPlan {
get production() {
return this._adjustments.reduce((sum, a) => sum + a.amount, 0); // ⭐️
}
applyAdjustment(adjustment) {
this._adjustiments.push(adjustment);
}
}
✏️ 참조를 값으로 바꾸기
배경
- 객체를 다른 객체에 중첩하면 내부 객체를 참조 혹은 값으로 취급할 수 있다.
- 참조로 다루는 경우 내부 객체는 그대로 둔 채 그 객체의 속성만 갱신한다.
- 값으로 다루는 경우는 새로운 속성을 담은 객체로 기존 내부 객체를 통째로 대체한다.
- 필드를 값으로 다루면 내부 객체의 클래스를 수정해 값 객체로 만들 수 있다.
- 값 객체는 불변이기에 자유롭게 활용하기 좋다.
절차
- 후보 클래스가 불변인지, 혹은 불변이 될 수 있는지 확인한다.
- 각각의 세터를 하나씩 제거한다.
- 값 객체의 필드들을 사용하는 비교 메서드를 만든다.
예시
Before
// 현재 상황: Person 클래스에서 생성한 telephoneNumber 데이터의 값을 갱신할 수 있다.
// 목표: 값을 변경하지 않는 방향으로 코드 개선
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.number;
}
set officeNumber(arg) {
this._telephoneNumber.number = arg; // ⭐️
}
}
class TelephoneNumber {
constructor(area, number) {
this._areaCode = area;
this._number = number;
}
get areaCode() {
return this._areaCode;
}
set areaCode(arg) {
this._areaCode = arg; // ⭐️
}
get number() {
return this._number;
}
set number(arg) {
this._number = arg; // ⭐️
}
}
After
- TelephoneNumber 클래스를 불변으로 만든다.
- 세터 함수 제거하기
- Person 클래스 세터 함수에서는 전화번호를 매번 대입해 바꾸도록 한다.
class Person {
constructor() {
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.number;
}
set officeNumber(arg) {
this._telephoneNumber = new TelephoneNumber(this.officeAreaCode, arg); // ⭐️
}
}
class TelephoneNumber {
constructor(area, number) {
this._areaCode = area;
this._number = number;
}
get areaCode() {
return this._areaCode;
}
get number() {
return this._number;
}
}
✏️ 값을 참조로 바꾸기
배경
- 논리적으로 같은 데이터를 물리적으로 복제해 사용할 때 가장 크게 문제되는 상황은 그 데이터를 갱신할 때다.
- 모든 복제본을 찾아 빠짐없이 갱신해야 하며, 하나라도 놓치면 데이터 일관성이 깨져버린다.
- 이런 상황이라면 복제된 데이터들을 모두 참조로 바꿔주는게 좋다.
절차
- 같은 부류에 속하는 객체들을 보관할 저장소를 만든다.
- 생성자에서 이 객체들 중 특정 객체를 정확히 찾아내는 방법이 있는지 확인한다.
- 호스트 객체의 생성자들을 수정해 필요한 객체를 이 저장소에서 찾도록 한다.
예시
Before
// 현재 상황: 고객 객체는 값으로써 사용되며, 만약 고객 ID가 123인 주문을 다섯 개 생성하면 독립된
// 객체가 다섯 개 생성돼 모두 변경해줘야 하는 상황
// 목표: 저장소를 만들어 물리적으로 똑같은 고객 객체를 사용하도록 수정
class Order {
constructor(data) {
this._number = data.number;
this._customer = new Customer(data.customerId); // 고객 ID
}
get customer() {
return this._customer;
}
}
class Customer {
constructor(id) {
this._id = id;
}
get id() {
return this._id;
}
}
After
- 고객 객체를 저장할 수 있는 저장소 객체를 생성한다.
- registerCustomer로 고객 정보를 등록하고 findCustomer로 찾고자 하는 고객을 반환하도록 함수를 제작한다.
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 Order {
constructor(data) {
this._number = data.number;
this._customer = registerCustomer(data.customerId);
}
get customer() {
return this._customer;
}
}
class Customer {
constructor(id) {
this._id = id;
return this._id;
}
}
✏️ 매직 리터럴 바꾸기
매직 리터럴: 소스 코드에 등장하는 일반적인 리터럴 값
ex) 9.80665, 3.14…
배경
- 코드를 읽는 사람이 값의 의미를 모른다면 숫자(값) 자체로는 의미를 명확히 알려주지 못하므로 매직 리터럴이라 할 수 있다.
- 코드를 읽는 사람을 위해 코드 자체의 뜻을 분명하게 드러낼 수 있도록 상수로 정의하는 것이 좋다.
절차
- 상수를 선언하고 매직 리터럴을 대입한다.
- 해당 리터럴이 사용되는 곳을 모두 찾는다.
- 찾은 곳 각각에 리터럴이 새 상수와 동일한 의미로 쓰였는지 확인하고 대체한다.
예시
Before
const potentialEnergy = (mass, height) => {
return mass * height * 9.81
}
After
const STANDARD_GRAVITY = 9.81
const potentialEnergy = (mass, height) => {
return mass * height * STANDARD_GRAVITY
}
반응형
'Programming > 13. Book' 카테고리의 다른 글
리팩터링 - 11장 (0) | 2024.03.02 |
---|---|
리팩터링 - 10장 (0) | 2024.03.02 |
리팩터링 - 5, 6장 (0) | 2024.03.02 |
리팩터링 - 2장 (0) | 2024.03.02 |
모던 리액트 Deep Dive - 11장 (0) | 2024.03.02 |
댓글