좋은 소프트웨어 설계의 핵심은 모듈성이다.
어떤 함수가 자신이 속한 모듈 A보다 다른 모듈 B의 요소들을 더 많이 참조한다면 모듈 B로 옮겨줘야 마땅하다.
소프트웨어의 나머지 부분이 모듈 B의 세부사항에 덜 의존하게 된다.
함수를 옮길지 말지 결정하는 법
: 함수의 현재 컨텍스트와 후보 컨텍스트를 둘러보면 도움이 된다.
class Account { get overdraftCharge() { // ... } }
class Account { get overdraftCharge() { return this.type.overdraftCharge() } } class AccountType { get overdraftCharge() { // ... } }
한 레코드를 변경하려 할 때 다른 레코드의 필드까지 변경해야만 한다면 필드의 위치가 잘못되었다는 신호다.
구조체 여러 개에 정의된 똑같은 필드들을 갱신해야 한다면 한 번만 갱신해도 되는 다른 위치로 옮기라는 신호다.
이어지는 설명에서는 클래스를 사용한다고 가정한다.
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 } }
이 리팩터링은 대체로 객체를 활용할 때가 더 수월하다. 캡슐화 덕에 데이터 접근을 메서드로 자연스럽게 감싸주기 때문이다. 반면, 여러 함수가 날 레코드를 직접 사용하는 경우라면 이 리팩터링은 훨씬 까다롭다.
이럴 때는 접근자 함수들을 만들고, 날 레코드를 읽고 쓰는 모든 함수가 접근자를 거치도록 고치면 된다. 옮길 필드가 불변이라면 값을 처음 설정할 때 소스와 타깃 필드를 한꺼번에 갱신하게 하고, 읽기 함수들은 점진적으로 마이그레이션하자.
문장들을 옮기려면 피호출 함수의 일부라는 확신이 있어야 한다.
피호출 함수와 한 몸은 아니지만 여전히 함께 호출돼야 하는 경우라면 단순히 해당 문장들과 피호출 함수를 통째로 또 하나의 함수로 추출한다.
이 방법도 절차는 똑같다. 단 마지막 인라인과 이름 바꾸기 단계만 제외하면 된다.
result.push(`<p>제목: ${person.photo.title}</p>`) result.concat(photoData(person.photo)) function photoData(aPhoto) { return [ `<p>위치: ${aPhoto.location}</p>`, `<p>날짜: ${aPhoto.date.toDateString()}</p>`, `<p>태그: ${aPhoto.tag}</p>`, ] }
result.concat(photoData(person.photo)) function photoData(aPhoto) { return [ `<p>제목: ${aPhoto.title}</p>`, `<p>위치: ${aPhoto.location}</p>`, `<p>날짜: ${aPhoto.date.toDateString()}</p>`, `<p>태그: ${aPhoto.tag}</p>`, ] }
8.3의 반대 리팩터링
여러 곳에서 사용하던 기능이 일부 호출자에게는 다르게 동작하도록 바뀌어야 한다면 함수가 어느 새 둘 이상의 다른 일을 수행하게 바뀔 수 있다.
그렇다면 개발자는 달라진 동작을 함수에서 꺼내 해당 호출자로 옮겨야 한다. 이런 상황에 맞닥뜨리면 우선 문장 슬라이드하기를 적용해 달라지는 동작을 함수의 시작 혹은 끝으로 옮긴 다음, 바로 이어서 문장을 호출한 곳으로 옮기기 리팩터링을 적용하면 된다.
emitPhotoData(outStream, person.photo) function emitPhotoData(outStream, photo) { outStream.write(`<p>제목: ${photo.title}</p>\n`) outStream.write(`<p>위치: ${photo.location}</p>\n`) }
emitPhotoData(outStream, person.photo) outStream.write(`<p>위치: ${person.photo.location}</p>\n`) function emitPhotoData(outStream, photo) { outStream.write(`<p>제목: ${photo.title}</p>\n`) }
똑같은 코드를 반복하는 대신 함수 호출로 변경하는 방법이다.
해당 기능을 하는 함수가 이미 존재한다면 이 방법을 사용하면 된다.
동일한 목적의 같은 코드가 반복 사용되고, 해당 목적의 함수가 존재할 때 같은 코드라도 만약 인라인 코드와 목적이 다르다면 이 방법을 사용하면 안된다.
let appliesToMass = false for (const s of states) { if (s === 'MA') { appliesToMass = true } }
appliesToMass = states.includes('MA')
서로 관련있는 코드가 흩어져있을 때, 한 곳으로 모으는 리팩터링이다.
const pricingPlan = retrievePricingPlan() const order = retrieveOrder() let charge const chargePerUnit = pricingPlan.unit
const pricingPlan = retrievePricingPlan() const chargePerUnit = pricingPlan.unit const order = retrieveOrder() let charge
반복문을 하나의 일만 하는 각각의 반복문으로 쪼개는 리팩터링
만약 이게 병목이라 밝혀지더라도 다시 합치기는 식은 죽 먹기이다.
게다가 그런 경우는 매우 드물고, 반복문 쪼개기는 더 강력한 최적화를 적용할 수 있는 길을 열어준다.
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
반복문을 파이프라인으로 변경하여 논리의 흐름을 쉽게 파악할 수 있다.
const names = [] for (const person of input) { if (person.job === 'programer') { names.push(person.name) } }
const names = input .filter((person) => person.job === 'programer') .map((person) => person.name)
사용하지 않는 코드는 제거한다. 남겨놓으면 복잡하며 나중에 다시 사용하게 된다면 버전 관리 시스템을 이용한다.
if(false) { doSomethingThatUsedToMatter(); }