클래스는 단순해야 한다
디자인이란 완벽함을 추구하는 행위라기보다 코드의 수정가능성을 보존하는 기술이다.
수정하기 쉽다
수정하기 쉽다는것은 아래와 같은 특징이 있는것을 의미한다. 아래 4가지 모두 잘 정의된 책임을 갖도록 해야 얻을수 있는 특징이다.
- 투명함: 수정된 코드는 수정의 결과가 뚜렷하게 드러나야 한다. (예측 못한 부작용을 낳지 않는다.)
- 적절함: 모든 수정 비용은 수정 결과를 통해 얻는 이득에 비례해야 한다. 즉, 요구사항이 조금 변했을 때 연관된 코드들을 조금만 수정해도 되야합니다.
- 사용가능: 현재 코드를 다시 사용하기 쉽다.
- 모범이 됨: 코드 자체가 나중에 수정하는 사람이 위의 특징을 이어나갈 수 있게 모범이 되야한다.
Transparent, Reasonable, Usable, Exemplary (TRUE) 코드
하나의 책임만 지는 클래스, 메서드, 변수 만들기
하나의 클래스는 최대한 작으면서도 유용한 것(smallest possible useful thing)만 해야한다. 다시 말해서 하나의 책임만 있어야 한다. 응집력이 높아야 한다.
기어비(ratio)를 구하는 메서드를 가지는 Gear 클래스를 만들면 다음과 같다.
ratio는 자전거의 앞 톱니수와 뒷 톱니수의 비율. 즉 기어 1단, 2단으로 표현하는 값을 의미한다.
class Gear {
constructor(
readonly chainring: number,
readonly cog: number,
) {}
get ratio() {
return this.chainring / this.cog;
}
}
여기에 바퀴 크기까지 고려된 gear inches를 추가하면 다음과 같이 코드가 변경된다.
gear inches = 바퀴 지름 * 기어비
바퀴지름 = 바퀴테 지름 + 타이어 높이 * 2
class Gear {
constructor(
readonly chainring: number,
readonly cog: number,
readonly rim: number, // 바퀴테 지름
readonly tire: number, // 타이어 높이
) {}
get ratio() {
return this.chainring / this.cog;
} // 추가된 메서드
get gearInches() {
return this.ratio * (this.rim + this.tire * 2);
}
}
단일 책임 원칙 확인 질문 던지기
우선 Gear 클래스의 책임을 정의해야 한다. 여기서는 이렇게 정의하였다.
기어가 자전거에 미치는 영향을 계산하는 클래스
그리고 아래와 같은 질문을 던져서 클래스가 단일책임을 가지고 있는지 확인한다.
- Gear씨, 당신의 기어비는 무엇인가요? 👌
- Gear씨, 당신의 기어 인치는 무엇인가요? ❓
- Gear씨, 당신의 타이어 높이는 무엇인가요? ❌
모든곳에 단일 책임 원칙을 강제하라
gearInches 속에는 바퀴의 지름을 구하는 계산이 숨어있다. 이는 gearInches의 단일책임 원칙을 위반한다.
이를 수정하면 다음과 같다.
// 기어가 자전거에 미치는 영향을 계산하는 클래스
class BikeGear {
constructor(
readonly chainring: number,
readonly cog: number,
readonly rim: number,
readonly tire: number,
) {}
get ratio() {
return this.chainring / this.cog;
} // 수정된 메소드 (단일 책임 원칙 적용)
get gearInches() {
return this.ratio * this.diameter;
} // 추가된 메소드
get diameter() {
return this.rim + this.tire * 2
}
}
이렇게 수정해보니 diameter는 BikeGear 클래스의 단일책임 원칙을 위반하는 메소드이다.
// 기어가 자전거에 미치는 영향을 계산하는 클래스
class BikeGear {
constructor(
readonly chainring: number,
readonly cog: number,
readonly wheel: Wheel,
) {}
get ratio() {
return this.chainring / this.cog;
} get gearInches() {
return this.ratio * this.wheel.diameter;
}
}// 추가된 Wheel 클래스
class Wheel {
constructor(
readonly rim: number,
readonly tire: number,
) {}
get diameter() {
return this.rim + this.tire * 2
}
}
여기까지 단일책임 원칙을 지킴으로서 나오는 장점 4가지를 보여준다.
- 클래스의 모든 메서드가 하나의 책임만 지게 되면 클래스 자체가 명확하게 드러난다. 오늘 당장 메서드들을 다른 클래스로 옮기고 재정리 하지 않더라도 하나하나가 단일한 목적을 취하면 클래스가 하는 일이 무엇인지 더욱 명확하게 드러나게 된다.
- 주석을 넣어야 할 필요가 없어진다. 위의 변경된 gearInches 메서드의 가독성을 비교하면 알 수 있다.
- 재사용을 유도한다.
- 다른 클래스로 옮기기 쉽다.
단일책임원칙을 위한 리펙토링 순서
- 단일책임 원칙을 위반한 메서드들을 수정하기
- 추가된 메서드들을 보면서 클래스의 책임 정의하기
- 클래스의 단일 책임 원칙을 위반하는 메서드 정리하기
마지막으로 BikeGear 클래스에서 Wheel의 값을 얻을수 있는것은 단일 책임 원칙을 위반한다고 생각했기 때문에 아래와 같이 수정하였다.
또한 Wheel에 대한 책임도 간단히 주석에 정리하였다.
// 기어가 자전거에 미치는 영향을 계산하는 클래스
class BikeGear {
constructor(
readonly chainring: number,
readonly cog: number,
private wheel: Wheel, // 접근자 변경
) {}
get ratio() {
return this.chainring / this.cog;
} get gearInches() {
return this.ratio * this.wheel.diameter;
}
}// Wheel이 자전거에 미치는 영향을 계산하는 클래스
class Wheel {
constructor(
readonly rim: number,
readonly tire: number,
) {}
get diameter() {
return this.rim + this.tire * 2
}
}