p.236 레코드 캡슐화하기
p.246 컬렉션 캡슐화하기
컬렉션 병수로의 접근을 캡슐화하면서 게터가 컬렉션 자체를 반환하도록 한다면, 그 컬렉션을 감싼 클래스가 눈치채지 못하는 상태에서 컬렉션의 원소들이 바뀌어버릴 수 있다.
이런 문제를 방지하기 위해 컬렉션을 감싼 클래스에 add(), remove()라는 이름의 컬렉션 변경자 메서드를 만든다. 이렇게 항상 컬렉션을 소유한 클래스를 통해서만 원소를 변경하도록 하면 프로그램을 개선하면서 컬렉션 변경방식도 원하는 대로 수정할 수 있다.
가장 흔히 사용하는 방식은 컬렉션 게터를 제공하되 내부 컬렉션의 복제본을 반환하는 것이다. 복제본을 수정해도 원본 컬렉션에는 영향을 주지 않는다.
기존코드
Person class
1
2
3
4
5
6
7
8
9
| class Person{
constructor(name){
this._name = name;
this._course = [];
}
get name(){return this._name;}
get courses(){return this._courses;}
set courses(aList){this._courses = aList;}
}
|
Course class
1
2
3
4
5
6
7
8
| class Course{
constructor(name,isAdvanced){
this._name = name;
this._isAdvanced = isAdvanced;
}
get name(){return this._name;}
get isAdvanced(){return this._isAdvanced;}
}
|
클라이언트
1
2
3
4
5
6
7
8
9
10
11
| numAdvancedCourses = aPerson.courses.filter(c => c.isAdvanced).length;
const basicCourseNames = readBasicCourseNames(filename);
aPerson.courses = basicCourseNames.map(name => new Course(name,false));
// 모든 필드가 접근자 메서드로 보호받고 있으니 데이터를 캡슐화했다고 생각할 수 있으나
//세터를 이용해 수업 컬렉션을 통째로 설정한 클라이언트는 누구든 이 컬렉션을 마음대로 수정할 수 있다.
for(const name of readBasicCourseNames(filename)){
aPerson.courses.push(new Course(name,false));
}
//이런 식으로 목록 데이터를 갱신할 수 있다면 Person 객체가 더는 컬렉션을 제어할 수 없으니 캡슐화가 깨진다.
//필드를 참조하는 과정만 캡슐화하고 필드에 담긴 내용은 캡슐화하지 않았기 때문에 발생하는 문제다.
|
수정된 코드
course 목록을 수정하지 못하도록 course 필드를 캡슐화한다.
Person class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| class Person{
constructor(name){
this._name = name;
this._course = [];
}
get name(){return this._name;}
get courses(){return this._courses.slice();}
// 메서드를 사용하지 않고서는 아무도 목록을 변경할 수 없게 만들기 위해 복제본을 제공한다.
set courses(aList){this._courses = aList.slice();}
//set course()를 제거하는 것이 가장 좋지만 set courses()를 제공해야할 필요가 있다면 인수로 받은 컬렉션의 복제본을 필드에 저장하게 한다.
addCourse(aCourse){
this._aCourse.push(aCourse);
}
removeCourse(aCourse,fnIfAbsent = () => {throw new RangeError();}){
const index = this._courses.indexOf(aCourse);
if(index === -1) fnIfAbsent();
else this._courses.splice(index,1);
}
}
|
Course class
1
2
3
4
5
6
7
8
| class Course{
constructor(name,isAdvanced){
this._name = name;
this._isAdvanced = isAdvanced;
}
get name(){return this._name;}
get isAdvanced(){return this._isAdvanced;}
}
|
클라이언트
1
2
3
4
5
6
7
8
| numAdvancedCourses = aPerson.courses.filter(c => c.isAdvanced).length;
for(const name of readBasicCourseNames(filename)){
aPerson.addCourse(new Course(name,false));
}
//개별 원소를 추가하고 제거하는 메서드를 제공하기 때문에 setCourses()를 사용할 일이 없어졌으니 제거한다. (set courses() 메서드 제거)
//set courses()를 제공해야할 필요가 있다면 인수로 받은 컬렉션의 복제본을 필드에 저장한다.
|
기존 데이터는 캡슐화하고 클라이언트가 데이터를 요구할 때 데이터의 복제본을 제공한다면 예상치 못한 데이터 수정이 촉발한 오류에도 기존 데이터는 그대로 남아있다. 컬렉션 관리를 책임지는 클래스라면 항상 복제본을 제공해야 한다.
p.251 기본형을 객체로 바꾸기
단순 출력 이상의 기능이 필요해지면 그 데이터를 표현하는 전용 클래스를 정의한다. 시작은 기본형 데이터를 단순히 감싼 것과 큰 차이가 없을지 모르나 나중에 특별한 동작이 필요해지면 이 클래스에 추가하면 되니 프로그램이 커질 수록 유용한 도구가 된다.
기존 코드
Order class
1
2
3
4
5
6
| class Order{
constructor(data){
this.priority = data.priority;
}
...
}
|
클라이언트
1
2
| highPriorityCount = orders.filter(o => "high" === o.priority ||
"rush" === o.priority).length;
|
1차 수정된 코드
Order class
1
2
3
4
5
6
7
8
| class Order{
constructor(data){
this.priority = data.priority;
}
get priorityString(){return this._priority.toString();}
set priority(aString){this._priority = new Priority(aString);}
...
}
|
데이터를 다루기 전에 변수부터 캡슐화한다.
우선순위 속성을 초기화하는 생성자에서 방금 정의한 세터를 활용,
이렇게 필드를 자가 캡슐화하면 필드 이름을 바꿔도 클라이언트 코드는 유지할 수 있다.
Priority 클래스를 만든다. 이 상황에서는 게터보다 toString()을 사용한다. 클라이언트 입장에서 보면 속성 자체를 받은 것이 아니라 해당 속성을 문자열로 표현한 값을 요청한게 되기 때문이다.
따라서 Order 클래스의 게터가 반환하는 값은 우선순위가 아닌 우선순위를 표현하는 문자열이기 때문에 이름을 priority에서 priorityString으로 바꿔준다.
Priority class
1
2
3
4
| class Priority{
constructor(value){this._value = value;}
toString(){return this._value;}
}
|
클라이언트
1
2
| highPriorityCount = orders.filter(o => "high" === o.priorityString ||
"rush" === o.priorityString).length;
|
2차 수정된 코드 접기/펼치기 버튼
2차 수정된 코드
Priority 객체를 제공하는 게터를 Order 클래스에 만든다.
Order의 세터가 Priority의 인스턴스를 받도록 한다.
Order class
1
2
3
4
5
6
7
8
9
| class Order{
constructor(data){
this.priority = data.priority;
}
get priority(){return this._priority;}
get priorityString(){return this._priority.toString();}
set priority(aString){this._priority = new Priority(aString);}
...
}
|
Priority class
우선순위 값을 검증하고 비교하는 로직을 추가한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Priority{
constructor(value){
if(value instancof Priority) return value;
if(Priority.legalValues().inclues(value))
this._value = value;
else
throw new Error(`<${value}>는 유효하지 않은 우선순위 입니다.`);
}
toString(){return this._value;}
get _index(){return Priority.legalValues().findIndex(s => s === this._value;)}
static legalValues(){return ['low','normal','high','rush']}
equals(other){return this._index === other._index;}
higherThan(other){return this._index > other._index;}
lowerThan(other){return this._index < other._index;}
}
|
클라이언트
1
| highPriorityCount = orders.filter(o => o.priority.higherThan(new Priority("normal"))).length;
|
p.256 임시변수를 질의 함수로 바꾸기
긴 함수의 한 부분을 별도 함수로 추출하고자 할때 먼저 변수들을 각각의 함수로 만들면 일이 수월해진다. 추출한 함수에 변수를 따로 전달할 필요가 없어지기 때문이다. 또한 이 덕분에 추출한 함수와 원래 함수의 경계가 더 분명해지기도 하는데, 그러면 부자연스러운 의존관계나 부수효과를 찾고 제거하는 데 도움이 된다. 이번 리팩터링은 클래스 안에서 적용할 때 효과가 크다. 클래스는 추출할 메서드들에 공유 컨텍스트를 제공하기 때문이다. 클래스 바깥의 최상위 함수로 추출하면 매개변수가 너무 많아져서 함수를 사용하는 장점이 줄어든다.
기존 코드
Order class
1
2
3
4
5
6
7
8
9
10
11
12
13
| class Order{
constructor(quantity, item){
this._quantity = quantity;
this._item = item;
}
get price(){
var basePrice = this._quantity * this._item.price;
var discountFactor = 0.98;
if(basePrice > 1000) discountFactor -= 0.03;
return basePrice * discountFactor;
}
}
|
수정된 코드
Order class
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Order{
constructor(quantity, item){
this._quantity = quantity;
this._item = item;
}
get price(){
return this.basePrice * this.discountFactor;
}
get basePrice(){ this._quantity * this._item.price;}
get discountFactor(){
var discountFactor = 0.98;
if(this.basePrice > 1000) discountFactor -= 0.03;
return discountFactor;
}
}
|
임시변수인 basePrice, discountFactor를 메서드로 바꾼다.
두 변수에 const를 붙여 수정을 못하게 변경한다. ( 지나친 재대입으로 컴파일에러가 발생한다. )
basePrice 대입문의 우변을 게터로 추출한다.
discountFactor와 관련된 연산을 게터로 추출한다.
p.260 클래스 추출하기
클래스는 반드시 명확하게 추상화하고 소수의 주어진 역할만 처리해야 한다. 메서드와 데이터가 너무 많은 클래스는 이해하기가 쉽지 않으니 잘 살펴보고 적절히 분리하는 것이 좋다. 특히 일부 데이터와 메서드를 따로 묶을 수 있다면 어서 분리하라는 신호다. 함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리한다. 특정 데이터나 메서드 일부를 제거해도 논리적으로 문제가 없다면 분리할 수 있다는 뜻이다.
기존의 코드
Person class
1
2
3
4
5
6
7
8
9
| class Person{
get name(){return this._name;}
set name(arg){this._name = arg;}
get telephoneNumber(){return `(${this.officeAreaCode})${this.officeNumber}`;}
get officeAreaCode(){return this._officeAreaCode;}
set officeAreaCode(arg){this._officeAreaCode = arg;}
get officeNumber(){return this._officeNumber;}
set officeNumber(arg){this._officeNumber = arg;}
}
|
수정된 코드
Person class
1
2
3
4
5
6
7
8
9
10
11
12
| class Person{
constructor(){
this._telephoneNumber = new TelephoneNumber();
}
get name(){return this._name;}
set name(arg){this._name = arg;}
get telephoneNumber(){return this._telephoneNumber.toString();}
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;}
}
|
TelephoneNumber class
1
2
3
4
5
6
7
| class TelephoneNumber{
get toString(){return `(${this.areaCode})${this.number}`;}
get areaCode(){return this._areaCode;}
set areaCode(arg){this._areaCode = arg;}
get number(){return this._number;}
set number(arg){this._number = arg;}
}
|
전화번호 정보가 쓸모가 많으니 TelephoneNumber 클래스를 클라이언트에게 공개하려면 ‘office’로 시작하는 메서드를 없애고 TelephoneNumber의 접근자를 바로 사용하도록 바꿀 수 있다. 그러나 기왕 이렇게 쓸거라면 전화번호를 값 객체로 만드는 방법도 있다. (9.4절참조를 값으로 바꾸기 참고)
p.264 클래스 인라인하기
제 역할을 못해서 그대로 두면 안되는 클래스를 인라인한다. 연락을 옮기는 리팩터링을 하고나니 특정 클래스에 남은 역할이 거의 없을 때 이런 현상이 자주 생긴다. 이럴 땐 가장 많이 사용하는 클래스로 흡수시키자.
두 클래스의 기능을 지금과 다르게 배분하고 싶을 때도 클래스를 인라인한다. 클레스를 인라인해서 하나로 합친 다음 새로운 클래스를 추출하는 게 쉬울 수도 있기 때문이다.
기존 코드
Shipment class
1
2
3
4
5
6
7
8
| class Shipment{
...
get trackingInfo(){return this._trackingInformation.display;}
get trackingInformation(){return this._trackingInformation;}
set trackingInformation(aTrackingInformation){
this._trackingInformation = aTrackingInformation;
}
}
|
1
2
3
4
5
6
7
| class TrackingInformation{
get shippingCompany(){return this._shippingCompany;}
set shippingCompany(arg){this._shippingCompany = arg;}
get trackingNumber(){return this._trackingNumber;}
set trackingNumber(arg){this._trackingNumber = arg;}
get display(){return `${this.shippingCompany} : ${this.trackingNumber}`;}
}
|
클라이언트
1
| aShipment.trackingInformation.shippingCompany = request.vendor;
|
수정된 코드
Shipment class
1
2
3
4
5
6
7
8
9
10
11
12
| class Shipment{
...
get shippingCompany(){return this._shippingCompany;}
set shippingCompany(arg){this.shippingCompany = arg;}
get trackingInfo(){return `${this.shippingCompany} : ${this.trackingNumber}`;}
get trackingInformation(){return this._trackingInformation;}
set trackingInformation(aTrackingInformation){
this._trackingInformation = aTrackingInformation;
}
get trackingNumber(){return this._trackingNumber;}
set trackingNumber(arg){this._trackingNumber = arg;}
}
|
클라이언트
1
| aShipment.shippingCompany = request.vendor;
|
p.268 위임 숨기기
캡슐화는 모듈들이 시스템의 다른 부분에 대해 알아야할 내용을 줄여준다. 캡슐화가 잘 되어 있다면 무언가를 변경해야 할때 함께 고려해야 할 모듈 수가 적어져서 코드를 변경하기가 훨씬 쉬워진다.
위임 객체의 인터페이스가 바뀌면 이 인터페이스를 사용하는 모든 클라이언트가 코드를 수정해야한다. 이러한 의존성을 없애려면 서버 자체에 위임 메서드를 만들어서 위임 객체의 존재를 숨기면 된다. 그러면 위임 객체가 수정되더라도 서버 코드만 고치면 되며, 클라이언트는 아무런 영향을 받지 않는다.
기존 코드
Person class
1
2
3
4
5
6
| class Person{
constructor(name){this._name = name;}
get name(){return this._name;}
get department(){return this._department;}
set department(arg){this._department = arg;}
}
|
Department class
1
2
3
4
5
6
7
| class Department{
...
get chargeCode(){return this._chargeCode;}
set chargeCode(arg){this._charCode = arg;}
get manager(){return this._manager;}
set manager(arg){this._manager = arg;}
}
|
클라이언트
1
| manager = aPerson.department.manager;
|
클라이언트에서 어떤 사람이 속한 부서의 관리자를 알고 싶어한다. 클라이언트는 부서 클래스가 관리자 정보를 제공한다는 사실을 알아야한다. 이러한 의존성을 줄이려면 클라이언트가 부서 클래스를 볼 수 없게 숨기고, 대신 사람 클래스에 간단한 위임 메서드를 만들면 된다.
수정된 코드
Person class
1
2
3
4
5
6
| class Person{
...
//manager 메서드 추가
get manager(){return this._department.manager;}
...
}
|
클라이언트
1
| manager = aPerson.manager;
|
p.271 중개자 제거하기
위임 숨기기는 위임 객체를 캡슐화하는 이점을 갖고있으나 위임을 위해 단순 전달만 하는 메서드를 계속 추가해야할 수 있다. 그러면 서버 클래스는 그저 중개자로 전락하여 차라리 클라이언트가 위임 객체를 직접 호출하는 게 나을 수 있다.
Comments powered by Disqus.