1. Separate Query from Modifier, 질의 함수와 변경 함수 분리하기
- 하나의 함수내에서 질의와 변경을 동시에 행하고 있다면 분리하자.
절차
- 대상 함수를 복제하고 질의 목적에 충실한 이름을 짓는다.
- 새 질의 함수에서 부수효과를 모두 제거한다.
- 정적 검사를 수행한다.
- 원래 함수(변경 함수)를 호출하는 곳을 모두 찾아낸다. 호출하는 곳에서 반환 값을 사용한다면 질의함수를 호출하도록 바꾸고, 원래 함수를 호출하는 코드를 바로 아래 줄에 새로 추가한다. 하나 수정할때마다 테스트한다.
- 원래 함수에서 질의 관련 코드를 제거한다.
- 테스트.
예시
before
function getTotalOutstandingAndSendBill() {
const result = customer.invoices.reduce((total, each) => each.amount + total, 0);
sendBill();
return result;
}
after
function totalOutstanding() {
return customer.invoices.reduce((total, each) => each.amount + total, 0);
}
function sendBill() {
emailGateway.send(formatBill(customer));
}
2. Parameterize Function, 함수 매개변수화하기
- 두 함수의 로직이 비슷하고 리터럴 값만 다르다면, 이 값을 매개변수로 받아처리한다.
절차
- 비슷한 함수 중 하나를 선택한다.
- 함수 선언 바꾸기로 리터럴들을 매개변수로 추가한다.
- 이 함수를 호출하는 곳 모두에 적절한 리터럴 값을 추가한다.
- 테스트.
- 매개변수로 받은 값을 사용하도록 함수 본문을 수정한다. 하나 수정할 때마다 테스트.
- 비슷한 다른 함수를 호출하는 코드를 찾아 매개변수화된 함수를 호출하도록 하나씩 수정한다. ( 하나씩 수정할때마다 테스트 )
예시
before
function tenPercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.1);
}
function fivePercentRaise(aPerson) {
aPerson.salary = aPerson.salary.multiply(1.05);
}
after
function raise(aPerson, factor) {
aPerson.salary = aPerson.salary.multiply(1 + factory);
}
3. Remove Flag Argument, 플래그 인수 제거하기
- 플래그 인수란 호출되는 함수가 실행할 로직을 호출하는 쪽에서 선택하기 위해 전달하는 인수이다.
- 불리언 타입의 플래그는 코드를 읽는 이에게 뜻을 온전히 전달하지 못하기 때문에 더욱 좋지 못하다.
- 기능 하나를 제공하는 명시적인 함수를 작성하는 것이 깔끔하다.
- 함수 내부에서 매개변수를 분배로직이 까다로울때는 원본 함수를 래핑하는 별도의 함수를 만들 수 도 있다.
절차
- 매개변수로 주어질 수 있는 값 각각에 대응하는 명시적 함수들을 생성한다.
- 원래 함수를 호출하는 코드들을 모두 찾아서 각 리터럴 값에 대응되는 명시적 함수를 호출하도록 수정한다.
예시
before
function setDimension(name, value) {
if (name === 'height') {
this._height = value;
return;
}
if (name === 'width') {
this._width = value;
return;
}
}
after
function setHeight(value) {
this._height = value;
}
function setWidth(value) {
this._width = value;
}
4. Preserve Whole Object,객체 통째로 넘기기
- 하나의 객체에서 값 두 개 이상을 가져와서 인수로 넘기는 코드를 보면, 그 값들 대신 객체 자체를 통째로 넘기자.
- 함수가 객체 자체에 의존하기를 원치 않을 때는 이 리팩터링을 수행하지 않는다.
- 함수와 객체가 서로 다른 모듈에 속한 상황일 때
절차
- 매개변수들을 원하는 형태로 받는 빈 함수를 만든다.
- 새 함수의 본문에서는 원래 함수를 호출하도록 하며, 새 매개변수와 원래 함수의 매개변수를 매핑한다.
- 정적 검사를 수행한다.
- 모든 호출자가 새 함수를 사용하게 수정한다. 하나씩 수정하며 테스트.
- 호출자를 모두 수정했다면 원래 함수를 인라인한다.
- 새 함수의 이름을 적절히 수정하고, 모든 호출자에 반영한다.
예시
before
const low = aRoom.daysTempRange.low
const high = aRoom.daysTempRange.high
if(aPlan.withinRange(low, high))
after
if(aPlan.withinRange(aRoom.daysTempRange))
5. Replace Parameter with Query, 매개변수를 질의 함수로 바꾸기
- 매개변수 목록은 함수의 변동 요인을 모아놓은 곳이다. 함수의 동작에 변화를 줄 수 있는 일차적인 수단이다.
- 피호출 함수가 스스로 쉽게 결정할수 있는 값을 매개변수로 건네는 것도 일종의 중복이다.
- 매개변수를 제거하면서 피호출 함수에 원치 않는 의존성이 생길 때는 매개변수를 제거해서는 안된다.
- 함수는 순수함수로 작성이 되어야한다. 따라서, 매개변수를 없애는 대신 가변 전역 변수를 이용하는 일은 하면 안된다.
절차
- 필요하다면 대상 매개변수의 값을 계산하는 코드를 별도 함수로 추출해놓는다.
- 함수 본문에서 대상 매개변수로의 참조를 모두 찾아서 그 매개변수의 값을 만들어주는 표현식을 참조하도록 바꾼다.
- 함수 선언 바꾸기로 대상 매개변수를 없앤다.
예시
before
availableVacation(anEmployee, anEmployee.grade);
function availableVacation(anEmployee, grade) {
/*...*/
}
after
availableVacation(anEmployee);
function availableVacation(anEmployee) {
const grade = anEmployee.grade;
// ...
}
6. Replace Query with Parameter, 질의 함수를 매개변수로 바꾸기
- 함수 안에 두기 거북한 참조를 발견할 때가 많다. ( 전역 변수를 참조한다거나, 제거하길 원하는 원소를 참조하는 경우 )
- 순수함수를 작성하고, 프로그램의 입출력과 가변 원소들을 다루는 로직으로 순수함수를 감싸는 패턴을 많이 활용한다.
- 결과적으로 이부분은 테스트하거나 다루기가 쉬워진다.
- 질의 함수를 매개변수로 바꾸면 어떤 값을 제공할지를 호출자가 알아내야 한다. 이러면 호출자가 복잡해지는 경우도 발생한다.
- 자바스크립트의 클래스 모델에서는 객체 안의 데이터를 직접 얻어낼 방법이 항상 존재하기 때문에 불변 클래스임을 보장하는 수단이 없다.
- 불변으로 설계했음을 알리고 그렇게 사용하라고 제안하는 것만으로 충분한 값어치를 할 때가 많다.
절차
- 변수 추출하기로 질의 코드를 함수 본문의 나머지 코드와 분리한다.
- 함수 본문 중 해당 질의를 호출하지 않는 코드들을 별도 함수로 추출한다.
- 방금 만든 변수를 인라인하여 제거한다.
- 원래 함수도 인라인한다.
- 새 함수의 이름을 원래 함수의 이름으로 고쳐준다.
예시
before
targetTemperature(aPlan);
function targetTemperature(aPlan) {
currentTemperature = thermostat.currentTemperature;
// ...
}
after
targetTemperature(aPlan, thermostat.currentTemperature);
function targetTemperature(aPlan, currentTemperature) {
// ...
}
7. Remove Setting Method, 세터 제거하기
- 객체 생성 후 수정되지 않기를 원하는 필드라면 세터를 제공하지 말자.
- 아래 2가지 경우에는 이 리팩터링을 진행하는게 좋다.
- 사람들이 접근자 메서드를 통해서만 필드를 다루려 할 때. ( 심지어 생성자 안에서도 )
- 생성 스크립트를 사용하여 객체를 생성할 때.
생성자를 호출한 후 일련의 세터를 호출하여 객체를 완성하는 형태의 코드.
절차
- 설정해야 할 값을 생성자에서 받지 않는다면 그 값을 받을 매개변수를 생성자에 추가한다. 그런 다음 생성자 안에서 적절한 세터를 호출한다.
- 생성자 밖에서 세터를 호출하는 곳을 찾아 제거하고, 대신 새로운 생성자를 사용하도록 한다. 하나씩 수정할 때마다 테스트.
- 세터 메서드를 인라인한다. 가능하다면 해당 필드를 불변으로 만든다.
- 테스트.
예시
before
class Person {
get name() {
/*...*/
}
set name(aString) {
/*...*/
}
}
after
class person {
get name() {
/*...*/
}
}
8. Replace Constructor with Factory Function, 생성자를 팩터리 함수로 바꾸기
- 생성자에는 일반 함수에는 없는 이상한 제약이 따라붙기도 한다.
- 자바의 경우 서브클래스의 인스턴스나 프락시를 반환할 수 없다.
- 생성자를 호출하려면 new 연산자를 써야해서 일반 함수가 오길 기대하는 자리에는 쓰기 어렵다.
절차
- 팩터리 함수를 만든다. 팩터리 함수의 본문에서는 원래의 생성자를 호출한다.
- 생성자를 호출하던 코드를 팩터리 함수 호출로 바꾼다.
- 하나씩 수정할 때마다 테스트.
- 생성자의 가시 범위가 최소가 되도록 제한한다.
예시
before
leadEngineer = new Employee(document.leadEngineer, 'E');
after
// after
leadEngineer = createEngineer(document.leadEngineer);
9. Replace Function with Command, 함수를 명령으로 바꾸기
- 함수를 객체 안으로 캡슐화하면 더 유용해지는 상황이 있다.
- 되돌리기와 같은 보조 연산을 제공할 수 있으며, 수명주기를 더 정밀하게 제어하는 데 필요한 매개변수를 만들어주는 메서드도 제공할 수 있다.
- 일급함수를 지원하지 않는 프로그래밍 언어를 사용할 때는 이를 흉내낼 수 있으며 중첩 함수들을 지원하지 않으면 메서드와 필드를 이용해 함수를 쪼갤 수 있다.
- 자바스크립트는 언어적으로 중첩함수, 일급함수 작성이 가능하니 굳이 이러한 명령 패턴을 사용할 필요는 없을 것 같다.
절차
- 대상 함수의 기능을 옮길 빈 클래스를 만든다. 클래스 이름은 함수 이름에 기초해 짓는다.
- 방금 생성한 빈 클래스로 함수를 옮긴다.
- 함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민해본다.
예시
before
function score(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
// 긴 코드 생략
}
after
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute() {
this._result = 0;
this._healthLevel = 0;
// 긴 코드 생략
}
}
10. Replace Command with Function, 명령을 함수로 바꾸기
- 명령은 그저 함수를 하나 호출해 정해진 일을 수행하는 용도로 주로 쓰인다.
- 로직이 크게 복잡하지 않다면 장점보다 단점이 크니 평범한 함수로 바꿔주는게 낫다.
절차
- 명령을 생성하는 코드와 명령의 실행 메서드를 호출하는 코드를 함께 함수로 추추루한다.
- 명령의 실행 함수가 호출하는 보조 메서드들 각각을 인라인한다.
- 함수 선언 바꾸기를 적용하여 생성자의 매개변수 모두를 명령의 실행 메서드로 옮긴다.
- 명령의 실행 메서드에서 참조하는 필드들 대신 대응하는 매개변수를 사용하게끔 바꾼다. 하나씩 수정할 때마다 테스트.
- 생성자 호출과 명령의 실행 메서드 호출을 호출자 안으로 인라인한다.
- 테스트.
- 죽은 코드 제거하기로 명령 클래스를 없앤다.
예시
before
class ChargeCalculator {
constructor(customer, usage) {
this._customer = customer;
this._usage = usage;
}
execute() {
return this._customer.rate * this._usage;
}
}
after
function charge(customer, usage) {
return customer.rate * usage;
}
11. Return Modified Value, 수정된 값 반환하기
- 데이터가 수정된다면 그 사실을 명확히 알려주어서 함수가 무슨 일을 하는지 쉽게 알 수 있게 하는 것이 중요하다.
- 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담아두도록 하는 것이다.
절차
- 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장하게 한다.
- 테스트.
- 피호출 함수 안에 반환할 값을 가리키는 새로운 변수를 선언한다.
- 테스트.
- 계산이 선언과 동시에 이뤄지도록 통합한다.
- 테스트.
- 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꿔준다.
- 테스트.
예시
before
let totalAscent = 0;
calculateAscent();
function calculateAscent() {
for (let i = 1; i < points.length; i++) {
const verticalCharge = points[i].elevation - points[i - 1].elevation;
totalAscent += verticalChange > 0 ? verticalCharge : 0;
}
}
after
const totalAscent = calculateAscent();
function calculateAscent() {
let result = 0;
for (let i = 1; i < points.length; i++) {
const verticalCharge = points[i].elevation - points[i - 1].elevation;
result += verticalChange > 0 ? verticalCharge : 0;
}
return result;
}
12. Replace Error Code with Exception, 오류 코드를 예외로 바꾸기
- 예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 메커니즘이다.
절차
- 콜스택 상위에 해당 예외를 처리할 예외 핸들러를 작성한다.
- 테스트.
- 해당 오류 코드를 대체할 예외와 그 밖의 예외를 구분할 식별 방법을 찾는다.
- 정적 검사를 수행한다.
- catch절을 수정하여 직접 처리할 수 있는 예외는 적절히 대처하고 그렇지 않은 예외는 다시 던진다.
- 테스트.
- 오류 코드를 반환하는 곳 모두에서 예외를 던지도록 수정한다. 하나씩 수정할 때마다 테스트한다.
- 모두 수정했다면 그 오류 코드를 콜스택 위로 전달하는 코드를 모두 제거한다. 하나씩 수정할 때마다 테스트한다.
예시
before
if (data) return new ShippingRules(data);
else return -23;
after
if (data) return new ShippingRules(data);
else return new OrderProcessingError(-23);
13. Replace Exception with Precheck, 예외를 사전확인으로 바꾸기
- 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면 예외를 던지는 대신 호출하는 곳에서 조건을 검사해야 한다.
절차
- 예외를 유발하는 상황을 검사할 수 있는 조건문을 추가한다. catch 블록의 코드를 조건문의 조건절 중 하나로 옮기고남은 try 블록의 코드를 다른 조건절로 옮긴다.
- catch 블록에 어서션을 추가하고 테스트.
- try문과 catch 블록을 제거.
- 테스트.
예시
before
double getValueForPeriod(int periodNumber) {
try {
return values[periodNumber]
} catch (ArrayIndexOutOfBoundsException e) {
return 0
}
}
after
double getValueForPeriod(int periodNumber) {
return periodNumber >= values.length ? 0 : values[periodNumber]
}