April 1, 2022 • ☕️☕️ 8 min read
기능 이동 - 1
모듈성이란 프로그램의 어딘가를 수정하려 할 때 해당 기능과 깊이 관련된 작은 일부만 이해해도 가능하게 해주는 능력이다. 모듈성을 높이려면 서로 연관된 요소들을 함게 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다.
모든 함수는 어떤 컨텍스트 안에 존재하며, 대부분은 특정 모듈에 속한다. 캡슐화를 위해 함수를 함수가 참조하는 곳이 많은 모듈로 옮겨주는 것이 좋다. 또한 호출자들의 현재 위치나 다음 업데이트 때 바뀌리라 예상되는 위치에 따라서도 함수를 옮겨야 할 수 있다.
함수를 옮기기 전에는 대상 함수의 현재 컨텍스트와 후보 컨텍스트를 둘러보고 대상 함수를 호출하는 함수, 대상 함수가 호출하는 함수, 대상 함수가 사용하는 데이터를 살펴봐야 한다.
// before
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() { ... }
}
중첩 함수인 calculateDistance()
를 최상위로 옮겨서 추적 거리를 다른 정보와는 독립적으로 계산하고 싶다.
// after
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) { ... }
function calculateTime() { ... }
// before
class Account {
// ...
get bankCharge() {
let result = 4.5;
if (this._daysOverdrawn > 0) result += this.overdraftCharge;
return result;
}
get overdraftCharge() {
if (this.type.isPremium) {
const baseCharge = 10;
if (this.daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (this.daysOverdrawn - 7) * 0.05;
}
} else {
return this.daysOverdrawn * 1.75;
}
}
}
계좌 종류에 따라 이자 책정 알고리즘이 달라지도록 고쳐보자. overdraftCharge()
를 계좌 종류 클래스인 AccountType
으로 옮긴다.
// after
class Account {
// ...
get bankCharge() {
let result = 4.5;
if (this._daysOverdrawn > 0) {
result += this.type.overdraftCharge(this.daysOverdrawn);
}
return result;
}
}
class AccountType {
overdraftCharge(daysOverdrawn) {
if (this.isPremium) {
const baseCharge = 10;
if (daysOverdrawn <= 7) {
return baseCharge;
} else {
return baseCharge + (daysOverdrawn - 7) * 0.85;
}
} else {
return this.daysOverdrawn * 1.75;
}
}
}
프로그램의 진짜 힘은 데이터 구조에서 나온다. 주어진 문제에 적합한 데이터 구조를 선택해야 한다. 현재 데이터 구조가 적절하지 않다면 곧바로 수정해야 한다.
예를 들어, 함수에 항상 함께 건네지는 데이터 조각들은 상호 관계가 명확하게 드러나도록 한 레코드에 담는 게 좋다. 구조체 여러 개에 정의된 똑같은 필드들을 갱신해야 한다면 한 번만 갱신해도 되는 다른 위치로 옮겨야 한다.
레코드 뿐 아니라 클래스나 객체가 와도 마찬가지다. 클래스의 데이터들은 접근자 메서드들 뒤에 감줘져 있으므로 클래스에 곁들여진 함수들은 데이터를 이리저리 옮기는 작업을 쉽게 해준다.
// before
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));
}
}
class CustomerContract {
constructor(startDate) {
this._startDate = startDate;
}
}
discountRate
필드를 Customer
에서 CustomerContract
로 옮기자
// after
class Customer {
constructor(name, discountRate) {
this._name = name;
this._contract = new CustomerContract(dateToday());
this._setDiscountRate(discountRate);
}
get discountRate() {
return this._contract._discountRate;
}
_setDiscountRate(aNumber) {
this._contract._discountRate = aNumber;
}
becomePreferred() {
this._setDiscountRate(this.discountRate + 0.03);
// ...
}
applyDiscount(amount) {
return amount.subtract(amount.multiply(this.discountRate));
}
}
class CustomerContract {
constructor(startDate, discountRate) {
this._startDate = startDate;
this._discountRate = discountRate;
}
get discountRate() {
return this._discountRate;
}
set discountRate(arg) {
this._discountRate = arg;
}
}
중복 제거는 코드를 건강하게 관리하는 가장 효과적인 방법 중 하나다. 코드가 반복되면 피호출 함수로 합친다. 이때 문장들을 함수로 옮기려면 그 문장들이 피호출 함수의 일부라는 확신이 있어야 한다.
// before
function renderPerson(outStream, person) {
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(p) {
return ["<div>", `<p>제목: ${p.title}</p>`, emitPhotoData(p), "</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");
}
// after
function renderPerson(outStream, 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
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>제목: ${photo.title}</p>\n`);
outStream.write(`<p>날짜: ${photo.date.toDateString()}</p>\n`);
outStream.write(`<p>위치: ${photo.location}</p>\n`);
}
renderPerson()
은 그대로 둔 채 listRecentPhotos()
가 위치 정보(location
)을 다르게 렌더링하도록 만들어 보자.
// after
function renderPerson(outStream, person) {
outstream.write(`<p>${person.name}</p>\n`);
renderPhoto(outStream, person.photo);
emitPhotoData(outStream, person.photo);
outStream.write(`<p>위치: ${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`);
}