「privateな変数にpublicなアクセサを定義する」?
きっかけ
こんなツイートがRTで流れてきた。
Java技術者です、って求職してきた人にする質問の一問目は
「なぜフィールドはprivateにしてgetter/setterプロパティを書くべきなのだと思いますか?」
がいいのではないかと思い始めた。
— ゆば大好き (@yuba) 2014, 9月 26
そのとき思ったこと
「変数をプライベートにして、それに対してアクセサを定義する」っていうのはちょっと違う気がする。
— S.Osa (@osa522) 2014, 9月 26
「外部に公開するインターフェイスを決定して、そのために必要な変数をプライベートで保持する」っていう感じ。
— S.Osa (@osa522) 2014, 9月 26
変数ありきなのではなく、インターフェイスありき。
インスタンスが持っている変数は実装の詳細であって、そのインスタンスのメソッドを呼び出す側からするとどうでも良いし、むしろ隠蔽したい。
前から思っていること
オブジェクト指向の入門書ではよく「変数は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されてきたツイートを見て感じたことをまとめてみました。 間違っている点などあると思いますので、ご指摘お待ちしております。