Yanonoblog!

こつこつと

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