11.7 세터 제거하기
- 세터 메서드가 있다는 것은 필드가 수정될 수 있다는 뜻. 객체 생성 후에는 수정되지 않길 원하는 필드라면 세터를 제공하지 않았을 것
- 세터 제거하기 리팩터링이 필요한 상황은 주로 두 가지임
- 첫째, 사람들이 무조건 접근자 메서드를 통해서만 필드를 다루려 할 때
- 두 번째는 클라이언트에서 생성 스크립트를 사용해 객체를 생성할 때
절차
- 설정해야 할 값을 생성자에서 받지 않는다면 그 값을 받을 매개변수를 생성자에 추가함. 그런 다음 생성자 안에서 적절한 세터를 호출
- 생성자 밖에서 세터를 호출하는 곳을 찾아 제거하고, 대신 새로운 생성자를 사용하도록 함
- 세터 메서드를 인라인. 가능하다면 해당 필드를 불변으로 만듦
예시
// before
class Person {
// ...
get name() {
return this._name;
}
set name(arg) {
this._name = arg;
}
get id() {
return this._id;
}
set id(arg) {
this._id = arg;
}
}
const martin = new Person();
martin.name = '마틴';
margin.id = '1234';
// id 필드는 객체를 생성한 뒤에 변경되면 안 됨.
// 생성자를 통해 id 를 설정하게끔 수정
// after
class Person {
constructor(id) {
this.id = id;
}
// ...
}
const martin = new Person('1234');
martin.name = '마틴';
11.8 생성자를 팩터리 함수로 바꾸기
- 생성자에는 이상한 제약이 따라붙기도 함.
- 자바 생성자는 반드시 생성자를 정의한 클래스의 인스턴스를 반환해야 함.
- 생성자의 이름도 고정되며, 생성자를 호출하려면 특별한 연산자(new)를 사용해야 함
-> 팩터리 함수에는 이런 제약이 없다.
절차
- 팩터리 함수를 만듦. 팩터리 함수의 본문에서는 원래의 생성자를 호출함
- 생성자를 호출하던 코드를 팩터리 함수 호출로 바꿈
- 하나씩 수정할 때마다 테스트
- 생성자의 가시 범위가 최소가 되도록 제한
예시
// before
class Employee {
constructor(name, typeCode) {
this._name = name;
this._typeCode = typeCode;
}
get name() {
return this._name;
}
get type() {
return Employee.legalTypeCodes[this._typeCode];
}
static get legalTypeCodes() {
return { E: 'Engineer', M: 'Manager', S: 'Salesperson' };
}
}
const leadEngineer = new Employee(document.leadEngineer, 'E');
// afters
function createEmployee(name) {
return new Employee(name, 'E');
}
const leadEngineer = createEmployee(document.leadEngineer);
11.9 함수를 명령으로 바꾸기
- 함수를 그 함수만을 위한 객체 안으로 캡슐화하면 더 유용해지는 상황이 있음
- 이런 객체를 가리켜 ‘명령 객체’ 혹은 단순히 ‘명령’이라 함
- 명령 객체 대부분은 메서드 하나로 구성되며, 이 메서드를 요청해 실행하는 것이 이 객체의 목적
- 명령은 평범한 함수 메커니즘보다 훨씬 유연하게 함수를 제어하고 표현할 수 있음
- 그러나 명령을 사용해 유연성을 얻더라도 복잡성이 커질 수 있음
- 명령보다 더 간단한 방식으로는 얻을 수 없는 기능이 필요할 때만 명령을 선택
절차
- 대상 함수의 기능을 옮길 빈 클래스를 만듦. 클래스 이름은 함수 이름에 기초해 지음
- 방금 생성한 빈 클래스로 함수를 옮김
- 함수의 인수들 각각은 명령의 필드로 만들어 생성자를 통해 설정할지 고민해봄
예시
// before
function score(candidate, medicalExam, scoringGuide) {
let result = 0;
let healthLevel = 0;
let highMedicalRiskFlag = false;
if (medicalExam.isSmoker) {
healthLevel += 10;
highMedicalRiskFlag = true;
}
let certificationGrade = 'regular';
if (scoringGuide.stateWithLowCertification(candidate.originState)) {
certificationGrade = 'low';
result -= 5;
}
// ...
result -= Math.max(healthLevel - 5, 0);
return result;
}
// after
function score(candidate, medicalExam, scoringGuide) {
return new Scorer(candidate, medicalExam, scoringGuide).execute();
}
class Scorer {
constructor(candidate, medicalExam, scoringGuide) {
this._candidate = candidate;
this._medicalExam = medicalExam;
this._scoringGuide = scoringGuide;
}
execute(medicalExam, scoringGuide) {
this._result = 0;
this._healthLevel = 0;
this._highMedicalRiskFlag = false;
this.scoreSmoking();
this._certificationGrade = 'regular';
if (this._scoringGuide.stateWithLowCertification(this._candidate.originState)) {
this._certificationGrade = 'low';
this._result -= 5;
}
// ...
result -= Math.max(this._healthLevel - 5, 0);
return result;
}
scoreSmoking() {
if (this._medicalExam.isSmoker) {
this._healthLevel += 10;
this._highMedicalRiskFlag = true;
}
}
}
11.10 명령을 함수로 바꾸기
- 명령 객체는 복잡한 연산을 다룰 수 있는 강력한 메커니즘을 제공하지만, 로직이 크게 복잡하지 않다면 명령 객체는 평범한 함수로 바꿔주는 게 나음
절차
- 명령을 생성하는 코드와 명령의 실행 메서드를 호출하는 코드를 함께 함수로 추출함
- 명령의 실행 함수가 호출하는 보조 메서드 각각을 인라인함
- 함수 선언 바꾸기를 적용하여 생성자의 매개변수 모두를 명령의 실행 메서드로 옮김
- 명령의 실행 메서드에서 참조하는 필드들 대신 대응하는 매개변수를 사용하게끔 바꿈
- 생성자 호출과 명령의 실행 메서드 호출을 호출자(대체 함수) 안으로 인라인함
- 죽은 코드 제거하기로 명령 클래스를 없앰
예시
// before
class ChargeCalculator {
constructor(customer, usage, provider) {
this._customer = customer;
this._usage = usage;
this._provider = provide;
}
get baseCharge() {
return this._customer.baseRate * this._usage;
}
get charge() {
return this.baseCharge + this._provider.connectionCharge;
}
}
const monthCharge = new ChargeCalculator(customer, usage, provider).charge;
// after
function charge(customer, usage, provider) {
const baseCharge = customer.baseRate * usage;
return baseCharge + provider.connectionCharge;
}
const monthCharge = charge(customer, usage, provider);
11.11 수정된 값 반환하기
- 데이터가 수정된다면 그 사실을 명확히 알려주어서, 어느 함수가 무슨 일을 하는지 쉽게 알 수 있게 하는 일이 대단히 중요
- 데이터가 수정됨을 알려주는 방법 중 하나는, 변수를 갱신하는 함수라면 수정된 값을 반환하여 호출자가 그 값을 변수에 담아두는 것
절차
- 함수가 수정된 값을 반환하게 하여 호출자가 그 값을 자신의 변수에 저장
- 피호출 함수 안에 반환할 값을 가리키는 새로운 변수를 선언
- 계산이 선언과 동시에 이뤄지도록 통합(즉, 선언 시점에 계산 로직을 바로 실행해 대입)
- 피호출 함수의 변수 이름을 새 역할에 어울리도록 바꿈
// before
let totalAscent = 0;
calculateAscent();
function calculateAscent() {
for (let i = 1; i < points.length; i++) {
const verticalChange = points[i].elevation - points[i - 1].elevation;
totalAscent += verticalChange > 0 ? verticalChange : 0;
}
}
// calculateAscent() 안에서 totalAscent 가 갱신된다는 사실이 드러나지 않음
// after
const totalAscent = calculateAscent();
function calculateAscent() {
let result = 0;
for (let i = 1; i < points.length; i++) {
const verticalChange = points[i].elevation - points[i - 1].elevation;
result += verticalChange > 0 ? verticalChange : 0;
}
return result;
}
11.12 오류 코드를 예외로 바꾸기
- 예외는 프로그래밍 언어에서 제공하는 독립적인 오류 처리 메커니즘으로, 예외가 던져지면 적절한 예외 핸들러를 찾을 때까지 콜스택을 타고 위로 전파됨
- 예외는 프로그램의 정상 동작 범주에 들지 않는 오류를 나타낼 때만 쓰여야 함
- 예외를 던지는 코드를 프로그램 종료 코드로 바꿔도 프로그램은 여전히 정상 동작 해야함
- 정상 동작하지 않을 것 같다면, 예외 대신 오류를 검출하여 프로그램을 정상 흐름으로 되돌리게끔 처리해야 함
절차
- 콜스택 상위에 해당 예외를 처리할 예외 핸들러를 작성
- 해당 오류 코드를 대체할 예외와 그 밖의 예외를 구분할 식별 방법을 찾음
- 정적 검사를 수행
catch
절을 수정하여 직접 처리할 수 있는 예외는 적절히 대처하고, 그렇지 않은 예외는 다시 던짐. 오류 코드를 반환하는 곳 모두에서 예외를 던지도록 수정- 모두 수정했다면 그 오류 코드를 콜스택 위로 전달하는 코드를 모두 제거
예시
// before
function localShippingRules(country) {
const data = countryData.shippingRules[country];
if (data) return new ShippingRules(data);
else return -23;
}
// before
// 이 코드는 국가 정보(country)가 유효한지를 이 함수 호출 전에 다 검증했다고 가정
// 이 함수에서 오류가 난다면 무언가 잘못됐음을 뜻함
function calculateShippingCosts(anOrder) {
// ...
const shippingRules = localShippingRules(anOrder.country);
if (shippingRules < 0) return shippingRules; // 오류 전파
}
const status = calculateShippingCosts(orderData);
if (status < 0) errorList.push({ order: orderData, errorCode: status });
// after
function localShippingRules(country) {
const data = countryData.shippingRules[country];
if (data) return new ShippingRules(data);
else throw new OrderProcessingError(-23);
}
function calculateShippingCosts(anOrder) {
// ...
const shippingRules = localShippingRules(anOrder.country);
}
try {
calculateShippingCosts(orderData);
} catch (e) {
if (e instanceof OrderProcessingError) {
errorList.push({ order: orderData, errorCode: e.code });
} else {
throw e;
}
}
class OrderProcessingError extends Error {
constructor(errorCode) {
super(`주문 처리 오류: ${errorCode}`);
this.code = errorCode;
}
get name() {
return 'OrderProcessingError';
}
}
11.13 예외를 사전확인으로 바꾸기
- 함수 수행 시 문제가 될 수 있는 조건을 함수 호출 전에 검사할 수 있다면, 예외를 던지는 대신 호출하는 곳에서 조건을 검사하도록 해야 함
절차
- 예외를 유발하는 상황을 검사할 수 있는 조건문을 추가.
catch
블록의 코드를 조건문의 조건절 중 하나로 옮기고, 남은try
블록의 코드를 다른 조건절로 옮김 catch
블록에 어서션을 추가하고 테스트try
문과catch
블록을 제거
예시(Java)
// ResourcePool 클래스.
public Resource get() {
Resource result;
try {
result = available.pop();
allocated.add(result);
} catch (NoSuchElementException e) {
result = Resource.create();
allocated.add(result);
}
return result;
}
private Deque<Resource> available;
private List<Resource> allocated;
- 풀에서 자원이 고갈되는 건 예상치 못한 조건이 아님. 사용하기 전에
allocated
컬렉션의 상태를 쉽게 확인할 수 있음