캡슐화
객체를 설계하기 위한 가장 기본적인 아이디어
- 변경의 정도에 따라 구현-인터페이스를 분리하고 외부에서는 인터페이스에만 의존하도록 관계를 조절하는 것 → (불안정한 부분 / 안정적인 부분을 분리해서 변경의 영향을 통제)
캡슐화?
- 객체지향에서 가장 중요한 원리
- 상태와 행동을 하나의 객체 안으로 모아 내부 구현을 외부로부터 감추자. (외부에서 알 필요 없는 부분을 감춤으로써 대상을 단순화시킴)
⇒ 불안정한 구현 세부사항을 안정적인 인터페이스 뒤로 숨김(캡슐화)
응집도와 결합도
응집도
- 모듈에 포함된 내부 요소들이 연관돼 있는 정도
- 모듈 내의 요소들이 하나의 목적을 위해 긴밀하게 협력한다면 그 모듈은 높은 응집도를 가진다.
변경의 관점
- 변경(요구사항)이 발생했을 때 모듈 내부에 발생하는 변경의 정도
- 응집도 👆 - 오직 하나의 모듈만 수정
- 응집도 👇 - 여러 모듈을 동시에 수정
(오브젝트 책 그림 참고하자)
결합도
- 의존성의 정도
- 다른 모듈에 대해 얼마나 많은 지식을 갖고 있는지를 나타냄
변경의 관점
- 하나의 모듈이 변경되기 위해서 다른 모듈의 변경을 요구하는 정도
- 결합도 👆 - 여러 모듈을 동시에 변경
- 결합도 👇 - 오직 하나의 모듈만 영향을 받음
(오브젝트 책 그림 참고하자)
목표 : 높은 응집도와 낮은 결합도 → 설계를 변경하기 쉬움
- 클래스의 구현이 아닌 인터페이스에 의존하도록 코드를 작성해야 낮은 결합도를 얻을 수 있다. (인터페이스에 대해 프로그래밍 하라)
캡슐화
를 지키면 모듈 안의 응집도는 높아지고 모듈 사이의 결합도는 낮아진다.
자신의 데이터를 스스로 처리하는 자율적인 객체(결합도 낮고, 응집도 높음)를 만들면 결합도를 낮출 수 있을뿐더라 응집도를 높일 수 있다.
1. 레코드 캡슐화하기(Encapsulate Record)
// 최상위
// 상수
const organization = {name: "애크미 구스베리", country: "GB"};
// 읽기/쓰기 예
result += `<h1>${organization.name}</h1>`
organization.name = newName;
// 최상위
const organization = new Organization({name: "애크미 구스베리", country: "GB"});
function getOrganization() {return organization;}
// 읽기/쓰기 예
result += `<h1>${getOrganization().name}</h1>`
getOrganization().name = newName;
- 사용자는 내부 행동을 몰라두됨
- 이름변경에 용이
- RequestUrlManager
2. 컬렉션 캡슐화하기(Encapsulate Collection)
가변 데이터를 캡슐화하면 데이터 구조가 언제 어떻게 수정되는지 파악하기 쉬워서 필요한 시점에 데이터 구조를 변경하기도 쉬워지는 장점이 존재.
컬렉션을 캡슐화할때 주의할 점
컬렉션 게터가 원본 컬렉션을 반환하지 않게 만들어서 클라이언트가 실수로 컬렉션을 바꿀 가능성을 차단하자.
- 절대로 컬렉션 값을 반환하지 않게 하자.
- 컬렉션을 읽기전용으로 제공하자.
- 프락시가 내부 컬렉션을 읽는 연산은 그대로 전달, 쓰기는 예외를 던지는 식
- 컬렉션 게터를 제공하되 내부 컬렉션의 복제본을 반환하자.
- 복제본을 수정해도 캡슐화된 원본 컬렉션에는 아무런 영향을 주지 않는다. (컬렉션이 크면 문제가 발생할 수 있음)
예제
// 모든 필드가 접근자 메서드로 보호받고 있다. 캡슐화가 잘 됬다..!
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;}
}
// Person 클래스가 더는 컬렉션을 제어할 수 없으니 캡슐화가 깨졌다고 볼 수 있다.
// 필드를 참조하는 과정만 캡슐화했을 뿐 필드에 담긴 내용은 캡슐화하지 않았음
// client
// 컬렉션을 통째로 설정
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));
}
class Person {
constructor(name) {
this._name = name;
this._courses = [];
}
get courses() {return this._courses.slice();} // 복제본 제공
addCourse(aCourse) { this._courses.push(aCourse); }
removeCourse(aCourse) { ... }
}
// Person 클래스가 컬렉션을 제어할 수 있게 수정한다.
// client
for(const name of readBasicCourseNames(filename)) {
aPerson.addCourse(new Course(name, false));
}
3. 기본형을 객체로 바꾸기(Replace Primitive with Object)
단순한 출력 이상의 기능이 필요해지는 순간 그 데이터를 표현하는 전용 클래스를 정의하자
프로그램이 커질수록 점차 유용한 도구가 된다.
orders.filter(o => "high" === o.priority
|| "rush" === o.priority);
// 우선순위 속성을 표현하는 값 클래스 Priority를 생성하여, 메소드를 정의한다.
orders.filter(o => o.priority.higherThan(new Priority("normal"))
단순 문자열 → Priorty 값 클래스로 변경 (Java enum?)
class Priority {
toString() {return this._value;}
constructor(value) {
if (Priority.legalValues().includes(value))
this._value = value;
else
throw new Error(`<${value}> is invalid for Priority`);
}
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;}
}
export class Order {
constructor(data) {
this._priority = new Priority(data.priority);
}
get priority() {
return this._priority;
}
get priorityString() {
return this._priority.toString();
}
set priorityString(value) {
this._priority = new Priority(value);
}
}
const orders = [new Order({priority: "normal"}), new Order({priority: "high"}), new Order({priority: "rush"})];
export const highPriorityCount = orders.filter(o => o.priority.higherThan(new Priority("normal"))).length;
ex. 전화번호 객체
- 단순 문자열로 표현하였었는데 나중에
포매팅
이나지역 코드 추출
같은 특별한 동작이 필요해질 수 있음
4. 임시 변수를 질의 함수로 바꾸기(Replace Templ with Query)
변수 대신 함수로 만들어두면 비슷한 계산을 수행하는 다른 함수에서도 사용할 수 있어 코드 중복이 줄어든다. (여러 곳에서 똑같은 방식으로 계산되는 변수를 발견할 때마다 함수로 바꿀 수 있는지 살펴본다.) - 클래스 안에서 적용할 때 효과가 가장 큼
// 임시 변수를 사용하면 값을 계산하는 코드가 반복되는 걸 줄이고 값의 의미를 설명할 수도 있어 유용
// but, 함수로 만들어 사용하는 편이 나을 수도 있음
class Order {
...
get price() {
var basePrice = this._quantity * this._item.Price;
var discountFactor = 0.98;
if (basePrice > 1000) discountFactor -= 0.03;
return basePrice * discountFactor;
}
}
class Order {
...
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 baseFactor;
}
}
5. 클래스 추출하기(Extract Class)
역할이 갈수록 많아지고 새끼를 치면서 클래스가 복잡해졌다면 클래스를 따로 분리(추출)하자.
클래스
명확하게 추상화하고 소수의 주어진 역할만 처리
but, 실무에선 클래스가 비대해지기 쉬움. 새로운 역할을 덧씌우다보면 역할이 많아지고 클래스가 복잡해지기 마련
class Person {
...
...
get offliceAreaCode() {return this._officeAreaCode;}
get officeNumber() {return this._officeNumber;}
// 전화번호 관련 동작을 별도 클래스로 뽑자
}
class Person {
constructor(name, areaCode, number) {
this._name = name;
this._telephoneNumber = new TelephoneNumber(areaCode, number);
}
get offliceAreaCode() {return this._telephoneNumber.areaCode;}
get officeNumber() {return this._telephoneNumber.number;}
...
}
class TelephoneNumber {
constructor(area, number) {
this._areaCode = area;
this._number = number;
}
get areaCode() {return this._areaCode;}
get number() {return this._number;}
...
}
- 일부 데이터와 메서드를 따로 묶을 수 있다면 클래스를 분리하자 (함께 변경되는 일이 많거나 서로 의존하는 데이터들도 분리)
6. 클래스 인라인하기(Inline Class)
더 이상 제 역할을 못하는(남은 역할이 거의 없을 때) 클래스는 인라인시키자.
class Person {
get offliceAreaCode() {return this._telephoneNumber.areaCode;}
get officeNumber() {return this._telephoneNumber.number;}
}
class TelephoneNumber {
get areaCode() {return this._areaCode;}
get number() {return this._number;}
}
class Person {
get offliceAreaCode() {return this._officeAreaCode;}
get officeNumber() {return this._officeNumber;}
}
TelephoneNumber 가 예전에 유용했을지는 몰라도 현재는 제 역할믈 못하고 있으니 Person 객체로 인라인시키자.
7. 위임 숨기기(Hide Delegate)
캡슐화 ⇒ 잘되어있다면 변경해야할 때 함께 고려해야할 모듈 수가 적어져 코드를 변경하기가 훨씬 쉬워진다.
예제
class 서버 {
public 위임객체;
...
}
interface 위임객체 {
void method();
}
// 클라이언트
서버.위임객체.method()
class 서버 {
private 위임객체;
...
void 위임메서드() {
this.위임객체.method();
}
}
interface 위임객체 {
void method();
}
// 클라이언트
서버.위임메서드();
위임 객체와의 의존성을 없애기 위해 위임 메서드를 만들어서 위임 객체의 존재를 숨기자.
이렇게 되면 위임 객체의 인터페이스가 변경되어도 클라이언트는 아무런 영향을 받지 않는다.
8. 중개자 제거하기(Remove Middle Man)
클라이언트가 위임 객체의 또 다른 기능을 사용하고 싶을 때마다 서버에 위임 메서드를 추가해야 하는데, 이렇게 기능을 추가하다 보면 단순히 전달만 하는 위임 메서드들이 점점 성가셔진다. 그러면 서버 클래스는 그저 중개자(middle man) 역할로 전락하여, 차라리 클라이언트가 위임 객체를 직접 호출하는게 나을 수 있다.
class 서버 {
private 위임객체;
...
void 위임메서드1() {
this.위임객체.method1();
}
void 위임메서드2() {
this.위임객체.method2();
}
void 위임메서드3() {
this.위임객체.method3();
}
...
}
interface 위임객체 {
void method1();
void method2();
void method3();
void method4();
...
}
// 클라이언트
서버.위임메서드1();
서버.위임메서드2();
서버.위임메서드3();
class 서버 {
private 위임객체;
...
위임객체 get위임객체(){
return 위임객체
}
}
interface 위임객체 {
void method1();
void method2();
void method3();
void method4();
...
}
// 클라이언트
위임객체 위임객체 = 서버.get위임객체();
위임객체.위임메서드1();
위임객체.위임메서드2();
위임객체.위임메서드3();
9. 알고리즘 교체하기(Substitude Algorithm)
문제를 해결하기위해 더 간결한 방법이 있다면 알고리즘 전체를 걷어내고 간결한 알고리즘으로 바꾸는 것을 고려해보자.
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 "";
}
function foundPerson(people) {
const candidates = ["Don", "John", "Kent"];
return people.find(p => candidates.includes(p)) || '';
}
리팩터링은 복잡한 대상을 단순한 단위로 나누는 방법. 알고리즘을 통재로 교체하는 것이 편할 수 있다.
'프로그래밍 노트 > 리팩토링' 카테고리의 다른 글
리팩토링(refactoring) 기법 (1) | 2021.10.22 |
---|