ファーストクラスコレクションで重複ロジックを解消する
はじめに
本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。
書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。
書籍ではJavaで説明されていたのですが本記事ではRubyに置き換えながら解説しています。
低凝集なコレクション処理
RPG系ゲームの戦闘外のフィールドマップ上のメンバーの制御を行うクラスの例です。
class FieldManager MAX_MEMBER_COUNT = 5 # パーティの最大メンバー数を定義 # メンバーを追加する def add_member(members, new_member) if members.any? { |member| member.id == new_member.id } raise '既にパーティに加わっています。' end if members.size == MAX_MEMBER_COUNT raise 'これ以上メンバーを追加できません。' end members << new_member end # パーティメンバーが1人でも生存している場合trueを返す def party_is_alive?(members) members.any?(&:is_alive?) end end
add_member
メソッドは引数に指定されたメンバーのコレクションをパーティに追加します。- 例外: 既に同じIDのメンバーがパーティにいる場合
- 例外: パーティのメンバー数が最大数に達している場合
party_is_alive?**
メソッドはパーティのメンバーのうち、1人でも生存している場合にtrue
** を返します。
重複ロジックの例 - add_member
メンバーが追加されるロジックは、フィールドマップ中以外にも必要になる場合があります。
以下はその例で、イベントを制御するクラスに仲間が追加されるメソッドが定義される例です。
# ゲーム中の特別イベントを制御するクラス class SpecialEventManager # メンバーを追加する def add_member(members, member) members << member end end
重複ロジックの例 - is_alive? 系
先程はフィールドマップ上で判定していましたが下記の例は戦闘中の例になります。
このコードでは、メンバーリストの各メンバーを順にチェックし、生存しているメンバーがいるかどうかを判定しています。最初に生存しているメンバーが見つかった時点で結果を true
に設定し、ループを終了します。
# 戦闘を制御するクラス class BattleManager # パーティメンバーが1人でも生存している場合trueを返す def members_are_alive?(members) result = false members.each do |member| if member.is_alive? result = true break end end result end end
名前も実装も違いますが、ロジックの振る舞いは先程フィールド上のロジックを扱うFieldManagerクラスで定義したparty_is_alive?と同じく同様の処理を行う重複コードになります。
クラスコレクションで重複を解消する
ファーストクラスコレクション
ファーストクラスコレクション(First Class Collection)は、コレクションに関連するロジックをカプセル化する設計パターンです。
このパターンでは、単なるデータの集合体であるコレクションをオブジェクトとして扱い、それに関連する振る舞いや操作をメソッドとして定義します。
通常のクラスの初期化との比較
クラスには以下の2つが備わっている必要があります。
ファーストクラスコレクションは、この考え方の応用で、次の要素を備えます。
具体例
Rubyでは以下のように[]や{}を用いて初期化します。
class Party def initialize @members = [] end end
このような設計により、Party
クラスは複数のメンバーを管理することができます。
メンバーの追加や削除、検索などの操作は、@members
を操作するメソッドを通じて行われることが想定されます。
改修例 - ファーストコレクションクラス
addMember
メソッドをadd
メソッドに変更し、Party
クラスに移動しました。
これでメンバーの追加操作はParty
クラス内のadd
メソッドを通じて行われるようになりました。
add
メソッドでは、新しいメンバーを@members
に追加するだけの単純なロジックです。
class Party private attr_reader :members public def initialize @members = [] end # まだ修正は必要 def add(new_member) @members << new_member end end
改善: 破壊的なメソッドの定義に注意
上記のaddメソッドはmembersの要素が意図しない形で変化する副作用が発生する(破壊的な変更を許容してしまう)ため以下のようにコピーするような構造に修正します。
class Party private attr_reader :members public def initialize @members = [] end def add(new_member) # パーティのメンバーリストをコピーして新しいリストを作成 adding = members.dup # 新しいメンバーを追加 adding << new_member # 新しいパーティのインスタンスを作成して返す Party.new(adding) end end
members
のコピーを作成して新しいリストadding
に代入することで、既存のパーティのメンバーリストが変更されることを避けます。adding
に新しいメンバーを追加します(adding << new_member
)。- 最後に、新しいパーティのインスタンスを作成して返します。
これにより、元のパーティのインスタンスが変更されず、新しいパーティのインスタンスが作成されるため不変性が保たれ、パーティの状態の変更が制御されます。
改善後の設計
class Party MAX_MEMBER_COUNT = 4 attr_reader :members def initialize @members = [] # パーティのメンバーリストを空の配列で初期化 end def self.new_with_members(members) Party.new(members) # メンバーリストを指定して新しいパーティのインスタンスを作成 end def add(new_member) if exists?(new_member) raise "既にパーティに加わっています。" end if is_full? raise "これ以上メンバーを追加できません。" end adding = @members.dup # メンバーリストのコピーを作成 adding << new_member # 新しいメンバーを追加 Party.new(adding) # 新しいメンバーリストを持つパーティのインスタンスを作成して返す end def alive? @members.any?(&:alive?) # パーティのメンバーが1人でも生存しているかを判定 end def exists?(member) @members.any? { |m| m.id == member.id } # 指定されたメンバーが既にパーティに所属しているかを判定 end def is_full? @members.size == MAX_MEMBER_COUNT # パーティが満員かを判定 end private def initialize(members) @members = members # メンバーリストをインスタンス変数に設定 end end
このようにすることで、Party
クラスはパーティの操作と状態をカプセル化し、コレクションに関連するロジックを適切に扱うことができます。
使用例
改修後のコードは以下のようにインスタンス化してクラスのロジックを扱えるようになります。
# パーティの初期化 party = Party.new # メンバーの追加 member1 = Member.new(id: 1, name: "Alice") member2 = Member.new(id: 2, name: "Bob") party = party.add(member1) party = party.add(member2) # メンバーの追加に失敗する例 member3 = Member.new(id: 1, name: "Charlie") # member3は既にパーティに加わっているため、例外が発生する party = party.add(member3) # RuntimeError: 既にパーティに加わっています。 # メンバー数が最大に達している場合の例 member4 = Member.new(id: 4, name: "Dave") member5 = Member.new(id: 5, name: "Eve") # partyのメンバー数は既に最大なので、例外が発生する party = party.add(member4).add(member5) # RuntimeError: これ以上メンバーを追加できません。 # パーティの生存状態の判定 party.alive? # true(メンバー1とメンバー2が生存している) # 特定のメンバーがパーティに所属しているかの判定 party.exists?(member1) # true party.exists?(member3) # false # パーティが満員かの判定 party.is_full? # false(メンバー数は2で最大数未満) # メンバーリストの取得 party.members # [member1, member2]
ファーストクラスコレクションは、コードの凝集性(関連するロジックがまとまっていること)を高めるための重要な手法の一つです。
コレクション自体が独立したオブジェクトとして扱われることで、コードの保守性や拡張性が向上し、コレクションに関連する操作や制約を一元管理することができます。
参考
続く…
コメント
本記事の内容は以上になります!
書籍の続きのアウトプットも随時更新したいと思います。
プログラミングスクールのご紹介 (卒業生より)
お世話になったプログラミングスクールであるRUNTEQです♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。