「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_readerattr_writeattr_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_readerattr_writerattr_accessor」を使用します。自前での実装と比較して、シンプルに定義ができます。
3つのメソッドは、アクセスする用途ごとに使い分けます。

以下のコードは、自前実装で作成した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ローカル変数として解釈されます。 ローカル変数について、るりまの説明では以下のように書かれています。

変数と定数 (Ruby 3.1 リファレンスマニュアル)

小文字で始まる識別子への最初の代入はそのスコープに属するローカル変数の宣言になります。

そのため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メソッドはJavaC#privateメソッドとは異なる仕様があります。この点は、伊藤さんの記事が詳しいのでこちらを参照して下さい。

blog.jnito.com

また、チェリー本にはprivateメソッドの細かい使い方が解説していあります。

まとめ

今回はリファクタリング本の対立関係「getter、setter」と「インスタンス変数の直接アクセス」を比較する前段として、書き方を紹介しました。

書き方だけを比較すると、「インスタンス変数の直接アクセス」の方が簡易的なので、これ一択のようにも思えます。 しかし、リファクタリングのし易さや可読性の観点から見ると、一概にベストな選択とは言い切れなさそうです。

次回は、書き方以外の観点で比較してます!