디자인의 핵심인 메시지, 하지만 가장 명시적으로 드러나는 것은 클래스. 이번에 집중할 것
어떻게 알 수 있는지
클래스는 단순해야 한다 에 집중. 클래스는
클래스를 모델링 하자
우리는 코드 짤 줄 아는데, 코드를 어떻게 구성하고, 어디에 배치할지 모름
클래스는 우리가 애플리케이션에 대해 어떻게 생각하는지
에 대해 영원히 영향을 미침
변화를 잘 수용할 수 있어야함
그래서 디자인이란, 완벽함을 추구하는 것보단 코드의 수정가능성을 보정하는 기술
먼저 수정하기 쉽다 를 정의하기
작성하는 코드는 다음 특징을 가짐
투명하다
적절하다
사용 가능하다
모범이 된다
요번 챕터의 예시 주제로 자전거, 그리고 기어비를 계산 하는 애플리케이션 만드는거
일단 클래스가 될만한거는 자전거
(근데 지금 필요한거 아님)
반면 기어는 앞 톱니바퀴, 뒷 톱니바퀴, 기어비를 가짐.
둘 다 가지기 때문에 클래스 만들 수 있음
class Gear {
constructor(readonly cog: number, readonly chainRing: number) {}
ratio() {
return this.chainRing / this.cog;
}
}
let myGear = new Gear(52, 11).ratio();
let friendGear = new Gear(20, 5).ratio();
기어계산기 만듬.
요구사항이 늘어남
다양한 크기의 바퀴를 고려할 수 있는 계산기
위와 관련된 스펙이 미국에서 기어인치(=바퀴 지름 x 기어비)라고 함 이 때, 바퀴 지름은 = 바퀴테(rim) 지름 + 타이어 높이의 두 배
Gear 에 새로운 행동 추가
class Gear {
readonly cog: number;
readonly chainRing: number;
readonly rim: number;
readonly tire: number;
constructor(cog: number, chainRing: number, rim: number, tire: number) {
this.cog = cog;
this.chainRing = chainRing;
this.rim = rim;
this.tire = tire;
}
ratio() {
return this.chainRing / this.cog;
}
// 새로 추가된 함수
gear_inches() {
// 타이어는 바퀴테를 가싸고 있어서, 지름 계산시 타이어 높이에 2를 곱함
return this.ratio * (this.rim + this.tire * 2);
}
}
let myGearInches = new Gear(52, 11).gear_inches();
let friendGearInches = new Gear(20, 5).gear_inches();
현재 모습이 코드 최선으로 배치하는 방법일까? 정답은 상황에 따라 다르다 이다.
쉽게 수정할 수 있는 애플리케이션은 재사용하기 쉬운 클래스로 구성
재사용하기 쉬운 클래스란 쉽게 가져다 쓸 수 있는 코드(pluggable units)
한 개 이상의 책임이 있는 클래스는 재사용이 어려움
이 상황에서 할 수 있는 거
재사용하려는 클래스는 스스로의 역할에 대해 혼란스럽고 책임들이 뒤얽힘
클래스가 인격이 있는 것처럼 질문하기. 예컨대,
ex. Gear 님, 당신의 기어비는 무엇인가여?
클래스의 책임을 한 문장으로 만들어보기
최대한 작으면서 유용한 것만 행해야 하는 것(smallest possible useful thing)
가장 단순한 표현이 그리고
, 또는
이라는 단어 사용시 연관된지 않은 둘 이상의 책임을 가지고 있다고 판단
응집력 클래스 안의 모든 것들이 1 개의 핵심 목표와 연관되어있을 때 이 클래스는 강하게 응집되어 있음. 또는 1 개의 책임을 가진다고 말할 수 있음
Gear 클래스의 책임을 표현하자면 “앞, 뒤 톱니바퀴 사이의 기어비를 계산한다”: Gear 클래스는 많은 거 하고 있음 “자전거에 기어가 미치는 영향을 계산한다”: 더 어울림 그렇다면 gear_inches 는 Gear 에 속하는 것이 맞음. 하지만 타이어 높이는 애매
Gear 처럼 불안한 클래스를 본다면, “지금 암것도 안하면 나중에 어떤 대가 치를까?” 라고 질문하기
현재 Gear 는 다른 객체와의 의존성이 생길 경우 투명함
과 적절함
을 잃게 됨
바로 이 순간이 코드 재구성해야할 때
객체는 행동과 함께 데이터를 가짐
데이터는
accessor method
를 통해 접근변수 직접 참조하기 보다 언제나 accessor 를 통해 접근하기
class Gear {
constructor(readonly cog: number, readonly chainRing: number) {}
getCog() {
return this.cog;
}
ratio() {
// return this.chainRing / this.cog; // 멸망의 길
return this.chainRing / this.getCog();
}
}
여러 곳에서 참조하고 있는 데이터
에서 단 한 번만 정의된 행동
으로 변경
직접 참조하기 단점
cog
를 열 군데에서 참조시 내용 바꿔야 한다면 10 군데 전부 일일히 수정해야함accessor 의 장점
데이터를 마치 메시지를 이해하는 객체
처럼 취급하는 것은 두 가지 새로운 이슈를 발생
가시성
public vs private (4 장 유연한 인터페이스 만들기에서 다시 다룸)
추상적
모든 변수를 래퍼 메서드로 감싸고 변수를 객체처럼 사용할 수 있음
이 때문에, 데이터와 객체 사이의 구분이 무의미
가끔 애플리케이션의 특정 부분을 행동과 무관한 데이터라 생각하는게 편리하지만
대부분의 경우 데이터를 일반적인 객체로 이해하는게 맘편함
class ObscuringReference {
constructor(readonly data: array) {}
diameter() {
return this.data[0] + this.data[1] * 2;
}
}
data 는 복잡한 구조여서 단순히 인스턴스 변수 감추는 것으로 불충분
diameter
메서드는 지름을 계산하는 방법만 아는게 아니라
배열의 어디에서 바쿠테 지름과 타이어 높이를 찾아야하는지도 알고 있다.
[0]
: 바퀴테 지름, [1]
: 타이어 지름왜냐면 복잡한 구조를 직접 참조시 진짜 데이터가 무엇인지 드러나지 않음
또한, 배열의 구조 변경시 그 영향이 코드 전체로 퍼짐
복잡한 구조를 직접 참조하면 진짜 데이터가 무엇인지 드러내지 않기 떄문에 헷갈림
배열 구조 바뀔 때마다 모든 참조지점 찾아서 수정해야 하기 때문에 지옥의 유지보수 경험
해결해보자
diameter 는 배열의 내부 구조에 대한 지식이 0
입력받은 배열의 구조에 대한 모든 지식은 wheelify 메서드 속에 격리됨
정리하자면, 데이터 구조를 들여다 보단 작업
을 객체에 대한 메시지를 전송
으로 대체
interface WheelInterface {
rim: number;
tire: number;
}
class Wheel implements WheelInterface {
public rim: number;
public tire: number;
constructor(rim: number, tire: number) {
this.rim = rim;
this.tire = tire;
}
}
class RevealingReferences {
readonly wheel: Wheel;
constructor(data: Array<number>) {
this.wheel = this.wheelify(data);
}
wheelify(data: Array<number>) {
const [rim, tire] = data;
return new Wheel(rim, tire);
}
diameter() {
return this.wheel.rim + this.wheel.tire * 2;
}
}
console.log(new RevealingReferences([10, 20]).diameter());
클래스 뿐 아니라 메서드에서도 단일 책임 원칙 강제하자
gear_inches() {
return ratio * (rim + (tire * 2));
}
gear_inches 속에 바퀴의 지름을 구하는 계산이 숨어있음
gear_inches() {
return ratio * diameter;
}
diameter() {
return rim + (tire * 2);
}
메서드가 하나의 책임을 질 때
메서드 속에 있는 코드 한 조각에 주석을 달아야 한다면 별도의 메서드로 추출
너무 많은 책임을 지고 있는 클래스가 있다면, 이 책임을 다른 클래스 속으로 분리하자.
class Gear {
readonly cog: number;
readonly chainRing: number;
readonly wheel: Wheel;
constructor(cog: number, chainRing: number, wheel: Wheel) {
this.cog = cog;
this.chainRing = chainRing;
this.wheel = wheel;
}
ratio() {
return this.chainRing / this.cog;
}
// 새로 추가된 함수
gear_inches() {
// 타이어는 바퀴테를 가싸고 있어서, 지름 계산시 타이어 높이에 2를 곱함
return this.ratio * this.wheel.diameter();
}
}
class Wheel {
rim: number;
tire: number;
constructor(rim: number, tire: number) {
this.rim = rim;
this.tire = tire;
}
diameter() {
return this.rim * (this.tire * 2);
}
}
Gear 와 Wheel 모두 1 개의 책임안 지게 됨.
수정하기 쉽고 OOP 소프트웨어 만드는 길은 1 개의 책임을 지는 클래스 만드는 것부터 시작
클래스는 데이터와 행동을 가짐
어떤 행동을 구현할까?
1 개의 클래스는 다른 클래스에 대해 얼마나 알고 있을까?
다른 클래스에게 어느 정도까지 열려 있을까?
단순해야 한다
라는 것에 집중하여 위 질문에 답하기
단일 책임 원칙
기법
인스턴스 변수 숨기기
여러 곳에서 참조하고 있는 데이터
에서 단 한 번만 정의된 행동
으로 변경데이터 구조 숨기기
클래스의 추가 책임을 격리시키기