리팩터링 2판의 Chatper 09를 보고 정리한 글입니다.
데이터 구조는 프로그램에서 중요한 역할을 한다. 이 장에서는 데이터 구조에 집중한 리팩터링에 대해서 다룬다.
나중에 쉽게 참조하려는 목적으로 쓰인 변수에는 값을 단 한번만 대입해야 한다. 대입이 두 번 이상 이뤄진다면 여러가지 역할을 수행한다는 신호이다.
역할이 둘 이상인 변수가 있다면 코드를 읽는 이에게 혼란을 주기 때문에 쪼개자.
리팩터링 전
음식이 다른 지역으로 전파된 거리를 구하는 코드
function distanceTravelled(scenario, time) {
let result;
let acc = scenario.primaryForce / scenario.mass; // 가속도(a) = 힘(F) / 질량(m)
const primaryTime = Math.min(time, scenario.delay);
result = 0.5 * acc * primaryTime * primaryTime; // 전파된 거리
const secondaryTime = time - scenario.delay;
if (secondaryTime > 0) { // 두 번째 힘을 반영해 다시 계산
const primaryVelocity = acc * scenario.delay;
acc = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
result += primaryVelocity * secondaryTime + 0.5 * acc * secondaryTime * secondaryTime;
}
return result;
}
이 코드를 자세히보면, acc 변수에 값이 두 번 대입되는 것을 볼 수 있다. 하나는 첫 번째 힘이 유발한 초기 가속도를 저장하는 역할이고, 다른 하나는 두 번째 힘까지 반영된 후의 가속도를 저장하는 역할이다.
→ 쪼개야 할 변수다.
리팩터링 후
function distanceTravelled(scenario, time) {
let result;
const primaryAcceleration = scenario.primaryForce / scenario.mass;
const primaryTime = Math.min(time, scenario.delay);
result = 0.5 * primaryAcceleration * primaryTime * primaryTime;
const secondaryTime = time - scenario.delay;
if (secondaryTime > 0) {
const primaryVelocity = primaryAcceleration * scenario.delay;
// 여기 원래의 acc까지 1차 리팩터링
const secondaryAcceleration = (scenario.primaryForce + scenario.secondaryForce) / scenario.mass;
result += primaryVelocity * secondaryTime + 0.5 * secondaryAcceleration * secondaryTime * secondaryTime;
}
// 이후 2차 리팩터링
return result;
}
이 예제 말고 입력 매개변수의 값을 수정하는 예제도 나오는데, 입력 값에 기초하여 코드의 명확한 목적을 더 잘 드러내는 코드로 리팩터링하는 예제이다.
리팩터링 전
function discount(inputValue, quantity) {
if (inputValue > 50) inputValue -= 2;
if (quantity > 100) inputValue -= 1;
return inputValue;
}
리팩터링 후
function discount(inputValue, quantity) {
let result = inputValue;
if (inputValue > 50) result -= 2;
if (quantity > 100) result -= 1;
return result;
}
레코드 구조체의 필드 이름과 같은 데이터 구조는 프로그램을 이해하는 데 큰 역할을 한다.
클래스의 게터와 세터 이름 바꾸기도 사용자 입장에서 필드와 다름 없어서 레코드 구조체의 필드 이름 바꾸기와 똑같이 중요하다.
리팩터링 전
const organization = { name: '애크미 구스베리', country: 'GB' };
이 상수의 '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; }
}
class Organization {
constructor(data) {
this._title = (data.title !== undefined) ? data.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: '애크미 구스베리', country: 'GB' });
가변 데이터는 연쇄 효과를 일으켜 다른 쪽 코드에 원인을 찾기 어려운 문제를 야기한다.
→ 저자는 가변 데이터의 유효 범위를 가능한 한 좁혀야 한다고 주장한다.
단, 새로운 데이터 구조를 생성하는 변형 연산과 같이 계산 결과가 일정한 불변인 경우를 제외하고.
리팩터링 전
class ProductionPlan {
...
get production() { return this._production; }
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
this._production += anAdjustment.amount;
}
}
리팩터링 후
책에서는 어서션을 추가해보고, 실패하지 않으면 코드를 수정하여 계산 결과를 직접 반환받도록 리팩터링이 진행된다.
class ProductionPlan {
...
get production() { return this._adjustments.reduce((sum, a) => sum + a.amount, 0); }
applyAdjustment(anAdjustment) {
this._adjustments.push(anAdjustment);
}
}
반대 리팩터링: 값을 참조로 바꾸기
객체(데이터 구조)를 다른 객체(데이터 구조)에 중첩하면 내부 객체를 참조 혹은 값으로 취급할 수 있다.
→ 불변을 원한다면 값으로 취급하는게 맞다.
그러나 이러한 리팩터링은 특정 객체를 여러 객체에서 공유하고자 한다면, 참조로 다뤄야하기 때문에 맞지 않다.
리팩터링 전
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 {
get areaCode() {return this._areaCode;}
set areaCode(arg) {this._areaCode = arg;}
get number() {return this._number;}
set number(arg) {this._number = arg;}
}
리팩터링 후
전화번호를 불변으로 만들고, 필드들의 세터를 제거하자.
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(areaCode, number) {
this._areaCode = areaCode;
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;}
}
이후 동치성을 값 기반으로 평가하여 동치성 검사를 수행하자.
반대 리팩터링: 참조를 값으로 바꾸기
논리적으로 같은 데이터를 물리적으로 복제해 사용할 때 가장 크게 문제되는 상황은 데이터를 갱신해야 할 때다.
→ 이런 상황이라면 복제된 데이터들을 모두 참조로 바꿔주는게 좋다.
리팩터링 전
class Order {
constructor(data) {
this._number = data.number;
this._customer = new Customer(data.customer);
}
get customer() {return this._customer;}
}
class Customer {
constructor(id){
this._id = id;
}
get id() {return this._id;}
}
현재는 고객이 값이고, 주문 다섯 개를 생성한다면 독립된 고객 객체가 다섯 개 만들어진다. 이를 참조로 바꾸어 물리적으로 똑같은 고객 객체를 사용하도록 만들어보자.
리팩터링 후
저자는 저장소 객체를 사용하여 리팩터링함
let _repositoryData;
export function initialize() {
_repositoryData = {};
_repositoryData.customers = new Map();
}
export function registerCustomer() {
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.customer);
// 다른 데이터를 읽어 들인다.
}
get customer() {return this._customer}
}
위 repository는 ID 하나당 오직 고객 객체만 생성됨을 보장한다. 이를 통해 같은 고객을 공유하는 주문 모두에서 갱신된 데이터를 사용하게 된다.
매직 리터럴 = 소스 코드에 등장하는 일반적인 리터럴 값
이러한 리터럴 값은 코드를 읽는사람이 의미를 모르는경우 가독성을 방해하며 코드 자체의 뜻을 분명하게 드러내는 게 좋다.
리팩터링 전
function potentialEnergy(mass, height) {
return mass * 9.81 * height;
}
리팩터링 후
const STANDARD_GRAVITY = 9.81;
function potentialEnergy(mass, height) {
return mass * STANDARD_GRAVITY * height;
}