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プロジェクトでインスタンス変数を使っているモジュールがあるなら、引数を使うことでコード品質を向上できないか、検討してみることをお勧めします。
