オブジェクト指向設計実践ガイド~Rubyでわかる 進化しつづける柔軟なアプリケーションの育て方を読んでいて、インスタンス変数とアクセサメソッドの違いに関して興味を持ったのでまとめます。
詳細の内容は、本書の第二章の『単一責任のクラスを設計する』に書かれています。
オブジェクトとデータ
オブジェクトは「データ」と「振る舞い」を持ちます。
データは、任意のデータ(文字列、ハッシュ、配列など)をインスタンス変数内に保持します。
データへのアクセスは次のうちのどちらかの方法で行われます。
- インスタンス変数を直接参照する
- インスタンス変数をアクセサメソッドで包み隠す
それぞれに関してみていきましょう。
①インスタンス変数を直接参照する
Gearクラスにギア比を計算するratioメソッドを定義しています。
class Gear
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
@chainring / @cog.to_f # 破滅への道
end
end
メソッドratioは何がダメなのでしょうか?
理由は、インスタンス変数を直接参照しているからです。
例えば、「@cog」のデータ(内容)を変更する場合を考えてみます。
「@cog」を複数箇所で参照していた場合、全ての箇所で「@cog」のデータを変更する必要があります。
下記のサンプルコードをみてみましょう。
class Gear
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
@chainring / @cog.to_f # @cogを使用
end
def ratio_second
# 何らかの処理
@chainring / @cog.to_f # @cogを使用
end
end
予期せぬ調整が入った場合、下記2箇所でメソッドの変更が必要になります。
class Gear
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def unanticipated_adjustment_factor # 予期せぬ調整
2
end
def ratio
@chainring / (@cog * unanticipated_adjustment_factor).to_f # 変更が必要
end
def ratio_second
# 何らかの処理
@chainring / (@cog * unanticipated_adjustment_factor).to_f # 変更が必要
end
end
これは、データ(どこからでも参照される)から振る舞い(1箇所で定義される)へと変えることによって解決することができます。
独自のメソッドを用意し、振る舞いを変えて対応させます。
そして、クラス内では常にデータではなく、振る舞いに依存させます。
②インスタンス変数をアクセサメソッドで包み隠す
データではなく振る舞いに依存させるためには、このように書き換えます。
class Gear
attr_reader :chainring, :cog # 追加
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
chainring / cog.to_f # 変更
end
end
この場合、サンプルコードはどのように変わるでしょうか?
class Gear
attr_reader :chainring, :cog
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
chainring / cog.to_f # cogメソッドを使用
end
def ratio_second
# 何らかの処理
chainring / cog.to_f # cogメソッドを使用
end
end
メソッド(振る舞い)に依存させることによって、予期せぬ変更に対して変更箇所は1箇所で終了します。
class Gear
attr_reader :chainring, :cog
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def cog
@cog * unanticipated_adjustment_factor # ここのみを変更
end
def unanticipated_adjustment_factor # 予期せぬ調整
2
end
def ratio
chainring / cog.to_f # ここは変更しなくて良い
end
def ratio_second
# 何らかの処理
chainring / cog.to_f # ここは変更しなくて良い
end
end
@kts_h さんからサンプルコードに関してご指摘をいただきました!ありがとうございます!
また、「歯車(cog)の数が不正確な場合」の記述例を挙げていただきました。
※詳細はコメント欄で確認できます。
用語
参考として使っているメソッドを簡単にまとめます。
attr_reader
attr_readerを使うと、Rubyは自動で下記メソッドをつくります。
# attr_readerによるデフォルトの実装
def cog
@cog
end
参考:Module#attr_reader (Ruby 3.0.0 リファレンスマニュアル)
このcogメソッドは、コードの中で唯一cogが何を意味するか?を知っています。
このメソッドを実装することによって、データが振る舞い(1箇所で定義される)へと変わります。
そのため、データである@cogを変更する必要が出た場合は、「@cog」に関して複数箇所の変更をする必要がなく、コグについての独自メソッドを定義するだけでよくなります。
def cog
if 条件
@cog * 特殊な計算が発生
else
@cog
end
end
initialize
Rubyのコンストラクタです。
コンストラクタはオブジェクト生成時に自動的に呼び出され、クラスのデータ初期化処理を行う特別なメソッドです。
参考:Object#initialize (Ruby 3.0.0 リファレンスマニュアル)
【おまけメモ】 attr_readerを利用時の挙動の違い
initialize という名前のメソッドは自動的に private に設定されます。
そのため、クラスの外からアクセスすることができません。
class Gear
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
@chainring / @cog.to_f
end
end
gear1 = Gear.new(10, 20)
puts gear1.cog
`<main>': undefined method `cog' for #<Gear:0x00007fb49015a968 @chainring=10, @cog=20> (NoMethodError)
クラス外からアクセスするにはゲッター/セッターメソッドが必要です。
class Gear
attr_reader :cog
def initialize(chainring, cog)
@chainring = chainring
@cog = cog
end
def ratio
@chainring / @cog.to_f
end
end
gear1 = Gear.new(10, 20)
puts gear1.cog
=> 20
attr_readerを使うとこのような挙動の違いが発生します。
【まとめ】データではなく、振る舞い(メソッド)に依存させる
振る舞い(メソッド)に依存させることによって、変更しやすいコードにすることができました。
インスタンス変数の隠蔽
インスタンス変数は常にアクセサメソッドで包み、直接参照しないようにすることが本書で推奨されています。
【疑問】 振る舞いではなく、データに依存させてみる
変更をしやすくするための解決方法として、振る舞いに依存させず、データを変えることによって対応させる方法はどうでしょうか?
class Gear
def initialize(chainring, cog)
@chainring = chainring
if 条件
@cog = cog * 特殊な計算が発生
else
@cog = cog
end
end
def ratio
@chainring / @cog.to_f
end
end
initializeというメソッドはあくまでも「データの初期化という役割」を果たすため、「データの中身に対する処理」を持つのは適切ではないため、このパターンはアンチパターンなのではないでしょうか?
本書を読み進めていくうえで理解が深まり、アンチパターンであるという理由が言語化されることを期待します。
コードや内容に関して不備や改善点等ございましたら、ご指摘お待ちしております。