ITエンジニアによるITエンジニアのためのブログ

Rubyのモジュール・メソッドでインスタンス変数より引数を使うべき理由

Rubyのモジュール・メソッドでインスタンス変数を使用するコードを時々見かけますが、これは推奨されるプラクティスとは言えません。多くの場合、代わりに引数を使用することで、より質の高いコードになります。

その理由として、以下の4点が挙げられます。

  • コード可読性の向上
  • 副作用の減少
  • テストの容易性
  • 再利用性の向上

この記事では、それぞれの理由についてコード例を用いて解説します。

コード可読性の向上

モジュールメソッドでインスタンス変数を使うと、データの流れを追跡するのが困難になる可能性があります。

例えば、以下のコードを見てください。

module Example
  def even_number?
    @number.even?
  end
end

class Numbering
  include Example

  def initialize(number)
    @number = number
  end

  def check_number
    if even_number?
      puts "偶数です。"
    else
      puts "偶数ではありません。"
    end
  end
end

number = Numbering.new(1)
number.check_number

上記のコードでは、インスタンス変数@numberが使用されていますが、モジュール内にはその定義がなく、クラスで定義されています。これでは、モジュールファイルを読んだだけでは、一体このインスタンス変数がどこで定義されているのか分からず、データの流れを追うのが困難です。

一方、引数を使って書き直したコードがこちらです。

module Example
  def even_number?(number)
    number.even?
  end
end

class Numbering
  include Example

  def initialize(number)
    @number = number
  end

  def check_number
    # @numberを引数として明示的に渡す
    if even_number?(@number)
      puts "偶数です。"
    else
      puts "偶数ではありません。"
    end
  end
end

number = Numbering.new(1)
number.check_number

上記の例では、メソッドコードの1行目を読んだだけで、メソッドが必要としているデータが全て把握できます。そのため、別のクラスファイルを開くことはもちろん、同じモジュールの他の場所を確認する必要さえありません。

副作用の減少

モジュールでインスタンス変数を使用すると、意図せずオブジェクトの状態を変えてしまう可能性があります。

例えば、以下のコードを見てみましょう。

module Counter
  def self.increment
    @count ||= 0
    @count += 1
  end

  def self.value
    @count
  end
end

puts Counter.increment # 出力: 1
puts Counter.increment # 出力: 2
puts Counter.value     # 出力: 2

module AnotherModule
  def self.use_counter
    Counter.increment
    puts Counter.value
  end
end

AnotherModule.use_counter # 出力: 3
puts Counter.value     # 出力: 3

意図としては、AnotherModuleモジュールで0からカウントを始めたかったのに、Counterモジュール自身でインスタンス変数の状態を保持してしまっているため、意図しない数が出力されてしまっています。

これは、以下のようにインスタンス変数の代わりに引数を使って書き直すことで、副作用を防ぐことができます。

module Counter
  def self.increment(count = 0)
    count + 1
  end

  def self.value(count)
    count
  end
end

count = 0
count = Counter.increment(count)
puts count # 出力: 1
count = Counter.increment(count)
puts count # 出力: 2
puts Counter.value(count)     # 出力: 2

module AnotherModule
  def self.use_counter(c)
    c = Counter.increment(c)
    puts Counter.value(c)
    return c
  end
end
c = 0
c = AnotherModule.use_counter(c) # 出力: 1
puts Counter.value(c)     # 出力: 1
count = 5
count = AnotherModule.use_counter(count) # 出力: 6
puts Counter.value(count)     # 出力: 6

このバージョンのモジュールではインスタンス変数を使用しないため、状態を保持せず、メソッドの戻り値は引数のみで決定されるため、意図しない副作用を引き起こす心配はありません。

テストの容易性

インスタンス変数を使ったモジュールメソッドでは、テスト前にインスタンス変数を設定してからテストを実行する必要があります。

require 'minitest/autorun'

module Config
  def self.set_value(val)
    @value = val
  end

  def self.get_value
    @value
  end
end

class ConfigTest < Minitest::Test
  def test_get_value
    Config.set_value("test")
    assert_equal "test", Config.get_value
  end
end

上記のモジュールではセッターメソッドがあるだけまだましですが、ない場合はさらに設定に手間がかかります。

一方、インスタンス変数を使わないで引数のみのモジュールメソッドにした場合、このようなテスト前の設定は不要になります。

require 'minitest/autorun'

module Config
  def self.get_value(val)
    val
  end
end

class ConfigTest < Minitest::Test
  def test_get_value
    assert_equal "test", Config.get_value("test")
  end
end

再利用性の向上

インスタンス変数を使ったモジュールは、ミックスインされるクラスのオブジェクトにそのインスタンス変数が存在する前提で書かれています。

しかし、モジュール利用者の観点からはそのような前提は明確ではありませんし、たとえその前提が理解できたとしても、ミックスインするクラスでそのようなインスタンス変数を設定したくないかもしれません。そのような場合、モジュールの再利用性を制限してしまうことになりかねません。

module DataProcessor
  def process
    @data.map(&:upcase) # @dataが存在する前提
  end
end

# @dataが存在しないクラスでは使えない。
class MyClass
  include DataProcessor
end

my_instance = MyClass.new
my_instance.process #  undefined method `map' for nil:NilClass (NoMethodError)

インスタンス変数の代わりに引数を使えば、自身のクラスで不要なインスタンス変数を定義する必要がなくなるので、モジュールが利用しやすくなります。

# 引数を使うように修正
module DataProcessor
  def process(data)
    data.map(&:upcase)
  end
end

class MyClass
  include DataProcessor
end

my_instance = MyClass.new
puts my_instance.process(['a', 'b'])

まとめ

このように、モジュール・メソッドでインスタンス変数ではなく引数を使うことは、コード可読性の向上、副作用の減少、テストの容易性、再利用性の向上というメリットがあります。

もし自身のRubyプロジェクトでインスタンス変数を使っているモジュールがあるなら、引数を使うことでコード品質を向上できないか、検討してみることをお勧めします。