Yanonoblog!

こつこつと

デッドコード、YAGNI原則、マジックナンバーの概要

はじめに

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

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

デッドコード

デッドコードとは

どんな条件であったとしても実行されることのないコードのことをデッドコード、または到達不能コードと呼びます。

例えば以下のコードは金額と数量の引数を受け取って合計金額を算出する関数ですが、

数量が0の場合は早期リターンしているのにその後のコードでは、同じ条件でconsole.log("数量は0です");で出力するコードが書かれています。

呼ばれることがないconsole.log("数量は0です");の部分をデッドコードといいます。

function calculateTotalPrice(price, quantity) {
  if (quantity === 0) {
    return 0;
  }

  if (quantity === 0) {
    console.log("数量は0です");
  }

  const totalPrice = price * quantity;
  return totalPrice;
}

const price = 10;
const quantity = 0;
const total = calculateTotalPrice(price, quantity);
console.log("合計金額:", total);
デッドコードの弊害

コードの可読性が低下します。コードの読み手がデッドコードの周辺を読むたびに、どういう条件で実行されるかを読み手に考えさせてしまいます。

なぜ実行されないようなコードが残っているのか、何か意図があるのではと、読み手を混乱させる場合があります。

これまで到達不能だったのが、なんらかの仕様変更によりデッドコード周辺のロジックが変わり、到達可能になることでバグを引き起こす可能性があります。

YAGNI原則

YAGNI原則とは

YAGNI原則(You Ain't Gonna Need It、必要ないものは作るな)は、ソフトウェア開発における設計原則の一つです。

不要な機能や機能拡張を避け、現時点で必要とされていないものを開発しない。実際に必要になったときにのみ実装せよ、という方針です。

実際の開発においても将来の仕様を予見し、ついつい先回りしてつくり込んでしまうことがあるでしょう。しかし、先回りで実装されたロジック

は、現実にはほとんど使われないばかりか、バグの原因になるなど悪魔化することが多いです。

不要な機能を作る弊害

YAGNI原則を守らず先回りしてつくることで予測が外れ使われなくなったロジックは、デッドコードになります。

先回りでつくられたロジックは仕様にないため可読性が低下し、読み手を混乱させます。

先回りでつくり込んだ分だけ時間が無駄になります。

今必要な機能だけをつくり、構造をシンプルにしましょう。

マジックナンバー

マジックナンバーとは

マジックナンバー(Magic Number)は、ソフトウェア開発において、プログラム内に直接値が埋め込まれた数値や定数のことを指します。

これらの数値は、その意味や目的が明示されず、単にコード中に現れる数値として使われるため、「魔法のような」存在としてマジックナンバーと呼ばれています。

マジックナンバーの特徴

マジックナンバーは、以下のような特徴を持ちます

  1. 直接数値が埋め込まれている:コード内に数値がそのまま書かれており、何を表しているのかは明示されていない場合があります。
  2. 意味が不明確:数値自体からはその目的や意味が分かりにくいため、コードの理解や保守性の低下を招く可能性があります。
  3. 変更の影響範囲が広いマジックナンバーが複数箇所に使用されている場合、その値を変更する際に全ての箇所を探し出して修正する必要があります。変更漏れや誤った修正が発生するとバグの原因となります。
def calculate_total_price(quantity)
  if quantity > 10
    discount_rate = 0.2 # マジックナンバー:割引率が直接コード内に埋め込まれている
    total_price = 1000 * quantity * (1 - discount_rate) # マジックナンバー:1000は商品の単価を表しているが、その意味が明示されていない
  elsif quantity > 5
    discount_rate = 0.1 # マジックナンバー:割引率が直接コード内に埋め込まれている
    total_price = 800 * quantity * (1 - discount_rate) # マジックナンバー:800は商品の単価を表しているが、その意味が明示されていない
  else
    total_price = 1000 * quantity # マジックナンバー:1000は商品の単価を表しているが、その意味が明示されていない
  end

  return total_price
end

マジックナンバーは避けるべきであり、代わりに意味を持つ定数や列挙型を使用することが推奨されます。

マジックナンバーを使わないコードに修正

修正されたコードでは、割引のしきい値や割引率、商品の単価などを定数として宣言しています。

マジックナンバーの代わりに意味を持つ定数を使用することで、コードの可読性や保守性が向上します。

DISCOUNT_THRESHOLD = 10
HIGH_DISCOUNT_RATE = 0.2
MID_DISCOUNT_RATE = 0.1
BASE_PRICE = 1000
MID_QUANTITY_THRESHOLD = 5
MID_PRICE = 800

def calculate_total_price(quantity)
  if quantity > DISCOUNT_THRESHOLD
    discount_rate = HIGH_DISCOUNT_RATE
    total_price = BASE_PRICE * quantity * (1 - discount_rate)
  elsif quantity > MID_QUANTITY_THRESHOLD
    discount_rate = MID_DISCOUNT_RATE
    total_price = MID_PRICE * quantity * (1 - discount_rate)
  else
    total_price = BASE_PRICE * quantity
  end

  return total_price
end

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky

単一責任の原則で密結合を解消する

はじめに

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

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

結合度

結合度(Coupling)は、モジュールやクラス間の相互依存の度合いを表す指標です。

密結合

密結合(Tight Coupling)とは、あるクラスが他の多くのクラスに強く依存している状態を指します。

密結合なコードは、他のクラスへの強い依存関係があるため、理解が難しくなります。

1つのクラスの変更が他のクラスにも影響を及ぼす可能性があり、修正や保守が困難になります。

また、テストや再利用性の面でも問題が生じることがあります。

疎結合

密結合と逆の意味である、疎結合(Loose Coupling)な構造では、クラス間の依存が少なく、互いに独立して変更が容易になります。

各クラスが独立して存在し、変更が局所的に行われるため、保守性や拡張性が高まります。

さらに、テストや再利用性も容易になります。

密結合と責務

密結合なコード例

書籍ではECサイトのコードを例に説明されていました。

  • 商品1点につき300円割引する
  • 上限20,000円まで商品を追加できる

上記の仕様のJavaのコードをRubyに置き換えして改変しています。

# 商品割引に関連するクラス
class DiscountManager
  attr_reader :discount_products, :total_price

  def initialize
    @discount_products = []
    @total_price = 0
  end

  # 商品を追加する
  def add(product, product_discount)
    # 引数の妥当性をチェック
    if product.id < 0
      raise ArgumentError, "無効な商品IDです"
    end
    if product.name.empty?
      raise ArgumentError, "商品名が空です"
    end
    if product.price < 0
      raise ArgumentError, "無効な商品価格です"
    end
    if product.id != product_discount.id
      raise ArgumentError, "商品IDが割引情報と一致しません"
    end

    discount_price = get_discount_price(product.price)
    tmp = product_discount.can_discount ? total_price - discount_price : total_price + product.price

    # 上限金額のチェック
    if tmp <= 20000
      @total_price = tmp
      @discount_products << product
      return true
    else
      return false
    end
  end

  # 割引価格を取得する
  def get_discount_price(price)
    discount_price = price - 300
    if discount_price < 0
      discount_price = 0
    end
    return discount_price
  end
end

# 商品クラス
class Product
  attr_reader :id, :name, :price

  def initialize(id, name, price)
    @id = id
    @name = name
    @price = price
  end
end

# 商品割引情報クラス
class ProductDiscount
  attr_reader :id, :can_discount

  def initialize(id, can_discount)
    @id = id
    @can_discount = can_discount
  end
end
  • add メソッドは商品と割引情報を受け取り、割引を適用して商品を追加します。引数の妥当性をチェックし、割引価格を計算して上限金額のチェックを行います。
  • get_discount_price メソッドは商品価格から割引価格を計算します。
  • Product クラスは商品の情報を表すクラスで、idnameprice の属性を持ちます。
  • ProductDiscount クラスは商品の割引情報を表すクラスで、idcan_discount の属性を持ちます。

問題点

通常割引以外に、夏季限定割引の仕様が追加されたとします。

夏季限定割引は上限30,000円まで商品追加が可能な仕様とする場合、以下のようにSummerDisountManagerクラスが実装します。

コード例

今回の場合、DiscountManagerのインスタンスを流用、割り引く仕様も通常割引と同じ300円であるため、DiscountManager.getDiscountPriceを流用して割引価格を計算しています。

addメソッドは金額上限が30,000円に変更されているため新たに定義しています。

# 夏季限定割引を管理するクラス
class SummerDiscountManager
  def initialize
    @discount_manager = DiscountManager.new  # 割引処理を担当するDiscountManagerのインスタンスを作成
  end

  # 商品を追加する
  # @param product [Product] 追加する商品
  # @return [Boolean] 追加に成功した場合は true、それ以外は false
  def add(product)
    if product.id < 0
      raise ArgumentError, "商品IDが無効です"
    end

    if product.name.empty?
      raise ArgumentError, "商品名が空です"
    end

    tmp = if product.can_discount
            @discount_manager.total_price - DiscountManager.get_discount_price(product.price)
          else
            @discount_manager.total_price + product.price
          end

    if tmp < 30000
      @discount_manager.total_price = tmp
      @discount_manager.discount_products << product
      return true
    else
      return false
    end
  end
end

# 商品クラス
class Product
  attr_accessor :id, :name, :price, :can_discount

  def initialize(id, name, price, can_discount)
    @id = id
    @name = name
    @price = price
    @can_discount = can_discount
  end
end
問題が発生するケース

「通常割引の割引価格を、300円から400円に変更する」というような仕様変更を行った場合にミスが発生しやすくなります。

  def get_discount_price(price)
    discount_price = price - 400 # 流用元のコードを書き換えしまったことに気が付かないことでバグが発生する
    if discount_price < 0
      discount_price = 0
    end
    return discount_price
  end

DiscountManagerの実装担当者は、割引計算をするget_discount_priceの400円に変更した場合

夏季割引サービスでも割り引かれる価格が400円になってしまいます。

夏季割引サービスを担うSummerDiscountManagerで、DiscountManagerのget_discount_priceが流用されることで意図しないバグが発生しやすくなります。

一部のクラスに処理が集中していたり、一方別のクラスでは何も処理を持っていなかったり、ほかのクラスの一部のメソッドを無理矢理都合よく流用したりしています。

これらは責務が考慮されていないクラスといえます。

単一責任の原則

ソフトウェア設計の原則の一つです。

あるクラスやモジュールは1つの責任を持つべきであり、それ以上の責任を持つべきではないという考え方です。

密結合を解消

単一責任の原則では、クラスやモジュールは個別の責任を持つため、他のクラスやモジュールとの結合度を低くすることで変更が発生した場合に影響範囲を最小限に抑えることができます。

責任の明確化

クラスやモジュールは明確に特定の責任を持つよう設計されます。

責任は、そのクラスやモジュールの役割や目的に関連しています。

例えば、データベースアクセス、ユーザー認証、データの変換など、個々の責任が明確に分離されます。

責務が単一になるようにクラスを設計

改修の方針
  • 商品の定価についてはRegularPriceクラスを用意
  • 価格に不正が発生しないよう、バリデーションロジックを持たせる(定価に責任を持つクラス構造)

バリデーションロジックがRegularPriceクラス内に凝集しているため、バリデーションロジックの重複コードが生じにくくなります。

定価クラス

定価を表すクラスであるRegularPriceを定義しています。

解説はコメントアウトに記述しています。 

# 定価クラス
class RegularPrice
  MIN_AMOUNT = 0 # 定価の最小値を表す定数

  attr_reader :amount # **amount**インスタンス変数への読み取り専用アクセスを提供し

  def initialize(amount) # 指定された価格をもとに**RegularPrice**オブジェクトを初期化
    if amount < MIN_AMOUNT
      raise ArgumentError, "価格が0以上ではありません。" # 引数**amount**が**MIN_AMOUNT**未満の場合は、**ArgumentError**例外をスロー
    end
    @amount = amount
  end
end

通常割引・夏季割引は別個にわけて定義

通常割引価格、夏季割引価格については、それぞれ個別に責任を負うクラスをつくります。

以下RegularDiscountedPrice、SummerDiscountedPriceも値オブジェクトとして設計します。

# 通常割引価格クラス
class RegularDiscountedPrice
  MIN_AMOUNT = 0
  DISCOUNT_AMOUNT = 400 # 通常割引の割引額を表す定数

  attr_reader :amount

  def initialize(price)
    discounted_amount = price.amount - DISCOUNT_AMOUNT
    discounted_amount = MIN_AMOUNT if discounted_amount < MIN_AMOUNT
    @amount = discounted_amount
  end
end

# 夏季割引価格クラス
class SummerDiscountedPrice
  MIN_AMOUNT = 0
  DISCOUNT_AMOUNT = 300 # 夏季割引の割引額を表す定数

  attr_reader :amount

  def initialize(price)
    discounted_amount = price.amount - DISCOUNT_AMOUNT
    discounted_amount = MIN_AMOUNT if discounted_amount < MIN_AMOUNT
    @amount = discounted_amount
  end
end

この設計では、SummerDiscountedPriceクラスが夏季割引価格を表現する専用のクラスとして独立しています。

割引後の価格の計算や制約は、SummerDiscountedPriceクラス内で完結していることにより、割引価格の計算ロジックが分散せず、単一の責務を持つクラスとなっています。

また、夏季割引の割引額や最低価格の設定を定数として別途定義しているため、変更が必要な場合には定数の値を変更するだけで済みます。

以上のような設計を心がけることで、クラスの役割が明確になり、単一責任の原則に基づいたコードを実現できます。

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky

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

はじめに

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

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

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

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

はじめに

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

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

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

Railsにおけるupdate_allとupdateの違い

背景

一括更新機能の実装を行った際に軽く調べた内容を書き留めておきたいと思いました。

本記事では、一括更新時のupdateとupdate_allに関する説明を簡単に解説していきます。

概要

update_allメソッド

update_allは、Active Recordのクエリインターフェースを使用して、条件に一致するレコードを一括更新するためのメソッドです。

SQLのUPDATE文に相当するものであり、複数のレコードを一度に更新することができます。

以下はupdate_allを用いて投稿の公開非公開フラグを一括で更新する例です。

def bulk_update_is_published
  Post.update_all(is_published: params[:is_published] == "1")
end

メリット - 高速である

モデルのコールバックやupdated_atカラムを更新しないため、処理が高速です。

レコードを一括更新する際にSQL文1回で済むことも処理が高速になる要因の一つです。

特に今回のようなテーブルのカラムすべてを一括更新する内容だと処理が重くなるケースがあります。(あまりないですが)

作業的に何度も切り替えたりするようなフラグであれば動作性の恩恵があるupdate_allは良いと思いました。

デメリット - バリデーションやコールバックが行われない

update_allは、一括更新時にRails上のバリデーションやコールバックはまったく動作しません。

一般的なバリデーション機能、updated_atカラムも更新しないため、手動で更新する必要があります。

update_allは処理が高速でメモリ消費も少なく、大量のレコードを更新する場合には有効なメソッドですが、注意点もあるため適切に使用する必要があります。

updateメソッド

updateは、Active Recordのインスタンスメソッドとして提供されるレコードを更新するためのメソッドです。

eachで投稿の公開非公開フラグを繰り返し更新する例です。

  def bulk_update_is_activate
    Post.all.each do |post|
      post.update(is_published: params[:is_published] == "1")
    end
  end

メリット - データの整合性を保つことが出来る

バリデーションやコールバックが実行されるため、データの整合性を保つことができます。

エラーが発生した場合に更新が中断されます。一方でupdate_allは、エラーが発生しても更新が完了してしまいます。

デメリット - パフォーマンスの低下

メモリの使用量が増加するため、大量のレコードを更新する場合はパフォーマンスが低下します。

デメリット - 処理速度の低下

更新ごとにSQL文が実行されるため、update_allに比べて処理が遅くなる可能性があります。

まとめ

私の場合は作業的に更新を切り替えするカラムだったためupdate_allが適切と思いましたが、updated_atが更新されたほうが嬉しいとのことでしたため、update_allはやめてupdateを繰り返し実行するように実装しました。

  • 単純かつ大量の一括更新を作業的に行うような場合にupdate_allを使用する
  • コールバックやエラーなどを正確にハンドリングを行いたい場合はupdateを使用する
  • update_allでupdated_atも一括更新するでもよかったのでしょうか。あまり変わらないかもしれませんが。(笑)

    おわりに

    コメント

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

    参考になったり学びのきっかけになりますと幸いです。

    間違いがありましたら修正いたしますので、ご指摘ください。

    興味があれば他の記事も更新していきますので是非ご覧になってください♪


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

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

    https://runteq.jp/r/ohtFwbjW

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

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

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

    https://twitter.com/outputky

    ポリシーパターン

    はじめに

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

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

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

    ネストで複雑化した分岐コード

    多重にネストし複雑化した分岐の解消にも役立ちます。

    ECサイトにおいて、優良顧客かどうかを判定するロジックです。

    顧客の購入履歴を調べ、次の条件をすべて満たす場合にゴールド会員と判定します。

    • これまでの購入金額が10万円以上であること
    • 1か月あたりの購入頻度が10回以上であること
    • 返品率が0.1%以内であること
    # ゴールド会員かどうかを判定するメソッド
    # @return [Boolean] ゴールド会員である場合はtrue
    # @param history [PurchaseHistory] 購入履歴
    def is_gold_customer?(history)
      if history.total_amount >= 100000 # これまでの購入金額が10万円以上であること
        if history.purchase_frequency_per_month >= 10 # 1か月あたりの購入頻度が10回以上であること
          if history.return_rate <= 0.001 # 返品率が0.1%以内であること
            return true
          end
        end
      end
      return false
    end
    

    次の条件をすべて満たす場合にシルバー会員と判定します。

    • 1か月あたりの購入頻度が10回以上であること
    • 返品率が0.1%以内であること
    # 会員ランクを判定するメソッド
    # @return [Boolean] シルバー会員である場合はtrue
    # @param history [PurchaseHistory] 購入履歴
    def is_silver_customer?(history)
      if history.purchase_frequency_per_month >= 10
        if history.return_rate <= 0.001
          return true
        end
      end
      return false
    end
    

    会員ランクの判定条件が一部ゴールド会員と同じであり、将来的にブロンズなどの会員ランクが追加され、同様の判定条件が存在する場合、現在の実装では同じ判定ロジックが複数の場所に散らばってしまいます。

    このような場合、保守性が低下するため判定ロジックの再利用性を高める方法が求められます。

    再利用性を高めるためには、共通の判定ロジックを別の場所にまとめ、必要な箇所から呼び出せるようにする必要があります。

    ポリシーパターンで条件を集約する

    条件の部品化、部品化した条件を組み替えてのカスタマイズを可能にします。

    # 優良顧客のルールを表現するインターフェース
    module ExcellentCustomerRule
      # 条件を満たす場合はtrueを返す
      def ok?(history)
        # 実際の条件判定ロジックを実装する必要があります
      end
    end
    
    ルール

    優良顧客のルールを表現するExcellentCustomerRuleモジュールをincludeしたクラスを定義します。

    • ゴールド会員の購入金額ルールを表すGoldCustomerPurchaseAmountRuleクラス
    • 購入頻度のルールを表すPurchaseFrequencyRuleクラス
    • 返品率のルールを表すReturnRateRuleクラスです。

    これらのクラスはExcellentCustomerRuleインターフェースを実装しており、**ok**?メソッドを持っています。

    # ゴールド会員の購入金額ルール
    class GoldCustomerPurchaseAmountRule
      include ExcellentCustomerRule
    
      def ok?(history)
        history.total_amount >= 100_000
      end
    end
    
    # 購入頻度のルール
    class PurchaseFrequencyRule
      include ExcellentCustomerRule
    
      def ok?(history)
        history.purchase_frequency_per_month >= 10
      end
    end
    
    # 返品率のルール
    class ReturnRateRule
      include ExcellentCustomerRule
    
      def ok?(history)
        history.return_rate <= 0.001
      end
    end
    

    Policyクラス

    RubyにおけるPolicyクラスは、特定のビジネスルールや方針を表現するためのクラスです。

    通常、条件判定や許可/禁止の制御ロジックをカプセル化し、再利用可能で柔軟な方法でアプリケーションの振る舞いを設定します。

    優良顧客の方針を表現するExcellentCustomerPolicyクラスを定義します。

    このクラスは、複数の優良顧客ルールを保持し、そのルールをすべて満たすかどうかを判定します。

    ExcellentCustomerPolicyクラスは、@rulesというインスタンス変数でルールの集合を管理しています。

    # 優良顧客の方針を表現するクラス
    class ExcellentCustomerPolicy
      def initialize
        @rules = Set.new # @rulesを空の**Set**オブジェクトで初期化
      end
    
      # ルールを追加する
      def add(rule)
        @rules.add(rule) # 引数として与えられたルールを**@rules**に追加
      end
    
      # 全てのルールを満たすか判定する
      def comply_with_all?(history)
        @rules.all? { |rule| rule.ok?(history) } # 各ルールに対して呼び出し全てのルールが満たされているか
     end
    end
    
    作成したPolicyの使い方

    ExcellentCustomerPolicyを作成し、ゴールド会員の条件判定

    gold_customer_policy = ExcellentCustomerPolicy.new
    gold_customer_policy.add(GoldCustomerPurchaseAmountRule.new)
    gold_customer_policy.add(PurchaseFrequencyRule.new)
    gold_customer_policy.add(ReturnRateRule.new)
    
    # purchaseHistoryを条件判定にかける
    if gold_customer_policy.comply_with_all?(purchase_history)
      # ゴールド会員の処理
      puts "この顧客はゴールド会員です。"
    else
      # ゴールド会員ではない顧客の処理
      puts "この顧客はゴールド会員ではありません。"
    end
    

    上記ノ通りif文が一つだけとなりロジックが単純化しました。

    ただ、この書き方でどこかのクラスにベタ書きしてしまうと、ゴールド会員以外の無関係なロジックを挿し込まれる可能性があるためまだ不安定な構造です。

    会員ごとのポリシーを作成しロジックを集約する

    ゴールド会員の条件をまとめたクラス

    ExcellentCustomerPolicyインスタンスを作成し、そのインスタンスGoldCustomerPurchaseAmountRulePurchaseFrequencyRuleReturnRateRuleインスタンスを追加しています。

    これにより、ゴールド会員の判定条件が設定されます。

    # ゴールド会員の方針を表現するクラス
    class GoldCustomerPolicy
      def initialize
        @policy = ExcellentCustomerPolicy.new
        @policy.add(GoldCustomerPurchaseAmountRule.new)
        @policy.add(PurchaseFrequencyRule.new)
        @policy.add(ReturnRateRule.new)
      end
    
      # 条件判定を行うメソッド
      def comply_with_all?(history)
        @policy.comply_with_all?(history)
      end
    end
    
    シルバー会員の条件をまとめたクラス

    SilverCustomerPolicyの初期化メソッドでは、ExcellentCustomerPolicyインスタンスを作成し、そのインスタンスPurchaseFrequencyRuleReturnRateRuleインスタンスを追加しています。

    これにより、シルバー会員の判定条件が設定されます。

    # シルバー会員の方針を表現するクラス
    class SilverCustomerPolicy
      def initialize
        @policy = ExcellentCustomerPolicy.new
        @policy.add(PurchaseFrequencyRule.new)
        @policy.add(ReturnRateRule.new)
      end
    
      # 条件判定を行うメソッド
      def comply_with_all?(history)
        @policy.comply_with_all?(history)
      end
    end
    

    今後それぞれの会員の条件に変更があれば、このGoldCustomerPolicyやSilverCustomerPolicyだけ変更すれば良くなります。

    参考

    続く…

    コメント

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

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


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

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

    https://runteq.jp/r/ohtFwbjW

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

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

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

    https://twitter.com/outputky

    switch文の重複によるアンチパターン

    はじめに

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

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

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

    switch文

    switch文は、プログラミング言語の制御構文の一つであり、特定の式や変数の値に基づいて、異なる条件に応じた処理を行うために使用されます。

    通常、複数のcase(条件)とそれに対応する処理が記述され、入力値が各条件と一致する場合に対応する処理が実行されます。

    Rubyはswitchではなくcase文

    Rubyでswitch文と同じような動作を行いたい場合はcase文やelse句が用いられます。

    def process_fruit(fruit)
      case fruit
      when 'apple'
        puts '選択されたフルーツ: りんご'
      when 'banana'
        puts '選択されたフルーツ: バナナ'
      when 'orange'
        puts '選択されたフルーツ: オレンジ'
      else
        puts '不明なフルーツ'
      end
    end
    
    # Usage
    process_fruit('apple')
    process_fruit('banana')
    process_fruit('grape')
    
    JavaScriptのswitch文
    def process_fruit(fruit)
      case fruit
      when 'apple'
        puts '選択されたフルーツ: りんご'
      when 'banana'
        puts '選択されたフルーツ: バナナ'
      when 'orange'
        puts '選択されたフルーツ: オレンジ'
      else
        puts '不明なフルーツ'
      end
    end
    
    # 使用方法
    process_fruit('apple')
    process_fruit('banana')
    process_fruit('grape')
    

    switch文は、if-else文や複数の条件分岐文の代替として使われることがあります。

    特定の値に応じて異なる処理を行いたい場合や、列挙型や定数を扱う際に有用です。

    switch文の注意点

    ただし、switch文の誤った使用方法によって可読性が低下したり、バグの発生源となったりすることがあります。

    下記はゲームで魔法を使用するロジックをRubyで書いたものです。

    # 魔法の名前を取得するクラス
    class MagicManager
      def get_name(magic_type)
        name = ""
    
        # 魔法のタイプによって名前を設定
        case magic_type
        when :fire
          name = "ファイア"
        when :shiden
          name = "紫電"
        end
    
        return name
      end
    
        # 魔法の消費マジックポイントを計算するメソッド
        def cost_magic_point(magic_type, member)
          magic_point = 0
        
          # 魔法のタイプによって消費マジックポイントを設定
          case magic_type
          when :fire
            magic_point = 2
          when :shiden
            magic_point = 5 + (member.level * 0.2).to_i
          end
        
          return magic_point
        end
    
        # 魔法の攻撃力を計算するメソッド
        def attack_power(magic_type, member)
          attack_power = 0
        
          # 魔法のタイプによって攻撃力を設定
          case magic_type
          when :fire
            attack_power = 20 + (member.level * 0.5).to_i
          when :shiden
            attack_power = 50 + (member.agility * 1.5).to_i
          end
        
          return attack_power
        end
    end
    

    MagicTypeで処理を切り替えるswitch文が3つも登場しています。

    同じ条件式のswitch文が複数実装されるのは良くない兆候です。

    追加漏れが発生しやすくなる

    仮に使用の追加で「アイスボール」という魔法が追加されたとき先程のコードだと3つのメソッドのcase文に修正が必要です。

    def get_name(magic_type)
      name = ""
      # --略--
      # 魔法のタイプによって名前を設定
      case magic_type
      when :ice_ball
        name = "アイスボール"
      end
      # --略--
    end
    
    def cost_magic_point(magic_type, member)
    
      # --略--
    
      # 魔法のタイプによって消費マジックポイントを設定
      case magic_type
      when :ice_ball
        magic_point = 16
      end
    
      # --略--
    
      return magic_point
    end
    
    def attack_power(magic_type, member)
      attack_power = 0
    
      # --略--
    
      # 魔法のタイプによって攻撃力を設定
      case magic_type
      when :ice_ball
        attack_power = 80 + (member.level * 0.5).to_i
      end
    
      # --略--
    
      return attack_power
    end
    

    上記のいずれかでも修正に漏れが発生すると簡単に不具合が発生してしまいます。

    • ダメージの加算がされていない
    • 技名が違う

    など。

    実際のゲームでは数十種類にとどまりません。

    処理の切り替え対象も魔法の説明文、攻撃範囲、命中率、属性、アニメーションなどによって増幅するでしょう。

    そういった場合に適切な対処が必要です。

    条件分岐を一箇所にまとめる

    switch文の重複コードを解消するには、単一責任選択の原則の考え方が重要です。

    同じ条件式の条件分岐を複数書かず、一箇所にまとめようとする原則です。

    この原則に従うことで、重複したコードを解消し、メンテナンス性や可読性を向上させることができます。

    改善例

    initializeメソッドを使用して、与えられた魔法の種類とメンバーオブジェクトに基づいて魔法の属性を設定しています。

    case文による条件分岐を使って、各魔法の属性を設定しています。 各属性はインスタンス変数として保持され、クラス外部から参照できるようになっています。

    class Magic
      attr_reader :name, :cost_magic_point, :attack_power, :cost_technical_point
    
      def initialize(magic_type, member)
        case magic_type
        when :fire
          @name = "ファイア"
          @cost_magic_point = 2
          @attack_power = 20 + (member.level * 0.5)
          @cost_technical_point = 0
        when :shiden
          @name = "紫電"
          @cost_magic_point = 5 + (member.level * 0.2)
          @attack_power = 50 + (member.agility * 1.5)
          @cost_technical_point = 5
        when :ice_ball
          @name = "アイスボール"
          @cost_magic_point = 16
          @attack_power = 200 + (member.magic_attack * 0.5 + member.vitality * 2)
          @cost_technical_point = 20 + (member.level * 0.4)
        else
          raise ArgumentError, "Invalid magic type"
        end
      end
    end
    

    switch文があちこち複数箇所に実装されず、一箇所にまとまっているので、仕様変更時の抜け漏れを抑止できます。

    interfaceやモジュールでよりスマートに

    単一責任選択の原則にもとづきswitch文は一箇所にまとまりましたが、切り替えたいものが増えた場合、ロジックは膨れ上がります。

    interfaceはJavaなどのオブジェクト指向言語特有のしくみで、機能の切り替えや差し替えを容易にします。interfaceを使うと、分岐ロジックを書かずに分岐と同じことが実現可能になります。そのため条件分岐が大幅に減り、ロジックがシンプルになります。

    書籍ではJavaのinterfaceが用いられていましたが、Rubyでは相当する機能として抽象クラスやモジュールがあります。

    改善例

    Magicクラスを抽象クラスとして定義し、魔法を表現する各クラスがそれを継承しています。抽象クラスでは各メソッドを定義しているが、具体的な実装は各サブクラスに任せています。

    # 魔法を表現する抽象クラス
    class Magic
      def initialize(member)
        @member = member
      end
    
    # 魔法「ファイア」を表現するクラス
    class Fire < Magic
      def name
        "ファイア"
      end
    
      def cost_magic_point
        2
      end
    
      def attack_power
        20 + (@member.level * 0.5).to_i
      end
    
      def cost_technical_point
        0
      end
    end
    
    # 魔法「紫電」を表現するクラス
    class Shiden < Magic
      def name
        "紫電"
      end
    
      def cost_magic_point
        5 + (@member.level * 0.2).to_i
      end
    
      def attack_power
        50 + (@member.agility * 1.5).to_i
      end
    
      def cost_technical_point
        5
      end
    end
    
    # 魔法「アイスボール」を表現するクラス
    class IceBall < Magic
      def name
        "アイスボール"
      end
    
      def cost_magic_point
        16
      end
    
      def attack_power
        200 + (@member.magic_attack * 0.5 + @member.vitality * 2).to_i
      end
    
      def cost_technical_point
        20 + (@member.level * 0.4).to_i
      end
    end
    
    改善例: 条件分岐の処理

    改善された結果の条件分岐コードは以下になります。

    # 魔法の種類に応じて該当する魔法オブジェクトを生成し、データを取得するメソッド
    def get_magic_data(magic_name, member)
      case magic_name
      when "ファイア"
        Fire.new(member)
      when "紫電"
        Shiden.new(member)
      when "アイスボール"
        IceBall.new(member)
      else
        raise ArgumentError, "有効な名前ではありません。"
      end
    end
    

    このようにクラスを用いることでswitch文がスッキリします。

    参考

    続く…

    コメント

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

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


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

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

    https://runteq.jp/r/ohtFwbjW

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

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

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

    https://twitter.com/outputky