Effective Java の BuilderパターンをJavaScriptで書いてみた
Javaの参考書のなかでよくおすすめと言われている Effective Java で紹介されている Builder パターンを、JavaScript で書いてみた。
Effective Java ってなんやそれ、っていう方はこちら。
Amazon.co.jp: EFFECTIVE JAVA 第2版 (The Java Series): Joshua Bloch, 柴田 芳樹: 本
普通に Java で書かれたコードについてはこちら。Effective Java で紹介されているBuilderパターン意外も紹介されています。とてもわかりやすかったです。
Javaで書くBuilderパターンのパターン - Qiita
Builder パターンを使用しない場合
ここでは、以下のユーザ情報を登録できるシステムを想定して、ユーザ情報を保持するオブジェクトの生成を例にみていく。 * 名前(必須項目) * 年齢 * 郵便番号 * 住所 * 血液型 * 星座 * 好きな食べ物
以下 Builder パターンを使用しない場合のコード。 引数ですべての属性を指定している。
/** * ユーザクラス * @param {String} name * @param {Number} age * @param {String} postalCode * @param {String} address * @param {String} phoneNumber * @param {String} boodType * @param {String} constellation * @param {String} favoriteDish */ var User = function (name, age, postalCode, address, phoneNumber, boodType, constellation, favoriteDish) { 'use strict'; if (!name) { throw new Error('必須項目[名前]が未設定です。'); } this.name = name; this.age = age; this.postalCode = postalCode this.address = address; this.phoneNumber = phoneNumber; this.bloodType = boodType; this.constellation = constellation; this.favoriteDish = favoriteDish; }; User.prototype.toString = function () { 'use strict'; var string = (' 名前: ' + this.name + ' 年齢: ' + this.age + ' 郵便番号: ' + this.postalCode + ' 住所: ' + this.address + ' 電話番号: ' + this.phoneNumber + ' 血液型: ' + this.bloodType + ' 星座: ' + this.constellation + ' 好きな食べ物: ' + this.favoriteDish) ; return string; }; // 新規のユーザを作成 var user = new User('数多の世界を救済した勇者 ああああ', 60, 'XXX-XXXXX', '勇者の城(旧魔王の城)', 'XXX-XXXX-XXXXX', 'AB', '乙女座', '牛ヒレ肉とフォアグラのロッシーニ');
このコードの悪いところ (1つめ)
オブジェクトを生成するタイミングで、引数の順番を間違いやすい。 第二引数くらいまでは、まぁ間違わないでしょう。 ただ、第7引数、第8引数にあたりになると、どっちに「星座」で、どっちに「好きな食べ物」設定するんだ?ってなる。
// 新規のユーザを作成 var user = new User('数多の世界を救済した勇者 ああああ', 60, 'XXX-XXXXX', '勇者の城(旧魔王の城)', 'XXX-XXXX-XXXXX', 'AB', '乙女座', '牛ヒレ肉とフォアグラのロッシーニ'); // ↑ こんなに順番覚えていられない。やってられっかばーか ってなる。
このコードの悪いところ (2つめ)
「名前」と「好きな食べ物」だけ設定したい場合も第2引数〜第7引数を指定しなければならない。 コードにすると以下のようになる。
// 新規のユーザを作成 var user = new User('数多の世界を救済した勇者 ああああ', undefined, undefined, undefined, undefined, undefined, undefined, '牛ヒレ肉とフォアグラのロッシーニ'); // こんなの絶対おかしいよ
Builder パターンを使用した例
Builder パターンを使用して、以下のようにコードを改修した。
/** * ユーザクラス * @param {Builder} builder */ var User = function (builder) { 'use strict'; this.name = builder.name; this.age = builder.age; this.postalCode = builder.postalCode; this.address = builder.address; this.phoneNumber = builder.phoneNumber; this.bloodType = builder.bloodType; this.constellation = builder.constellation; this.favoriteDish = builder.favoriteDish; }; User.prototype.toString = function () { 'use strict'; var string = (' 名前: ' + this.name + ' 年齢: ' + this.age + ' 郵便番号: ' + this.postalCode + ' 住所: ' + this.address + ' 電話番号: ' + this.phoneNumber + ' 血液型: ' + this.bloodType + ' 星座: ' + this.constellation + ' 好きな食べ物: ' + this.favoriteDish) ; return string; }; /** * ユーザインスタンスを生成するためのビルダークラス */ User.Builder = function () {}; /** * 名前を設定します。 * @param {String} name * @returns {Builder} */ User.Builder.prototype.name = function (name) { 'use strict'; this.name = name; return this; }; /** * 年齢を設定します。 * @param {Number} age * @returns {Builder} */ User.Builder.prototype.age = function (age) { 'use strict'; this.age = age; return this; }; /** * 郵便番号を設定します。 * @param {String} postalCode * @returns {Builder} */ User.Builder.prototype.postalCode = function (postalCode) { 'use strict'; this.postalCode = postalCode; return this; }; /** * 電話番号を設定します。 * @param {String} phoneNumber * @returns {Builder} */ User.Builder.prototype.phoneNumber = function (phoneNumber) { 'use strict'; this.phoneNumber = phoneNumber; return this; }; /** * 住所を設定します。 * @param {String} phoneNumber * @returns {Builder} */ User.Builder.prototype.address = function (address) { 'use strict'; this.address = address; return this; }; /** * 血液型を設定します。 * @param {String} bloodType * @returns {Builder} */ User.Builder.prototype.bloodType = function (bloodType) { 'use strict'; this.bloodType = bloodType; return this; }; /** * 星座を設定します。 * @param {String} constellation * @returns {Builder} */ User.Builder.prototype.constellation = function (constellation) { 'use strict'; this.constellation = constellation; return this; }; /** * 好きな食べ物を設定します。 * @param {String} favoriteDish * @returns {Builder} */ User.Builder.prototype.favoriteDish = function (favoriteDish) { 'use strict'; this.favoriteDish = favoriteDish; return this; }; /** * ユーザインスタンスを生成して返します。 * @throws {Error} * @returns {User} */ User.Builder.prototype.build = function () { 'use strict'; if (!this.name) { throw new Error('必須項目[名前]が未設定です。'); } return new User(this); }; var userBuilder = new User.Builder(); var user; userBuilder .name('数多の世界を救済した勇者 ああああ') .age(60) .address('勇者の城(旧魔王の城)') .phoneNumber('XXX-XXXX-XXXXX') .postalCode('XXX-XXXXX') .favoriteDish('牛ヒレ肉とフォアグラのロッシーニ') .constellation('乙女座') .bloodType('AB') ; user = userBuilder.build();
ポイント
setter的なメソッド ↓ で builder 自身を返しているところ。
/** * 好きな食べ物を設定します。 * @param {String} favoriteDish * @returns {Builder} */ User.Builder.prototype.favoriteDish = function (favoriteDish) { 'use strict'; this.favoriteDish = favoriteDish; return this; };
これにより、jQuery のメソッドチェーンとか、Java の StringBuilder の append("~").append("~~~~~").append("") みたいに書けるようになる。イケメン。
改修前のコードの問題が解決されているか
改修前のコードの以下の問題が解決できているかみてみる。
- 引数の順番を覚えておかないといけない
- 設定不要なパラメータも指定しなければいけない
var userBuilder = new User.Builder(); var user; // 各属性を設定する場合、その属性に設定するためのメソッドを呼べばよいため、 // 順番をきにする必要はなくなっている。 // また、設定したくない属性がある場合、 // その属性を設定するためのメソッドを呼ばなければよいため2つ目の問題も解決されている。 userBuilder .name('数多の世界を救済した勇者 ああああ') .age(60) .address('勇者の城(旧魔王の城)') .phoneNumber('XXX-XXXX-XXXXX') .postalCode('XXX-XXXXX') .favoriteDish('牛ヒレ肉とフォアグラのロッシーニ') .constellation('乙女座') .bloodType('AB') ; user = userBuilder.build();
結論
やっぱりBuilderパターンってすげーや。