今回はデザインパターンを学習していて、少し理解に時間のかかった 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における複雑なオブジェクトの構築を簡素化し、コードの可読性と保守性を高める効果的な方法です。このパターンを適切に活用することで、より清潔で、管理しやすいコードベースを実現できます。
ここまで読んでいただきありがとうございました!