読者です 読者をやめる 読者になる 読者になる

ゆとり世代プログラマの備忘録

ゆとり世代プログラマが備忘録を書いていく

Effective Java の BuilderパターンをJavaScriptで書いてみた

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パターンってすげーや。