refactoring 7: Encapsulation

캡슐화
refactoring

시작하며


7.1 레코드 캡슐화하기 Encapsulate Record

// **AS-IS: 레코드(객체 리터럴)**
const organization = {name: "루미", country: "KR"};

// **TO-BE: 클래스(책에서 말하는 '객체' 예시)**
// 레코드를 캡슐화하는 목적은 변수 자체는 물론, 내용을 조작하는 방식도 통제하기 위함
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;
  }
}

// **AS-IS: 중첩된 레코드**
// 중첩 정도가 심할수록 체이닝이 심해진다.
// data[id].usages[year][month]...
'1994': {
	name: '루미',
	usages: {
		'2016': {
			'1': 50,
			'2': 55
		}
	}
},
...

// **TO-BE:**
// 아래에서 rawData 메서드를 통해 내부 데이터를 복제해 사용하므로,
// 사용하는 개발자(클라이언트)가 데이터를 직접 수정할 수는 없게 된다.
class CustomerData {
	get rawData() {
		return _.cloneDeep(this._data);
	}

	constructor(data) {
		this._data = data;
	}
}

function getCustomerData() {
	return customerData;
}

function getRawDataOfCustomers() {
	return customerData.rawData;
}

function setRawDataOfCustomers(arg) {
	customerData = new CustomerData(arg);
}

function compareUsage(id, year, month) {
	const later = getCustomerData().rawData[id].usages[year][month];
	const earlier = getCustomerData().rawData[id].usages[year - 1][month];

	return {
		laterAmount: later,
		change: later - earlier
	};
}



7.2 컬렉션 캡슐화하기 Encapsulate Collection

// AS-IS:
// getter가 컬렉션 자체를 반환하기 때문에
// 클래스가 눈치채지 못한 상태에서 컬렉션의 원소들이 바뀔 수 있다.
class Person {
	get courses() { return this._courses; }
	set courses(list) { this._courses = list; }
}

// TO-BE:
// getter가 컬렉션의 복제본을 만들어 반환한다.
// 컬렉션을 소유한 클래스를 통해서만 원소를 변경하게 만들 수 있다.
class Person {
	get courses() { return this._courses.slice(); }
	addCourse(course) { ... }
	removeCourse(course) { ... }
}



7.3 기본형을 객체로 바꾸기 Replace Primitive with Query

// AS-IS:
orders.filter((o) => "high" === o.priority || "rush" === o.priority).length;

// TO-BE:
// Order를 통해서 Priority 객체를 제공받도록 해서 Priority를 직접 건드리지 않는다.
// 우선순위 값들 또한 Priority의 메서드로만 조작해 반환하도록 한다.
orders.filter((o) => o.priority.higherThan(new Priority("normal"))).length;

class Order {
  get priority() {
    return this._priority;
  }
  get priorityString() {
    return this._priority.toString();
  }
  set priority(string) {
    this._priority = new Priority(string);
  }
}

class Priority {
  constructor(value) {
    if (value instanceof Priority) return value;
    this._value = 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;
  }
}



7.4 임시 변수를 질의 함수로 바꾸기 Replace Temp with Query

// (Order class 내부)

// AS-IS:
// 여기서 임시 변수는 basePrice와 discountFactor이다.
get price() {
	var basePrice = this._quantity * this._item.price;
	var discountFactor = 0.98;

	if (basePrice > 1000) discountFactor -= 0.03;

	return basePrice * discountFactor;
}

// TO-BE:
// 이제 다른 곳에서도 basePrice, discountFactor를 사용할 수 있다.
get basePrice() {
	return this._quantity * this._item.price;
}

get discountFactor() {
	var discountFactor = 0.98;

	if (basePrice > 1000) discountFactor -= 0.03;

	return discountFactor;
}

get price() {
	return this.basePrice * this.discountFactor;
}

7.5 클래스 추출하기 Extract Class

// AS-IS:
class Person {
  get officeAreaCode() {
    return this._officeAreaCode;
  }
  get officeNumber() {
    return this._officeNumber;
  }
}

// TO-BE:
// 이제 전화번호를 회사 뿐만 아니라 다른 곳에서도 사용할 수 있다.
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;
  }
}

7.6 클래스 인라인하기 Inline Class

// AS-IS:
class Person {
  get officeAreaCode() {
    return this._telephoneNumber.areaCode;
  }
  get officeNumber() {
    return this._telephoneNumber.number;
  }
}

// TO-BE:
// telephoneNumber를 회사번호로만 사용하고 있다면 아래처럼 합치는 편이 낫다.
class Person {
  get officeAreaCode() {
    return this._officeAreaCode;
  }
  get officeNumber() {
    return this._officeNumber;
  }
}

7.7 위임 숨기기 Hide Delegate

// AS-IS:
manager = aPerson.department.manager;

// TO-BE:
manager = aPerson.manager;

class Person {
  get manager() {
    return this.department.manager;
  }
}



7.8 중개자 제거하기 Remove Middle Man

// AS-IS:
manager = aPerson.manager;

class Person {
  get manager() {
    return this.department.manager;
  }
}

// TO-BE:
manager = aPerson.department.manager;



7.9 알고리즘 교체하기 Substitue Algorithm

// AS-IS:
function foundPerson(people) {
  for (let i = 0; i < people.length; i++) {
    if (people[i] === "dawn") {
      return "dawn";
    }
    if (people[i] === "daisy") {
      return "daisy";
    }
    if (people[i] === "lena") {
      return "lena";
    }
  }

  return "";
}

// TO-BE:
function foundPerson(people) {
  const candidates = ["dawn", "daisy", "lena"];

  return people.find((p) => candidates.includes(p)) || "";
}


[절차]

  1. 교체할 코드를 함수 하나에 모은다.

  2. 이 함수만을 이용해 동작을 검증하는 테스트를 마련한다.

  3. 대체할 알고리즘을 준비한다.

  4. 정적 검사를 수행한다.
    (컴파일 단계에서 타입 에러는 없는지 등을 확인하라는 뜻인듯)

  5. 기존과 새 알고리즘의 결과를 비교하는 테스트를 수행한다. 두 결과가 같다면 리팩터링이 끝난다. 그렇지 않다면 기존 알고리즘을 참고해서 새 알고리즘을 테스트하고 디버깅한다.