Yanonoblog!

こつこつと

ファーストクラスコレクションで重複ロジックを解消する

はじめに

本記事では「良いコード 悪いコードで学ぶ設計入門」で学んだ内容と別途気になって調べた内容や知識も含めアウトプットしています。

書籍に記載されている内容を順序立ててまとめるというよりは、整理しておきたい周辺の知識と深ぼった内容を雑多に書いています。

書籍では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です♪

https://runteq.jp/r/ohtFwbjW

こちらのリンクを経由すると1万円引きになります。

RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。

もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。

https://twitter.com/outputky