8. 기능 이동
8.1 함수 옮기기
배경
- 좋은 소프트웨어 설계의 핵심은 모듈화가 얼마나 잘 되어 있느냐를 뜻하는 모듈성 이다
- 모듈성을 높이려면 서로 연관된 요소들을 함께 묶고, 요소 사이의 연결 관계를 쉽게 찾고 이해할 수 있도록 해야 한다.
- 어떤 함수가 자신이 속한 모듈 A의 요소들보다 다른 모듈 B의 요소들을 더 많이 참조한다면 모듈 B로 옮겨줘야 마땅하다 (캡슐화 측면에서, 의존성을 최대한 제거)
절차
- 선택한 함수가 현재 컨텍스트에서 사용 중인 모든 프로그램 요소를 살펴본다. 이 요소들 중에도 함께 옮겨야 할 게 있는지 고민해본다.
- 얽혀 있는 함수가 여러 개라면 다른 곳에 미치는 영향이 적은 함수부터 옮기자
- 선택한 함수가 다형 메서드인지 확인한다.
- 선택한 함수를 타깃 컨텍스트로 복사한다. 타깃 함수가 새로운 터전에 잘 자리 잡도록 다듬는다.
- 소스 컨텍스트에서 타깃 함수를 참조할 방법을 찾아 반영한다.
- 소스 함수를 타깃 함수의 위임 함수가 되도록 수정한다.
- 소스 함수를 인라인할지 고민해본다.
예시
class Account {
get overdraftCharge() {...}
}
-----------------------------------
class AccountType {
get overdraftCharge() {...}
}
8.2 필드 옮기기
배경
- 주어진 문제에 적합한 데이터 구조를 활용하면, 동작 코드는 자연스럽게 단순하고 직관적으로 짜여진다.
- 데이터 구조가 이상하다 싶으면 바로 수정해야한다. 지금 수정 안하면 나중에 더 큰 스노우볼이 굴려질 수 있다.
- 함수에 항상 함께 건네지는 데이터 조각들은 상호 관계가 명확하게 드러나도록 한 레코드에 담는 게 가장 좋다.
절차
- 소스 필드가 캡슐화되어 있지 않다면 캡슐화한다.
- 타깃 객체에 필드를 생성한다.
- 소스객체에서 타깃 객체를 참조할 수 있는지 확인한다.
- 접근자들이 타깃 필드를 사용하도록 수정한다
- 소스필드를 제거한다.
예시
class Customer {
get plan() {return this._plan}
get discountRate() {return this._discountRate;}
}
--------------------------------
class Customer {
get plan() {return this._plan}
get discountRate() {return this._plan._discountRate;}
}
8.3 문장을 함수로 옮기기
배경
- 중복 제거는 코드를 건강하게 관리하는 가장 효과적인 방법 중 하나다.
- 추후 반복되는 부분에서 무언가 수정할 일이 생겼을 때 한 곳만 수정하면 되기 때문이다.
절차
- 반복 코드가 함수 호출 부분과 멀다면 문장 슬라이드하기를 적용해 근처로 옮긴다.
- 타깃 함수를 호출하는 곳이 한 곳뿐이면, 단순히 소스 위치에서 해당 코드를 잘라내어 피호출 함수로 복사한다. 이 경우라면 나머지 단계는 무시한다.
- 호출자가 여러 곳이면, 호출자 중 하나에서 타깃 함수 호출 부분과 그 함수로 옮기려는 문장들을 함께 다른 함수로 추출한다.
- 다른 호출자 모두가 방금 추출한 함수를 사용하도록 수정한다.
- 모든 호출자가 새로운 함수를 사용하게 되면 원래 함수를 새로운 함수 안으로 인라인한 후 원래 함수를 제거한다.
- 새로운 함수의 이름을 원래 함수의 이름으로 바꿔준다.
예시
result.push(`제목: ${person.photo.title}`);
result.concat(photoData(person.photo));
function photoData(aPhoto) {
return [
`위치: ${aPhoto.location}`,
`날짜: ${aPhoto.date.toDateString()}`
];
}
------------------------------
result.concat(photoData(person.photo));
function photoData(aPhoto) {
return [
`위치: ${aPhoto.location}`,
`날짜: ${aPhoto.date.toDateString()}`,
`제목: ${aPhoto.photo.title}`
];
}
8.4 문장을 호출한 곳으로 옮기기
배경
- 함수는 프로그래머가 쌓아 올리는 추상화의 기본 빌딩 블록이다.
- 추상화라는 것은 그 경계를 항상 올바르게 긋기가 어렵다. 그래서 코드베이스의 기능 범위가 달라지면 추상화의 경계도 움직이게 된다.
- 작은 변경이라면 문장을 호출한 곳으로 옮기는 것으로 충분하지만, 호출자와 호출 대상의 경계를 완전히 다시 그어야 할 때도 있다.
절차
- 호출자가 한두 개뿐이고 피호출 함수도 간단한 단순한 상황이면, 피호출 함수의 처음줄을 잘라내어 호출자로 복사해 넣는다.
- 더 복잡한 상황에서는, 이동하지 않길 원하는 모든 문장을 함수로 추출한 다음 검색하기 쉬운 임시 이름을 지어준다.
- 원래 함수를 인라인 한다
- 추출된 함수의 이름을 원래 함수의 이름으로 변경한다.
예시
emitPhotoData(outStream, person.photo);
function emitPhotoData(outStream, photo) {
outStream.write(`제목: ${photo.title}`);
outStream.write(`위치: ${photo.location}`);
}
-----------------------------------------
emitPhotoData(outStream, person.photo);
outStream.write(`위치: ${person.photo.location}`);
function emitPhotoData(outStream, photo) {
outStream.write(`제목: ${photo.title}`);
}
8.5 인라인 코드를 함수 호출로 바꾸기
배경
- 함수는 여러 동작을 하나로 묶어준다.
- 함수의 이름이 코드의 동작 방식보다는 목적을 말해주기 때문에 함수를 활용하면 코드를 이해하기가 쉬워진다.
- 함수는 중복을 없애는 데도 효과적이다.
절차
- 인라인 코드를 함수 호출로 대체한다.
예시
let appliesToMass = false;
for (const s of states) {
if (s === "MA") appliesToMass = true;
}
------------------------------
appliesToMass = state.includes("MA")
8.6 문장 슬라이드하기
배경
- 관련된 코드들이 가까이 모여 있다면 이해하기가 더 쉽다.
- 하나의 데이터 구조를 이용하는 문장들은 한데 모여 있어야 좋다.
절차
- 코드 조각을 이동할 목표 위치를 찾는다. 코드 조각의 원래 위치와 목표 위치 사이의 코드들을 훑어보면서, 조각을 모으고 나면 동작이 달라지는 코드가 있는지 살핀다.
- 코드 조각을 원래 위치에서 잘라내어 목표 위치에 붙여 넣는다.
예시
- 코드 조각을 슬라이드할 때는 두 가지를 확인해야한다.
- 무엇을 슬라이드 할지
- 슬라이드할 수 있는지
- 코드 조각을 슬라이드하기로 했다면, 그 다음 단계로는 그 일이 실제로 가능한지를 점검해야 한다.
- 슬라이드할 코드 자체와 그 코드가 건너뛰어야 할 코드를 모두 살펴야한다.
const pricingPlan = retrievePricingPlan();
const order = retrieveOrder();
let charge;
const chargePerUnit = pricingPlan.unit;
----------------------------------------
const pricingPlan = retrievePricingPlan();
const chargePerUnit = pricingPlan.unit;
const order = retrieveOrder();
let charge;
8.7 반복문 쪼개기
배경
- 두가지 일을 수행하는 하나의 반복문에서 수정이 필요할때, 각각의 반복문으로 분리해두면 수정할 동작 하나만 이해하면 된다.
- 반복문을 분리하면 사용하기도 쉬워진다.
- 반복문 쪼개기는 서로 다른 일들이 한 함수에서 이뤄지고 있다는 신호일 수 있고, 반복문 쪼개기와 함수 추출하기는 연이어 수행하는 일이 잦다
절차
- 반복문을 복제해 두 개로 만든다.
- 반복문이 중복되어 생기는 부수효과를 파악해서 제거한다.
- 테스트한다.
- 완료되면, 각 반복문을 함수로 추출할지 고민해본다.
예시
let averageAge = 0;
let totalSalary = 0;
for (const p of people) {
averageAge += p.age;
totalSalary += p.salary;
}
averageAge = averageAge / people.length;
----------------------------------
let totalSalary = 0;
for (const p of people) {
totalSalary += p.salary;
}
let averageAge = 0;
for (const p of people) {
averageAge += p.age;
}
averageAge = averageAge / people.length;
8.8 반복문을 파이프라인으로 바꾸기
배경
- 컬렉션 파이프라인을 이용하면 처리 과정을 일련의 연산으로 표현할 수 있다.
- 논리를 파이프라인으로 표현하면 이해하기 훨씬 쉬워진다.
- 객체가 파이프라인을 따라 흐르며 어떤 흐름으로 처리되는지 읽기 편해지기 떄문이다
절차
- 반복문에서 사용하는 컬렉션을 가리키는 변수를 하나 만든다.
- 반복문의 첫 줄부터, 각각의 단위 행위를 적절한 컬렉션 파이프라인 연산으로 대체한다.
- 반복문의 모든 동작을 대체했다면 반복문 자체를 지운다.
예시
const names = [];
for (const i of input) {
if (i.job === "programmer")
names.push(i.name)
}
--------------------------------------------------------
const names = input
.filter(i => i.job === "programmer")
.map(i => i.name)
8.9 죽은 코드 제거하기
배경
- 최신 컴파일러들은 안쓰는 코드를 알아서 제거해주긴하지만, 소프트웨어 동작을 이해하는 데 걸림돌이 될 수 있으니 제거하는것이 좋다.
절차
- 죽은 코드를 외부에서 참조할 수 있는 경우라면 혹시라도 호출하는 곳이 있는지 확인한다.
- 없다면 죽은 코드를 제거한다.
'핌팩토링' 카테고리의 다른 글
10. 조건문 로직 간소화 (0) | 2022.04.28 |
---|---|
9. 데이터 조직화 (0) | 2022.04.28 |
7. 캡슐화 (0) | 2022.04.27 |
6. 기본적인 리팩터링 (0) | 2022.03.26 |
4. 테스트 구축하기 (0) | 2022.03.12 |