読者です 読者をやめる 読者になる 読者になる

ネットの海の片隅で

技術ネタの放流、あるいは不法投棄。

「privateな変数にpublicなアクセサを定義する」?

きっかけ

こんなツイートがRTで流れてきた。

そのとき思ったこと

変数ありきなのではなく、インターフェイスありき。

インスタンスが持っている変数は実装の詳細であって、そのインスタンスメソッドを呼び出す側からするとどうでも良いし、むしろ隠蔽したい。

前から思っていること

オブジェクト指向の入門書ではよく「変数はprivateにして、publicなアクセサを定義するんですよー」って書いてあるけど、これだと「変数をpublicにしてアクセサ無くしても一緒じゃん」ってなるのも仕方ないと思う。*1

署名機

たとえば、与えられた文字列に対してあるキーを使って署名するオブジェクトがあるとする。イメージとしてはこんな感じ。

signatory = Signatory.new
signatory.key = "SECRET_KEY"
signatory.sign("foo") #=> "sdjhfawp980763hreaflidao8fgpaw"
signatory.sign("bar") #=> "65vn7nybf89r4bcf93473ed2ex2e"
signatory.key = "ANOTHER_KEY"
signatory.sign("foo") #=> "apcsdufcps8up89pc9asdpfc8adjp"

このオブジェクトは内部に@keyのような変数を持つことになるだろうけど、だからといってsignatory.key #=> "SECRET_KEY"となるのは秘密にすべきキーの情報がダダ漏れで非常によろしくない。

つまり、このオブジェクトは

  • 文字列を渡すとキーを使って署名した文字列を返す
  • 署名に使うキーは外部から設定できる
  • 設定されたキーを外部から読み取ることはできない

という仕様を満たす必要があって、そのためには

  • Signatory#sign
  • Signatory#key=

というpublicなメソッドが必要になる一方で、Signatory#keyというメソッドは提供してはならない。

この外から見た時の仕様あるいは振る舞いを満たすということが重要で、逆に言えば上記の仕様さえ満たすのであれば@keyというインスタンス変数を使わずに、

class Signatory
  def key=(key)
    @x = key
  end

  def sign(str)
    # @xを使ってstrに署名するコード
  end
end

という実装にしても、このクラスを使用する外部のプログラムは何一つ困ることがない。 *2

このように外部に提供するメソッドから考えると「private変数にアクセサを定義する」という考え方は出にくいように思う。

Wrapper

他の例としてWrapperを考える。

HTTPで通信するクライアントを作るとして、そのクライアントにURIを"http://example.com/path/to/file"という形式で渡したいとする。イメージはこんな感じ。

client = HogeClient.new
client.uri = "http://example.com/path/to/file"
client.connect

このとき欲しいメソッド

  • HogeClient.uri=
  • HogeClient.connect

になるため、このクラスはこれらのメソッドを提供すれば良いことになる。

さて、外部に提供するメソッドが決まったので実装に移るが、クライアント内部で使用する汎用的なHTTPライブラリはURI文字列ではなく、URIに含まれる各要素を別々に指定する必要があるとすると、次のような実装になるかもしれない。

class HogeHttpClient
  def uri=(uri)
    @protocol = ... # parse URI
    @host = ... # parse URI
    @port = ... # parse URI
    @path = ... # parse URI
  end

  def connect
    http = CommonHttpClient.new(@host, @port, @path)
    http.ssl = true if @protocol == "https"
    http.connect
  end
end

このとき、このクラスは#uri=というSetterを持っているが、@uriという変数は保持していない。また、@protocol, @host, @port, @pathというインスタンス変数は保持しているが、それらへのアクセサは持っていない。

つまり……?

基本

Setterとは

  • 変数をSetするのではなく、オブジェクトの持つ状態をSetする
  • 変数にSetterを定義するのではなく、Setterの値を変数で受ける
  • Setterの名前とインスタンス変数の名前が一致するとは限らない

最後に

あまり上手く書けませんでしたが、RTされてきたツイートを見て感じたことをまとめてみました。 間違っている点などあると思いますので、ご指摘お待ちしております。

*1:Rubyだとインスタンス変数に無条件でattr_accessorつけるとか。

*2:もちろん、実際はクラス内部でのリーダビリティを考えてキーを保持するインスタンス変数は@keyにする。