응집력: 클래스 안의 모든 것들이 하나의 핵심 목표와 연관되어 있다. -> 하나의 책임을 가지고 있다(단일 책임 원칙).
다른 객체와의 의존성이 생긴다면 Gear 클래스는 투명함과 적절함을 잃게 된다. 바로 이 순간 코드 재구성을 해야 한다. 새로운 의존성이 좋은 디자인을 결정하기 위한 정보를 제공해 준다.
코드가 잘못된 의도를 전달하고 있다면 다른 개발자가 오해하고 잘못된 의도를 전파하지 않도록 명시적으로 알려주어야 한다.
데이터(data)가 아니라 행동(behavior)에 기반한 코드를 작성하라
인스턴스 변수를 숨기고 getter 를 만들기: getter 를 구현하여 ‘여러 곳에서 참조하고 있는 데이터’ 를 ‘단 한번만 정의된 행동’ 으로 바꾸었다.
데이터를 ‘메세지를 이해하는 객체’ 처럼 취급할 때 얻게 되는 것
가시성 (다른 챕터에서 좀 더 이야기됨)
데이터와 객체 사이의 구분을 무의미하게 만듦: 대부분의 경우는 데이터를 그냥 일반적인 객체인 것처럼 이해해버리는 편이 낫다.
개발자 자신으로부터 데이터를 감추는 편이 좋다.
예상치 못한 변화로부터 코드를 보호할 수 있다.
개발자도 데이터의 모든 행동을 다 모를 수도 있다.
데이터 구조를 숨기자
// 클래스 구현체는 심플한데 들어가야 하는 데이터가 무조건 2차원 배열이라면?
class ObscuringReferences {
constructor(data) {
this.data = data;
}
// cell 이 뭔줄 알고?
get diameters() {
return this.data.map((cell) => cell[0] + cell[1] * 2);
}
}
console.log(
new ObscuringReferences([
[622, 20],
[622, 23],
[559, 30],
[559, 40],
]).diameters
);
데이터 구조가 바뀌면 코드도 변경되어야 하고, 애플리케이션 곳곳에서 배열이라는 자료구조에 대한 지식을 코드 곳곳에 흩뿌리게 되는 결과를 낳게 된다. 전혀 DRY 하지 않다.
루비라면 Struct를 이용하여 데이터 구조를 감쌀 수 있다. 다른 언어를 사용한다면 별도의 데이터 클래스를 만들 수도 있겠다만, JS/TS 는 아직 그런게 없다. 알아서 만들자.
// 임의의 Struct. 디버깅을 위해서는 클래스로 선언하는게 더 나을지도.
const Wheel = (rim, tire) => ({
rim,
tire
})
class RevealingReferences {
constructor(data) {
this.wheels = this._wheelify(data)
}
// 이제 이 getter 는 2차원 배열의 내부 구조에 대해 전혀 알 필요가 없다.
get diameters() {
return this.wheels.map(wheel => wheel.rim + (wheel.tire * 2))
}
// data 의 구조에 대한 지식은 여기다 격리
_wheelify(data) {
return data.map(([rim, tire]) => Wheel(rim, tire))
}
}
console.log(
new RevealingReferences([
[622, 20],
[622, 23],
[559, 30],
[559, 40],
]).diameters
);
이런 스타일의 코드는 외부 데이터 구조의 변화로부터 코드를 보호해주고, 보다 읽기 좋고 의미가 잘 드러나는 코드를 작성할 수 있게 해준다.
'데이터 구조를 들여다보는 작업' 을 '객체에 대한 메세지를 전송' 하는 것으로 대체한 것이다.
모든 곳에서 단일 책임 원칙을 강제하라.
클래스처럼 메서드도 하나의 책임만을 져야 한다.
개별 객체에 행해지는 액션과 객체들을 나열하는 활동이 결합되어 있는 것은 쉽게 발견할 수 있는 중복 책임의 예이다.