概要
Rubyにおいて、外部から無制限にオブジェクト生成させないために、特異メソッドnew
をプライベートにする方法が知られていますが、そうすると、インスタンスメソッドからのオブジェクト生成にも、外部からと同じ制約がかかってしまいます。
これは、外部からは制約あり、インスタンスメソッドからは制約なしでオブジェクト生成したい場合の障害となります。
そこで、この障害を回避する手段はないか? と考えました。
なお、テストはruby 2.3.1p112(Windows10 WSL/Ubuntu 16.04.2 LTS)で行っています。
問題
以下のサンプルクラスを考えます。
class Sample
private_class_method :new
def initialize(x)
@x=x
end
def self.paramcheck(x)
# newに渡すパラメータとして適切かをチェック
x.even?
end
def self.create(x)
return nil unless paramcheck(x)
# パラメータが不適切な場合は nil じゃなくて例外の方がイイ?
# raise ArgumentError.new("#{x} is invalid") unless paramcheck(x)
new(x)
end
end
これが、new
をプライベート化することで、オブジェクト生成時のパラメータに一定の制限を設けようとする例になっています。
次のirb実行例のように、生のnew
は使えず、用意したcreate
でのみ ( パラメータチェックを通した ) オブジェクト生成ができるという寸法です。
$ irb -r./sample.rb
irb(main):001:0> Sample.new(10)
NoMethodError: private method `new' called for Sample:Class
from (irb):1
from /usr/bin/irb:11:in `<main>'
irb(main):002:0> Sample.create(10)
=> #<Sample:0x0000000246bc00 @x=10>
さて、ここでこのクラスのインスタントメソッドから自クラスのオブジェクトを生成したいとします。もちろん、self.class.create
を使えば ( 外部からのオブジェクト生成と同様 ) 生成できますが、勝手知ったる自クラスなのでそこまでしたくない、と。
irb(main):003:0> class Sample
irb(main):004:1> def spawn
irb(main):005:2> self.class.new(@x*2)
irb(main):006:2> end
irb(main):007:1> end
=> :spawn
irb(main):008:0> Sample.create(10).spawn
NoMethodError: private method `new' called for Sample:Class
from (irb):5:in `spawn'
from (irb):8
from /usr/bin/irb:11:in `<main>'
そこで、自オブジェクトの情報を元に、新たにオブジェクトをnew
で生成するspawn
を定義してみますが…。たとえ自クラスであっても、インスタンスメソッドからは、プライベート指定の特異メソッドnew
を呼び出すことはできません。
対策
そこでどうするか、なのですが、特異メソッドのアクセス制御という意味では、外部からであっても、自クラスインスタンスメソッドからであっても違いはないのだろうと考え、別のアプローチを考えました。
先ほどのSample
クラスを継承し、対策版のspawn
を定義したサンプルを次に示します。
require './sample.rb'
class Sample2 < Sample
def spawn
obj=dup
obj.initialize(@x*2)
obj
end
protected :initialize
end
つまり、new
経由での生成をあきらめ、dup
でコピーしたオブジェクトにinitialize
をかけることで、新しくオブジェクトを作り出そうということです。
次の実行例のように、new
/create
の特性は以前のサンプルそのままに、インスタンスメソッドspawn
からcreate
を経由しないオブジェクト生成ができました。
$ irb -r./sample2.rb
irb(main):001:0> Sample2.new(10)
NoMethodError: private method `new' called for Sample2:Class
from (irb):1
from /usr/bin/irb:11:in `<main>'
irb(main):002:0> Sample2.create(10)
=> #<Sample2:0x000000009441e8 @x=10>
irb(main):003:0> Sample2.create(10).spawn
=> #<Sample2:0x00000000941bf0 @x=20>
さて、この対策での注意点ですが、それはinitialize
をprotected指定しておくことです。デフォルトのprivateのままだと、dup
したオブジェクトからinitialize
が呼べません。
なお、protectedであれば、外部から ( spawn
でやってるのと同じように ) dup
したオブジェクトからinitialize
を呼ぼうとしても、失敗に終わります。
これで内外の差別化ができている、という主張です。
irb(main):004:0> Sample2.create(10).dup.initialize(20)
NoMethodError: protected method `initialize' called for #<Sample2:0x00000000b38a30 @x=10>
Did you mean? initialize_dup
from (irb):5
from /usr/bin/irb:11:in `<main>'
終わりに
new
をプライベート化する話は色々見つかるのですが、このような使い分けを考える例がとんと検索に引っかからなかったため記事にしてみました。
ただ、私はRubyに詳しくないので、これがベストなのかは良く分かっていません。もっとちゃんとした手段があるとか、そもそも設計的に…とか、ご指摘は是非よろしくお願いします。