LoginSignup
5
5

More than 5 years have passed since last update.

Rubyでプライベートなコンストラクタ

Posted at

概要

Rubyにおいて、外部から無制限にオブジェクト生成させないために、特異メソッドnewをプライベートにする方法が知られていますが、そうすると、インスタンスメソッドからのオブジェクト生成にも、外部からと同じ制約がかかってしまいます。
これは、外部からは制約あり、インスタンスメソッドからは制約なしでオブジェクト生成したい場合の障害となります。
そこで、この障害を回避する手段はないか? と考えました。

なお、テストはruby 2.3.1p112(Windows10 WSL/Ubuntu 16.04.2 LTS)で行っています。

問題

以下のサンプルクラスを考えます。

sample.rb
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を定義したサンプルを次に示します。

sample2.rb
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を呼ぼうとしても、失敗に終わります。
これで内外の差別化ができている、という主張です。

外部からの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に詳しくないので、これがベストなのかは良く分かっていません。もっとちゃんとした手段があるとか、そもそも設計的に…とか、ご指摘は是非よろしくお願いします。

5
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
5