Yanonoblog!

こつこつと

出力引数が引き起こす問題

はじめに

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

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

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

出力引数 - アンチパターン

前回の記事で解説した共通処理クラスの例にもあるように、引数の扱いを誤ると低凝集に陥りやすいです。

低凝集を引き起こす

下記のコードでは、ActorManager クラスにおいて位置を移動させるための shift メソッドを定義しています。

class ActorManager
  # ゲームキャラの位置を移動する
  def shift(location, shift_x, shift_y)
    location.x += shift_x
    location.y += shift_y
  end
end

移動対象のインスタンスであるlocationを出力引数として渡し、変更しています。

データ操作対象はLocation、操作ロジックはActorManager、といった形で別々のクラスに定義されているため、この構造も低凝集として重複を生みやすいアンチパターンのコードといえます。

全く同じメソッドが別のクラスに実装される例
class SpecialAttackManager
  # ゲームキャラの位置を移動する
  def shift(location, shift_x, shift_y)
    location.x += shift_x
    location.y += shift_y
  end
end
補足 - 出力引数

関数やメソッドの引数の一種であり、関数内で処理された結果を引数経由で外部に返すために使用されます。

先程のコードを例に取ると、shift メソッドでは位置情報を変更するために location 引数を受け取っています。

この引数はメソッド内で変更され、結果として位置情報が変更された状態で外部に返されます。

一般的には、関数やメソッドが値を返す場合は戻り値を使用し、出力引数は避けることが推奨されます。

戻り値を使用することで、関数の使用側が戻り値を受け取り、処理結果を利用できるため、コードの可読性や保守性が向上します。

引数が破壊的に変更される内容が外部からわからない

下記のコードでは割引額を設定するsetメソッドを使用していますが、出力引数を用いており、どのように変更処理が加えられているか分からずメソッド側のコードを確認しにいく必要が発生します。

# MoneyDataインスタンスを生成
money_data = MoneyData.new(1000)

# DiscountManagerインスタンスを生成
discount_manager = DiscountManager.new

# 金額を設定する
discount_manager.set(money_data)

puts "割引後の金額: #{money_data.amount}"

ロジックは下記の通りです。引数として渡したmoneyの金額値を変更しています。

引数は入力値として受け渡すのが普通です。このように出力値として扱ってしまうと、引数が入力なのか出力なのか、メソッド内部のロジックを読んで確認しなければなりません。

class DiscountManager
  # 割引を適用する
  def set(money_data)
    money_data.amount = 2000
    if money_data.amount < 0
      money_data.amount = 0
    end
  end
end

class MoneyData
  attr_accessor :amount
  
  def initialize(amount)
    @amount = amount
  end
end

メソッドの中身をいちいち気にしなければならない構造は、ロジックを読み解く時間が増え、可読性の低下を招きます。

出力引数として設計せず、オブジェクト指向設計の基本にもとづいてデータとデータを操作するロジックを同じクラスに凝集するのが好ましいです。

改善例: 引数を変更しない構造にする - 割引の実装例

修正したコードでは、MoneyDataクラスにapply_discountメソッドを追加しました。

このメソッドは、割引を適用した新しいMoneyDataインスタンスを返すため、元のインスタンスmoney_dataは変更せずに、割引が適用された新しいインスタンスdiscounted_dataを取得することができます。

# MoneyDataインスタンスを生成
money_data = MoneyData.new(1000)

# DiscountManagerインスタンスを生成
discount_manager = DiscountManager.new

# 金額を設定する
discount_manager.set(money_data)

puts "割引後の金額: #{money_data.amount}"

class DiscountManager
  # 割引を適用する
  def set(money_data)
    money_data.amount = 2000
    if money_data.amount < 0
      money_data.amount = 0
    end
  end
end

class MoneyData
  attr_accessor :amount
  
  def initialize(amount)
    @amount = amount
  end
end

改善例: 引数を変更しない構造にする - 位置移動の実装例

Location クラスの shift メソッドが引数を変更せずに新しい Location インスタンスを返すようにします。

これにより、元の位置情報が変更されずに新しい位置情報が生成されるような構造となります。

class Location
  attr_reader :x, :y
  
  def initialize(x, y)
    @x = x
    @y = y
  end

  def shift(shift_x, shift_y)
    next_x = @x + shift_x
    next_y = @y + shift_y
    return Location.new(next_x, next_y)
  end
end

# Locationインスタンスを生成
location = Location.new(0, 0)

# 位置情報を移動する
new_location = location.shift(10, 20)

puts "移動前の位置情報: (#{location.x}, #{location.y})"
puts "移動後の位置情報: (#{new_location.x}, #{new_location.y})"

shift メソッド内で現在の位置情報 (@x@y) に shift_xshift_y を加算し、新しい位置情報を生成しています。そして、新しい位置情報を表す新たな Location インスタンスを返しています。

これにより、shift メソッドの呼び出し側は元の位置情報を保持したまま、移動後の位置情報を新しいインスタンスとして取得することができます。これにより、引数の変更を避けることができ、コードの可読性や保守性が向上します。

参考

続く…

コメント

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

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


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

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

https://runteq.jp/r/ohtFwbjW

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

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

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

https://twitter.com/outputky