はじめに
TDDでReact(TypeScript)の開発を行う際、任意のObjectを作成してStubするシーンはよくあると思います。
毎回のテストでベタ書きするのは、
テストコードの行数が長くなったり変更しないkey: value
ペアまで記述しなくてはならず、
非常に手間になってしまうので、Builderを作成するのが常套手段かと思います。
色々なプロジェクトを見てみると、Builderの作成方法も多岐に渡っているということを、
初めて知ったのでどんな書き方があるか?をまとめてみようと思い筆を取りました。
(キーボードを叩いているので筆ではないんですが。)
Builderの主な種類
- 関数形式のBuilder
- クラス形式(インスタンスメソッド)のBuilder
- クラス形式(静的メソッド)のBuilder
- Fluent interface
この4種類が主な記述方法のようなので、それぞれの記述方法をまとめたいと思います。
ここでは、作成したいObjectを便宜上customSampleObjとし、型は、
type SampleObj = {
id: number
date: string
name: string
}
とすることにします。
関数形式のBuilder
まずは、Builderの定義から記述します。
export const sampleObjBuilder = (overrides: Partial<SampleObj>): SampleObj => {
return {
id: 1,
date: "",
name: "",
...overrides,
}
}
呼び出す時は、
const customSampleObj = sampleObjBuilder({ date: "2024-08-01", name: "React" })
のような形になります。
初期値のままだった場合は、
const customSampleObj = sampleObjBuilder({})
このような形です。
{}
を記述するのが手間だったら、
customSampleObjを定義する際に、
export const sampleObjBuilder = (overrides: Partial<SampleObj> = {}): SampleObj => {
return {
id: 1,
date: "",
name: "",
...overrides,
}
}
としてしまえば、初期値の呼び出しは、
const customSampleObj = sampleObjBuilder()
のようになります。
呼び出しが非常に簡単なのもメリットかなと思います。
クラス形式(インスタンスメソッド)のBuilder
この場合の定義は下記の通り。
export class SampleObjFixture {
createSampleObj(overrides: Partial<SampleObj>): SampleObj {
return {
id: 1,
date: "",
name: "",
...overrides,
}
}
}
呼び出す時は、
const fixture = new SampleObjFixture()
const customSampleObj = fixture.createSampleObj({ date: "2024-08-01", name: "React" })
のような形になり、
一旦インスタンスかする必要があるのでテストコードがやや冗長になりますが、
インスタンスを使うことで状態を持たせる事ができるメリットもあります。
また、メソッドを追加することで、複雑なBuilderを作成することができる点はメリットです。
クラス形式(静的メソッド)のBuilder
続いて、静的メソッドの場合です。
export class SampleObjFixture {
static create(overrides: Partial<SampleObj>): SampleObj {
return {
id: 1,
date: "",
name: "",
...overrides,
}
}
}
インスタンスメソッドの場合と違うのは、staticを使用している点。
呼び出し方は、
const customSampleObj = SampleObjFixture.create({ date: "2024-08-01", name: "React" })
のような形です。
関数形式のBuilderと比べるとやや複雑な印象ですが、
インスタンス化は不要で、呼び出しは関数形式と同じく楽な印象です。
メソッドを追加するなどを考えた時に関数形式より柔軟性は高いのでケースバイケースで使い分けるのが良さそうです。
Fluent interface
柔軟性をさらに高めたBuilderで、呼び出し方もドットチェーンを用いる方式がこのパターンです。
class SampleObjBuilder {
private obj: SampleObj = {
id: 1,
date: "",
name: "",
}
setId(id: number): this {
this.obj.id = id
return this
}
setDate(date: string): this {
this.obj.date = date
return this
}
setName(name: string): this {
this.obj.name = name
return this
}
build(): SampleObj {
return this.obj
}
}
定義するだけで複数のメソッドを準備する必要があり、
実装パターンを暗記していないとサクッと記述できないかもしれません。
呼び出す時の使い方は、
const customSampleObj = new SampleObjBuilder()
.setDate("2024-08-01")
.setName("React")
.build()
のように、ドットチェーンで各Valueを引数に与えて最後に.build()
で所望のObjを得ます。
この手法のメリットは、ドットチェーンのおかけで直感的に何を行なっているのか見やすいという点と、
柔軟性が高く、複雑なObjectの構築が可能という点が挙げられます。
一方で、実装は全力で複雑です。
シンプルなObjectやプロジェクト初期のタイミングではオーバーヘッドな実装になる可能性がありそうです。
まとめ
Builder一つとっても実装方法は複数あり、それぞれメリット・デメリットがあることを知りました。
今までの経験上、最後に挙げたFluent interfaceしか見たことがなかったというかなり特殊な経験をしていたので、今回シンプルな実装方法もその他の実装方法も学べたので勉強になりました。
実務でもリファクタリングするのでBuilder作りますね〜といいながら、サクッと実装できるまでこの形を頭に?身体に?染み込ませたいと思います。
それではみなさま、良いコードライフを!