Yanonoblog!

こつこつと

配列やループ処理におけるアンチパターンの解消

はじめに

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

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

書籍ではJavaで説明されていたのですが本記事ではRubyに置き換えながら解説しています。

自前でコレクション処理を実装 してしまう

ゲームで所持品の中に「牢屋の鍵」があるか調べるコードです。

各要素の name プロパティが "牢屋の鍵" と一致するかを判定し、一致した場合に has_prison_keytrue に変更し、ループを終了しますが、for文の中にif文がネストしていて、やや見通しの悪いコードです。

has_prison_key = false

# itemsはItemの配列
items.each do |item|
  if item.name == "牢屋の鍵"
    has_prison_key = true
    break
  end
end
標準ライブラリのメソッドを活用する - any?メソッド

Rubyでは、Enumerableモジュールが提供するメソッドを使って、ブロックを書かずに条件を満たす要素が存在するかどうかを判定することができます。

any? メソッドを使用すると先程のコードは以下のように簡潔に書くことができます。

has_prison_key = items.any? { |item| item.name == "牢屋の鍵" }

車輪の再発明

Rubyの標準ライブラリでは、コレクションの要素を条件に基づいて判定するためのメソッドが提供されているのに対し、同様の処理を自前でforループやif文を使って実装することを車輪の再発明と言います。

プログラミングにおいては、既存のメソッドもそうですがフレームワークやライブラリも車輪の再発明をせずに適切にリサーチして活用することが重要です。

ループ処理中の条件分岐ネスト

コレクション内で、所定の条件を満たす要素だけに何かの処理をしたいケースとして以下のコードを例とします。

メンバー全員の状態を調べ、毒状態の場合にヒットポイントを減少させるロジックを考えてみます。

members.each do |member|
  if member.hitPoint > 0
    if member.containsState?(StateType::POISON)
      member.hitPoint = 10
      if member.hitPoint <= 0
        member.hitPoint = 0
        member.addState(StateType::DEAD)
        member.removeState(StateType::POISON)
      end
    end
  end
end
  1. メンバーの hitPoint が0より大きいか
  2. メンバーが毒の状態であるか
  3. メンバーの hitPoint が0以下であるか
    1. 真の場合)メンバーの hitPoint を0に設定
    2. 真の場合)メンバーに DEAD の状態を追加
    3. 真の場合)メンバーから POISON の状態を削除

このコードもfor文の中にif文が何重にもネストしていて、見通しが悪くなっています。

早期continue

ループ処理中の条件分岐ネストは、returnを応用した、早期continueで解決可能です。

早期returnは「条件を満たさない場合にreturnで抜ける」という手法であることに対し、「条件を満たさない場合にcontinueで次のループ処理に移行する」手法です。

members.each do |member|
  # 生存していない場合、次のループ処理に移行する
  next if member.hitPoint == 0

  if member.containsState?(StateType::POISON)
    member.hitPoint = 10
    if member.hitPoint <= 0
      member.hitPoint = 0
      member.addState(StateType::DEAD)
      member.removeState(StateType::POISON)
    end
  end
end

生存状況を調べるif文を、「生存していなければcontinueで次のループ処理に移行する」形に変更します。

早期continueによりネストが1段浅くなり、後続の処理は実行されないようになるため、パフォーマンスと可読性が改善されます。

早期break

ループ処理中の制御構文としてcontinueの他にもbreakも有用です。

最初のコードを活用し、アイテムの名前が指定した名前とマッチした場合に後続の処理とループ処理を終了させることができます。

has_prison_key = false

# itemsはItemの配列
items.each do |item|
  # アイテムの名前が"牢屋の鍵"と一致する場合、目的のアイテムが存在することが分かる
  if item.name == "牢屋の鍵"
    has_prison_key = true
    # 目的のアイテムが見つかったため、ループを中断し、以降のアイテムに対する処理をスキップする
    break
    # ...後続の処理
  end
end

if has_prison_key
  puts "牢屋の鍵があります"
else
  puts "牢屋の鍵はありません"
end

早期breakを使うことで、目的のアイテムが見つかった時点でループ処理を中断し、それ以降のアイテムに対する処理を行わずに済むため、効率的な処理を実現することも可能です。

参考

続く…

コメント

本記事の内容は以上になります!

書籍の続きのアウトプットも随時更新したいと思います。


プログラミングスクールのご紹介 (卒業生より)

お世話になったプログラミングスクールであるRUNTEQです♪

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky