Ruby

【Ruby で解説するデザインパターン】Builder パターン

今回はデザインパターンを学習していて、少し理解に時間のかかった Builder パターンについて解説していきたいと思います。

はじめに

オブジェクト指向プログラミングにおいて、複雑なオブジェクトを構築する際に直面する一般的な問題は、コンストラクタが肥大化し、コードの可読性と保守性が低下することです。この問題を解決するための一つのアプローチが「Builderパターン」です。この記事では、Rubyを使ったBuilderパターンの例を通じて、そのメリットと使い方について解説します。

Builder パターンとは

Builderパターンは、複雑なオブジェクトの構築を簡素化するデザインパターンの一つです。このパターンの主な目的は、オブジェクトの構築プロセスをその表現から分離し、同じ構築プロセスで異なる表現を生成できるようにすることです。

Builder パターンを使用しない場合

class CustomPC
  attr_accessor :processor, :ram, :storage

  def initialize(processor, ram, storage)
    @processor = processor
    @ram = ram
    @storage = storage
  end

  def to_s
    "Custom PC with #{@processor} processor, #{@ram} RAM, #{@storage} storage."
  end
end

custom_pc = CustomPC.new("Intel Core i7", "16GB", "1TB SSD")
puts custom_pc

このアプローチでは、コンストラクタ(初期化のときに行う処理)が肥大化するため、詳細の変更が困難になります。

このアプローチの問題点

コンストラクタの複雑性が増す

インスタンスを生成する際に引数を多く渡す必要があるため、引数の順番を間違えるリスクが高まります。

また、新しいパラメータを追加したい場合や構築ロジックを変更したい場合、コンストラクタと呼び出し元のすべてのコードを修正する必要があります。これにより、既存のコードベースに大きな変更が必要になります。

不必要なパタメータを設定しなければならない場合がある

インスタンスによっては必要のないパラメータが存在する場合があります。その場合、普通デフォルト値が設定されますが、これによりオブジェクトの状態が不明確でバグの原因になる場合があります。

これを解決するのが Builder パターンです。

Builder パターンを使用する場合

class CustomPC
  attr_accessor :processor, :ram, :storage

  def initialize
    @processor = nil
    @ram = nil
    @storage = nil
  end

  def to_s
    "Custom PC with #{@processor} processor, #{@ram} RAM, #{@storage} storage."
  end
end

class CustomPCBuilder
  def initialize
    @custom_pc = CustomPC.new
  end

  def add_processor(processor)
    @custom_pc.processor = processor
    self
  end

  def add_ram(ram)
    @custom_pc.ram = ram
    self
  end

  def add_storage(storage)
    @custom_pc.storage = storage
    self
  end

  def build
    @custom_pc
  end
end

builder = CustomPCBuilder.new
custom_pc = builder.add_processor("Intel Core i7")
                  .add_ram("16GB")
                  .add_storage("1TB SSD")
                  .build

puts custom_pc

まず、インスタンス生成のロジックが読みやすくなっているのがパッと見て分かります。

例えば add_ram("16GB") となっていることで ram が 16GB であることがぱっと分かりますし、必要ない部品があればそれを省略することができます。

さらにこれにより、将来の拡張性が高まっています。

Builder パターンを利用せずにカスタムPCに新しい属性を追加する場合、以下のように更新する必要があります。

カスタムPCクラスの更新

class CustomPC
  def initialize(processor, ram, storage, graphics_card)
    @processor = processor
    @ram = ram
    @storage = storage
    @graphics_card = graphics_card
  end

  def to_s
    "Custom PC with #{@processor} processor, #{@ram} RAM, #{@storage} storage, #{@graphics_card} graphics card."
  end
end

クライアントコードの更新

custom_pc = CustomPC.new("Intel Core i7", "16GB", "1TB SSD", "NVIDIA RTX 3080")
puts custom_pc

コンストラクタのメソッドを更新して、さらにクライアントコードも更新しているのが分かります。

この場合、クライアントコードすべてで更新を行わないと AugumentError になってしまうのでバグを生むリスクが高まります。

さらに、この例では属性を追加しているだけですが、コンストラクタの中で変換などをしている場合は initialize の中が複雑になっていきます。

一方、Builder パターンを使用する場合は以下のように更新できます。

カスタムPCクラスの更新

class CustomPC
  attr_accessor :processor, :ram, :storage, :graphics_card

  # ... 既存のコード ...

  def to_s
    "Custom PC with #{@processor} processor, #{@ram} RAM, #{@storage} storage, #{@graphics_card} graphics card."
  end
end

Builder クラスの更新

class CustomPCBuilder
  # ... 既存のコード ...

  def add_graphics_card(graphics_card)
    @custom_pc.graphics_card = graphics_card
    self
  end

  # ... 既存のbuildメソッドなど ...
end

クライアントコードの更新

builder = CustomPCBuilder.new
custom_pc = builder.add_processor("Intel Core i7")
                  .add_ram("16GB")
                  .add_storage("1TB SSD")
                  .add_graphics_card("NVIDIA RTX 3080")
                  .build

puts custom_pc

これにより、変更が Builder クラスに新しいメソッドを作成する箇所に限定されています。

これにより、コンストラクタの中が複雑にならずに見通しがよくなりました。グラフィックカードの初期化過程でなにか処理を行ったとしても単一のメソッドにカプセル化されているので、可読性と変更のしやすさが高まっています。

さらに、クライアントコードもグラフィックカードが必要な箇所のみに限定されます。これによりすべてのクライアントコードを更新する必要はなく、初期化時にエラーになるリスクも無くせました。

まとめ

Builderパターンは、Rubyにおける複雑なオブジェクトの構築を簡素化し、コードの可読性と保守性を高める効果的な方法です。このパターンを適切に活用することで、より清潔で、管理しやすいコードベースを実現できます。

ここまで読んでいただきありがとうございました!

ABOUT ME
sakai
三重出身の28歳。前職はメーカーで働いていて、プログラミングスクールに通って未経験からWeb業界に転職しました。Railsをメインで使っていて、AWSも少しできます。音楽を聞くこととYoutubeを見るのが好きです。最近はへきトラ劇場にハマってます