「getter,setter (attr)」と 「インスタンス変数の直接アクセス」を比較してみた(書き方編)
はじめに
こんにちは、Webエンジニアを目指してフィヨルドブートキャンプ(FBC)で学習をしてますwataです。
ここ最近は、FBCのカリキュラム以外にも、リファクタリング本 *1 *2を学習中です。
リファクタリング本には、対立関係の項目が目立ちます。例えば、「引数の追加」と「引数の削除」です。 この対立関係において筆者は、片方の方法が絶対的な最適解ではない、つまり銀の弾丸ではないことを主張してます。
その一方で、私がコーディングをする際に使用しない項目も散見されます。本を読むだけだと、「本当にこの項目は、リファクタリングをするほどの価値があるのかな🤔」と思うことがあります。
そこで私が気になった、対立関係にある項目を、自分なりに比較してみることにしました。 対象は、タイトルにある「getter、setter」と「インスタンス変数の直接アクセス」を扱います。
本記事では前段として、Rubyでの「getter、setter」の書き方を紹介したいと思います。
インスタンス変数の直接アクセス
インスタンス変数を直接操作する方法は、ローカル変数とさほど変わりません。
インスタンス変数の先頭に@
を付けるだけです。簡単ですね!
サンプルコードにNameクラスを用います。名前(first_str)と名字(last_str)を引数に取ります。
class Name def initialize(first_str, last_str) # インスタンス変数へ直接代入する @first = first_str @last = last_str end def to_s # インスタンス変数を直接参照する "#{@last} #{@first}" end end
getter、setter
前提として、今回取り上げるgetter、setterは、クラス内部に限った話をします。 何故ならRubyは、「インスタンス変数の直接アクセス」がクラス外部から出来ないためです。
getter、setterとは、インスタンス変数をメソッド経由でアクセスする方法です。
皆さんお馴染みのアクセスメソッド(attr_reader
、attr_write
、attr_accessor
)もgetter、setterになります。
注意点として、getter、setterはインスタンス変数を加工するようなロジックは含みません。あくまで値を取り出す、値を代入するだけです。
# getter def first @first end # getterでない def uppercased_first @first.upcase end
リファクタリング本では、インスタンス変数自身をメソッドでカプセル化することから、フィールドの自己カプセル化
、自己カプセル化フィールド
などと呼称してます。
定義方法
getter、setterの定義方法について説明します。定義方法は「自前実装」と「アクセスメソッド」の2パターンがあります。
自前実装
後述するアクセスメソッドを用いずに自前で実装する方法です。とは言ったものの、コード量は少なく実装方法は簡単です。
class Name def initialize(first_str, last_str) @first = first_str @last = last_str end # getter def first @first end # setter def first=(first_str) @first = first_str end end # @lastについては省略
アクセスメソッド
アクセスメソッドの「attr_reader
、attr_writer
、attr_accessor
」を使用します。自前での実装と比較して、シンプルに定義ができます。
3つのメソッドは、アクセスする用途ごとに使い分けます。
- attr_reader :インスタンス変数の読み込み専用(getter)
- attr_writer:インスタンス変数の書き込み専用(setter)
- attr_accessor :インスタンス変数の読み書き両方(getter、setter)
以下のコードは、自前実装で作成したgetter、setterをattr_accessor
に置き換えたものになります。
attr_accessor
メソッドを呼び出すだけです。
class Name attr_accessor :first, :last def initialize(first_str, last_str) @first = first_str @last = last_str end end
呼び出し方
呼び出し方は、自前実装とアクセスメソッドで共通です。アクセスメソッドが自前実装を定義しているためです。
getter
メソッドの呼び出し方法は、通常の関数の呼び出し方法と同様です。
class Name attr_accessor :first, :last def initialize(first_str, last_str) @first = first_str @last = last_str end def uppercased_first # getterでの呼び出し first.upcase end end name = Name.new('wata', 'fjord') # 大文字に変換される name.uppercased_first #=> WATA
ここで注意すべきなのが、setterです!
インスタンス変数の初期化を、以下のようなsetterメソッドの呼び出し方法で置き換えてみます。
class Name attr_accessor :first, :last def initialize(first_str, last_str) first = first_str # setterのつもりで呼び出している @last = last_str end def uppercased_first first.uppcase end end name = Name.new('wata', 'fjord') # 大文字に変換されない! name.uppercased_first #=> undefined method `uppcase' for nil:NilClass (NoMethodError)
new
メソッドの第1引数(first_str)に文字列'wata'
を与えたのに、uppercased_first
では例外が発生しました。。。
実は、初期化で呼び出したはずのfirst
はsetterメソッドではないんです。first
はローカル変数として解釈されます。
ローカル変数について、るりまの説明では以下のように書かれています。
小文字で始まる識別子への最初の代入はそのスコープに属するローカル変数の宣言になります。
そのためinitialize
メソッド内では、ローカル変数first
の値は'wata'
ですが、インスタンス変数@first
の値はnilです。
class Name def initialize(first_str, last_str) first = first_str #=> "wata" @last = last_str p @first #=> nil end end name = Name.new('wata', 'fjord')
name.uppercased_first
メソッドを呼び出したのに例外が発生した理由は、 @first
に文字列"wata"が代入されず、値がnilの状態でuppcase
メソッドを呼び出したためです。
この問題を解消するために、クラス内部でsetterメソッドを呼び出すためにself
を用います。self
は実行中のメソッドのレシーバーになるオブジェクトを指します。
オブジェクト.メソッド名
と記述することで明示的にメソッドであることを伝えます。
class Name attr_accessor :first, :last def initialize(first_str, last_str) self.first = first_str # setterを利用 @last = last_str end def uppercased_first first.upcase end end name = Name.new('wata', 'fjord') name.uppercased_first #=> 'WATA'
getter、setterのアクセスをprivateにする方法
インスタンス変数への操作方法にgetter、setterを紹介しました。
しかし、上述で紹介した方法は、クラス外部からのアクセスが可能になってしまいます。アクセスするスコープをクラス内部限定にしたい場合もあると思います。
その場合は、private
メソッドを呼び出します。
private
メソッド以降に定義したメソッドは、クラス内のみのアクセスになります。
class Name def initialize(first_val, last_val) @first = first_val @last = last_val end def lowercased_first # クラス内部からのアクセス first.downcase end private attr_accessor :first, :last end name = Name.new('wata', 'fjord') name.lowercased_first #=> 'WATA' # クラス外部からのアクセスは不可能 name.first #=> private method `first' called for #<Name:0x00007fe7d70aae30 @first="wata", @last="fjord"> (NoMethodError)
1つ注意点として、private
メソッドはJavaやC#のprivate
メソッドとは異なる仕様があります。この点は、伊藤さんの記事が詳しいのでこちらを参照して下さい。
また、チェリー本にはprivate
メソッドの細かい使い方が解説していあります。
まとめ
今回はリファクタリング本の対立関係「getter、setter」と「インスタンス変数の直接アクセス」を比較する前段として、書き方を紹介しました。
書き方だけを比較すると、「インスタンス変数の直接アクセス」の方が簡易的なので、これ一択のようにも思えます。 しかし、リファクタリングのし易さや可読性の観点から見ると、一概にベストな選択とは言い切れなさそうです。
次回は、書き方以外の観点で比較してます!