Refactoring
Chapter 7. 캡슐화
레코드 캡슐화하기
변수 조작 방식 통제를 위함
레코드는 연관된 여러 데이터를 직관적인 방식으로 묶을 수 있어서, 따로 취급할 때보다 의미 있는 단위로 전달할 수 있게 해준다.
캡슐화에서는 값을 수정하는 부분을 명확하게 드러내고 한 곳에 모아두는 일이 중요
가변 데이터를 저장할 경우 레코드보다 객체를 선호
- 어떻게 저장했는지 숨기고, 각 값을 메서드로 제공
개요
?. Map type -> Class type
Before
const organization = { name: '애크미 구스베리', country: 'GB' };
After
class Organization {
constructor(data) {
this._name = data.name;
this._country = data.country;
}
get name() { return this._name; }
set name(arg) { this._name = arg; }
get country() { return this._country; }
set country(arg) { this._country = arg; }
}
절차
- 레코드를 담은 변수 캡슐화 (변수를 반환하는 함수)
- 레코드를 새로운 클래스로 정의 (Getter/Setter 생성)
- 테스트
- 새로 정의한 클래스 객체를 반환하는 함수 만들기
- 기존 레코드 반환 코드를 새 함수로 변경
- 클래스에서 원본 데이터 반환 접근자와 원본 레코드 반환 함수 제거
- 테스트
- 레코드 필드도 중첩 구조라면 레코드 캡슐화하기와 컬렉션 캡슐화하기를 재귀적으로 적용
컬렉션 캡슐화하기
Collection 변수 접근을 캡슐화하면서 Getter가 Collection 자체를 반환한다면, 원본이 변경될 수 있다.
- add(), remove() 라는 Collection 변경자 메서드를 만들자.
Collection Getter 를 제공하되 내부 Collection의 복제본을 반환하자.
Collection 을 다루는 클래스는 불필요한 복제본을 만드는 편이 예상치 못한 수정으로 인한 오류를 디버깅하는 것보다 낫다.
- 컬렉션 관리를 책임지는 클래스라면 항상 복제본을 제공하자.
개요
?. 컬렉션 관리는 복제본으로
Before
class Person {
constructor(name) {
this._name = name;
this._courses = [];
}
get name() { return this._name; }
get courses() { return this.courses; }
set courses(aList) { this._courses = aList; }
}
After
class Person {
constructor(name) {
this._name = name;
this._courses = [];
}
get name() { return this._name; }
get courses() { return this.courses.slice(); }
addCourse(aCourse) { this._courses.push(aCourse); }
removeCourse(aCourse, fbIfAbsent = () => { throw new RangeError();} {
const index = this._courses.indexOf(aCourse);
if (index === -1) fbIfAbsent();
else this._courses.splice(index, 1);
}
set courses(aList) { this._courses = aList.slice(); }
}
절차
- 컬렉션 캡슐화가 되어있지 않다면 ‘변수 캡슐화하기’ 진행
- 컬렉션에 원소를 추가/제거하는 함수 추가
- 컬렉션 자체를 통째로 바꾸는 Setter 는 제거하자.
- 정적 검사
- 기존 컬렉션 참조 코드를 추가/제거 함수 호출로 변경 및 테스트
- 컬렉션 Getter는 원본 내용을 수정할 수 없는 읽기 전용 프락시나 복제본 반환
- 테스트
기본형을 객체로 바꾸기
단순 출력 이상의 기능이 필요해지면 데이터를 표현하는 전용 클래스를 정의하자.
개요
Before
orders.filter((o) => 'high' === o.priority
|| 'rush' === o.priority);
After
orders.filter((o) => o.priority.higherThan(new Priority('normal')));
절차
- 변수 캡슐화하기
- 단순한 값 클래스 만들기
- 기본 생성자와 Getter/Setter 추가
- 정적 검사 수행
- Getter/Setter 수정
- 테스트
- 함수 이름 검토
Example
class Order {
constructor(data) {
this._priority = data.priority;
}
// 1, 2
get priority() { return this._priority; }
get priorityString() { return this._priority.toString(); } // 6
set priority(aString) { this._priority = new Priority(aString); } // 4
}
class Priority {
constructor(value) {
if (value instanceof Priority) { return value; }
// 우선순위 값 검증 및 비교 로직
if (Priority.legalValues().includes(value)) {
this._value = value;
} else {
throw new Error(`<${value}}> is invalid for Priority`);
}
}
static legalValues() { return ['low', 'normal', 'high', 'rush']; }
get _index() { return Priority.legalValues().findIndex((s) => s === this._value); }
toString() { return this._value; }
equals(other) { return this._index === other._index; }
higherThan(other) { return this._index > other._index; }
lowerThan(other) { return this._index < other._index; }
}
임시 변수를 질의 함수로 바꾸기
다른 함수에서도 사용할 수 있어 코드 중복을 줄일 수 있다.
여러 곳에서 똑같은 방식으로 계산되는 변수를 발견하면 질의 함수로 바꿀 수 있을지 살펴보자.
값이 대입된 변수가 있는데, 복잡한 로직에서 여러 차례 다시 대입되는 경우 모두 질의 함수로 추출하자.
개요
Before
class Order {
constructor(quantity, item) {
this._quantity = quantity;
this._item = item;
}
get price() { //
var basePrice = this._quantity * this._itemPrice;
var discountFactor = 0.98;
if (basePrice > 1000) {
discountFactor -= 0.03;
}
return basePrice * discountFactor;
}
}
After
class Order {
constructor(quantity, item) {
this._quantity = quantity;
this._item = item;
}
get basePrice() { return this._quantity * this._itemPrice; }
get discountFactor() {
var discountFactor = 0.98;
if (basePrice > 1000) {
discountFactor -= 0.03;
}
return discountFactor;
}
get price() { return this.basePrice * this.discountFactor; } //
}
절차
- 변수를 사용할 때마다 매번 다른 결과를 갖는지 확인
- 변수를 읽기 전용으로 만들 수 있다면 읽기 전용으로 만들기
- 테스트
- 재대입 코드가 있는지 발견할 수 있다.
- 변수 대입문을 함수로 추출
- 사이드 이펙트가 있다면 질의 함수와 변경 함수로 분리하기로 대처
- 테스트
- 변수 인라인하기로 임시 변수 제거
클래스 추출하기
- 반대 리팩터링 : 클래스 인라인하기
클래스는 명확하게 추상화하고 소수의 주어진 역할만 처리하자.
따로 묶을 수 있는 데이터와 메서드가 보인다면 어서 분리하자.
개요
Before
class Person {
get officeAreaCode() { return this._officeAreaCode; }
get officeNumber() { return this._officeNumber; }
}
After
class Person {
get officeAreaCode() { return this._telephoneNumber.areaCode; }
get officeNumber() { return this._telephoneNumber.number; }
}
class TelephoneNumber {
get areaCode() { return this._areaCode; }
get number() { return this._number; }
}
절차
- 클래스 역할 분리 방법 정하기
- 분리될 역할을 담당할 클래스 만들기
- 원래 클래스 생성자에서 새로운 클래스의 인스턴스 생성
- 분리된 역할에 필요한 필드들을 새로운 클래스로 옮기기
- 메서드들도 새로운 클래스로 옮기기(함수 옮기기)
- 호출을 당하는 일이 많은 메서드부터 옮기자
- 양쪽 클래스의 인터페이스를 살피며 불필요 메서드 제거 및 환경에 맞게 이름 수정(함수 선언 바꾸기)
- 새로운 클래스를 외부로 노출할지 결정
- 외부로 노출할 경우 “새로운 클래스에 참조를 값으로 바꾸기” 적용 고민
Example
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; }
get telephoneNumber() { return this._telephoneNumber.toString(); }
}
class TelephoneNumber {
get areaCode() { return this.areaCode; }
set areaCode(arg) { this.areaCode = arg; }
get number() { return this.number; }
set number(arg) { this.number = arg; }
get toString() { return `(${this.areaCode}) ${this.number}`; }
}
클래스 인라인하기
역할 옮기기 리팩터링 후 더 이상 제 역할을 못 하는 클래스는, 자신을 가장 많이 사용하는 클래스로 흡수시키자.
- 반대 리팩터링 : 클래스 추출하기
개요
Before
class Person {
get officeAreaCode() { return this._telephoneNumber.areaCode; }
get officeNumber() { return this._telephoneNumber.number; }
}
class TelephoneNumber {
get areaCode() { return this._areaCode; }
get number() { return this._number; }
}
After
class Person {
get officeAreaCode() { return this._officeAreaCode; }
get officeNumber() { return this._officeNumber; }
}
절차
- 소스 클래스의 각 public 메서드에 대응하는 메서드들을 타깃 클래스에 생성
- 소스 클래스의 메서드를 사용하는 코드를 모두 타깃 클래스의 위임 메서드를 사용하도록 수정 (수정마다 테스트)
- 소스 클래스의 메서드와 필드를 모두 타깃 클래스로 옮기기 (이동마다 테스트)
- 소스 클래스를 삭제
위임 숨기기
캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야 할 내용을 줄여준다.
인터페이스와의 의존성을 없애려면 위임 메서드를 만들어서 위임 객체의 존재를 숨기자.
- 반대 리팩터링 : 중개자 제거하기
개요
Before
manager = aPerson.department.manager;
After
manager = aPerson.manager;
class Person {
get manager() { return this.department.manager; }
}
절차
- 위임 객체의 각 메서드에 해당하는 위임 메서드를 서버에 생성
- 클라이언트가 서버를 호출하도록 수정
- 서버로부터 위임 객체를 얻는 접근자 제거
- 테스트
example
manager = aPerson.manager;
class Person {
constructor(name) {
this._name = name;
}
get name() { return this._name; }
get manager() { return this._department.manager; } // 부서 클래스를 숨기고 위임 메서드 생성
set department(arg) { this._department = arg; }
}
class Department {
get chargeCode() { return this._chargeCode; }
set chargeCode(arg) { this._chargeCode = arg; }
get manager() { return this._manager; }
set manager(arg) { this._manager = arg; }
}
중개자 제거하기
중개자 역할로 전략하여 단순히 전달만 하는 클래스(위임 메서드들로 쌓인)는 차라리 위임 객체를 직접 호출하게 하자.
위임 숨기기나 중개자 제거하기를 적당히 섞어 상황에 맞게 처리하자.
- 반대 리팩터링 : 위임 숨기기
개요
Before
manager = aPerson.manager;
class Person {
get manager() { return this.department.manager; }
}
After
manager = aPerson.department.manager;
절차
- 위임 객체를 얻는 Getter 생성
- 위임 메서드를 호출하는 코드를 Getter 로 수정
- 위임 메서드 삭제
알고리즘 교체하기
더 간명한 방법을 찾으면 복잡한 기존 코드를 간명한 방식으로 고치자.
알고리즘 교체를 위해 반드시 메서드를 가능한 잘게 나누자.
개요
Before
function foundPerson(people) {
for (let i = 0; i < people.length; i++) {
if (people[i] === 'Don') {
return 'Don';
}
if (people[i] === 'John') {
return 'John';
}
if (people[i] === 'Kent') {
return 'Kent';
}
}
return "";
}
After
function foundPerson(people) {
const candidates = ['Don', 'John', 'Kent'];
return people.find((p) => candidates.includes(p) || "");
}
절차
- 교체할 코드를 하나의 함수에 모으기
- 이 함수의 동작 검증 테스트 만들기
- 대체 알고리즘 준비
- 정적 검사 수행
- 기존 알고리즘과 새 알고리즘의 결과를 비교하는 테스트 수행
- if 결과가 같다면 리팩터링 종료
- else 기존 알고리즘을 참고해서 새 알고리즘 테스트 및 디버깅