생성자의 파라미터가 많아질 수록 각 파라미터가 의미하는 속성이 무엇인지 알기 어렵다.
IDE 내에서는 파라미터 이름과 타입을 표시해줘서 문제가 없다고 생각할 수 있다.
github, gitlab MR 코드 리뷰를 한다면? 또는 IDE 기능이 부실하다면?
const champion = new Champion('khazix', 450, 200, false) // 450, 200, false 는 뭐지?
덩그러니 위 와 같이 생성자만 표시된다.
450, 200, false
는 무엇을 의미하는지 파악하기 어렵다.
이럴 때 Champion "클래스 선언 부로 이동하여 확인해야한다." 는 불편함이 있다.
챔피언 이름은 최소 3글자 이상, hp, mp는 0 이상이어야한다는 조건이 있다고 가정해보자.
생성자 내에 각 property validation 코드가 들어간다.
class Champion {
constructor(
private readonly _name: string,
private readonly _hp : number,
private readonly _mp: number
) {
if (_name.length < 3) throw Error('name must be at least 3 characters.')
if (_hp < 0) throw Error('HP should be greater than or equal to 0')
if (_mp < 0) throw Error('MP should be greater than or equal to 0')
// .. 만약 조건식이 더 많아진다면?
}
// ..
}
더 많은 프로퍼티 혹은 복잡한 검증 로직을 가질 수록 생성자가 거대해진다.
거대한 생성자는 클래스의 본래 역할 (행위)를 파악하는데 방해된다.
// Champion class의 '생성'로직을 담당
class ChampionBuilder {
private _name: string
private _hp: number
private _mp: number
constructor() {
}
name(name: string): this {
if (name.length < 3) throw Error('name must be at least 3 characters.')
this._name = name
return this
}
hp(hp: number): this {
if (hp < 0) throw Error('HP should be greater than or equal to 0')
this._hp = hp
return this
}
mp(mp: number): this{
if (mp < 0) throw Error('MP should be greater than or equal to 0')
this._mp = mp
return this
}
build(): Champion {
return new Champion(
this._name,
this._hp,
)
}
}
// 기존 도메인 클래스는 오로지 도메인 클래스의 '표현'과 '행위' 만 담당한다.
class Champion {
constructor(
private readonly _name: string,
private readonly _hp : number,
private readonly _mp: number = undefined
) {
}
static builder(): ChampionBuilder {
return new ChampionBuilder()
}
// getter ..
}
const champion = Champion.builder()
.name('khazix')
.hp(500)
.mp(200)
.build()
빌더 패턴으로 2가지 효과를 얻었다.
champion 생성 단계에서 각 property 가 무엇인지 한 눈에 파악된다.
거대한 생성자를 쓸 필요가 없다.
빌더는 '생성' 을 도메인 객체는 '표현'(property) 과 '행위'(method) 를 담당한다.
속성에 대한 검증 로직은 빌더가 담당하고 본 도메인 객체는 표현과 행위만을 갖고 있다.
Typescript 는 inner class 를 지원하지 않기 때문에, 사용할 클래스 바깥에 빌더 클래스를 정의해한다.
class Champion {
constructor(
private readonly _name: string,
private readonly _hp : number,
private readonly _mp: number = undefined
) {
}
static builder(): ChampionBuilder {
return new ChampionBuilder()
}
static class ChampionBuilder { // ❌ 불가능.
}
}
class UserInfo {
id: number;
userName: string;
email: string;
}
const userInfo = Builder(UserInfo)
.id(1)
.userName('foo')
.email('foo@bar.baz')
.build();
builder-pattern 같은 라이브러리에서 쉽게 클래스에 대한 빌더 패턴 구현을 지원하나, 도메인 객체의 setter
가 열려있어야 한다는 한계가 있다.
(java lombok 의 @builder 어노테이션이 해주는 것 처럼 불변성을 보장해주기 어렵다.)
본 예제에선 도메인 객체의 불변성을 보장하기 위해서 setter 사용 없이 직접 구현했다.