//리팩토링 전
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) {
return (degrees * Math.PI) / 180;
}
function calculateTime() {
return 10000;
}
}
//리팩토링 후
function trackSummary(points) {
const time = calculateTime();
const distance = totalDistance(points)
const pace = time / 60 / distance;
return {
time,
distance,
pace: pace,
};
}
function calculateTime() {
return 10000;
}
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) {
return (degrees * Math.PI) / 180;
}
좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐를 뜻하는 모듈성이다. 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 하면 프로그램의 어딘가를 수정하려 할때 해당 기능과 깊이 관련된 작은 일부만 이해해도 가능하게 해준다. 다른 함수 안에서 도우미 역할로 정의된 함수 중 독립적으로도 고유한 가치가 있는 것은 접근하기 더 쉬운 장소로 옮기는 게 낫다.
//리팩토링 전
class Account {
constructor(number, type, interestRate) {
this._number = number;
this._type = type;
this._interestRate = interestRate;
}
get interestRate() {
return this._interestRate;
}
}
class AccountType {
constructor(nameString) {
this._name = nameString;
}
}
//리팩토링 후
class Account {
constructor(number, type) {
this._number = number;
this._type = type;
}
get interestRate() {
return this._type.interestRate;
}
}
class AccountType {
constructor(nameString, interestRate) {
this._name = nameString;
this._interestRate = interestRate;
}
get interestRate() {
return this._interestRate;
}
}
주어진 문제에 적합한 데이터 구조를 활용하면 동작 코드는 자연스럽게 단순하고 직관적으로 짜여진다. 현재 데이터 구조가 적절치 않음을 깨닫게 되면 곧바로 수정해야 한다. 어떤 레코드를 넘길 때마다 또 다른 레코드의 필드도 함께 넘기고 있다면 데이터 위치를 옮겨야 한다.
한 레코드를 변경하려 할 때 다른 레코드의 필드까지 변경해야만 한다면 필드의 위치가 잘못되었다는 신호이다. 구조체 여러 개에 정의된 똑같은 필드들을 갱신해햐 한다면 한 번만 갱신해도 되는 다른 위치로 옮기는 것이 좋다.
//리팩토링 전
function renderPerson(person) {
const result = [];
result.push(`<p>${person.name}</p>`);
result.push(renderPhoto(person.photo));
result.push(`<p>title: ${person.photo.title}</p>`);
result.push(emitPhotoData(person.photo));
return result.join('\n');
}
function photoDiv(p) {
return ['<div>', `<p>title: ${p.title}</p>`, emitPhotoData(p), '</div>'].join(
'\n'
);
}
function emitPhotoData(aPhoto) {
const result = [];
result.push(`<p>location: ${aPhoto.location}</p>`);
result.push(`<p>date: ${aPhoto.date.toDateString()}</p>`);
return result.join('\n');
}
function renderPhoto(aPhoto) {
return '';
}
//리팩토링 후
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(photo) {
return ['<div>', emitPhotoData(photo), '</div>'].join('\n');
}
function emitPhotoData(aPhoto) {
const result = [];
result.push(`<p>title: ${person.photo.title}</p>`);
result.push(`<p>location: ${aPhoto.location}</p>`);
result.push(`<p>date: ${aPhoto.date.toDateString()}</p>`);
return result.join('\n');
}
function renderPhoto(aPhoto) {
return '';
}
특정 함수를 호출하는 코드가 나올 때마다 그 앞이나 뒤에서 똑같은 코드가 추가로 실행되는 모습을 보면, 그 반복되는 부분을 피호출 함수로 합치는 방법을 궁리해야 한다. 추후 반복되는 부분에서 무언가 수정할 일이 생겼을 때 단 한 곳만 수정하면 된다.
//리팩토링 전
function renderPerson(outStream, person) {
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream, person.photo);
emitPhotoData(outStream, person.photo);
}
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>title: ${photo.title}</p>\n`);
outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
outStream.write(`<p>location: ${photo.location}</p>\n`);
}
//리팩토링 후
function renderPerson(outStream, person) {
outStream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream, person.photo);
emitPhotoData(outStream, person.photo);
outStream.write(`<p>location: ${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>다른 형식의 위치: ${photo.location}</p>\n`);
outStream.write('</div>\n');
});
}
function emitPhotoData(outStream, photo) {
outStream.write(`<p>title: ${photo.title}</p>\n`);
outStream.write(`<p>date: ${photo.date.toDateString()}</p>\n`);
}
함수는 프로그래머가 쌓아 올리는 추상화의 기본 빌딩 블록이다. 그런데 추상화라는 것이 그 경계를 항상 올바르게 긋기가 만만치 않다. 코드베이스의 기능 범위가 달라지면 추상화의 경계도 움직이게 되는데, 초기에는 응집도 높고 한 가지 일만 수행하던 함수가 어느새 둘 이상의 다른 일을 수행하게 바뀔 수 있다.
//리팩토링 전
let appliesToMass = false;
for (const s of states) {
if (s === 'MA') appliesToMass = true;
}
//리팩토링 후
const appliesToMass = states.includes('MA');
함수는 여러 동작을 하나로 묶어준다. 함수의 이름이 코드의 동작 방식보다는 목적을 말해주기 때문에 함수를 활용하면 코드를 이해하기 쉬워진다.
//리팩토링 전
// 예제 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;
}
//리팩토링 후
// 예제 1
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retreiveOrder();
let charge;
// 예제 2
function someFunc() {
const result = availableResources.length === 0
? createResource()
: availableResources.pop();
allocatedResources.push(result);
return result;
}
관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉬워지는데, 이 작업은 다른 리팩터링의 준비 단계로 자주 행해진다. 관련있는 코드들을 명확히 구분되는 함수로 추출하는 게 그저 문장들을 한데로 모으는 것보다 나은 분리법이다.
//리팩토링 전
function reportYoungestAgeAndTotalSalary(people) {
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 `youngestAge: ${youngest}, totalSalary: ${totalSalary}`;
}
//리팩토링 후
function reportYoungestAgeAndTotalSalary(people) {
return `youngestAge: ${youngestAge()}, totalSalary: ${totalSalary()}`;
function youngestAge () {
return Math.min(...people.map(person => person.age));
}
function totalSalary () {
return people.reduce((total, person) => total + person.salary, 0);
}
}
종종 반복문 하나에서 두 가지 일을 수행하는 모습을 보게 된다. 이 경우 가각의 반복문으로 분리해두면 수정할 동작 하나만 이해하면 된다. 반복문을 두 번 실행해야 하므로 이 리팩터링을 불편해할 수 도 있는데 최적화와 리팩터링을 분리하자. 오히려 반복문 쪼개기가 다른 더 강력한 최적화를 적용할 수 있는 경우도 있다.
//리팩토링 전
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;
}
//리팩토링 후
function acquireData(input) {
return input
.split('\n')
.slice(1)
.filter(line => line.trim() !== '')
.map(line => line.split(','))
.filter(records => records[1].trim() === 'India')
.map(records => ({
city: records[0].trim(),
phone: records[2].trim(),
}));
}
반복문 대신에 파이프라인을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다. 객체가 파이프라인을 따라 흐르며 어떻게 처리되는지를 읽을 수 있기 때문에 훨씬 이해하기 쉬워진다.