3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Rubyでpackage-private的な可視性を実現する

Last updated at Posted at 2019-07-24

TL;DR

private_constant というのを使えば定数のスコープを private にできるらしいのでそれを使う。
恥ずかしながら今まで知らなかった。

Ruby でも静的型検査したい

と思ってる人が世の中にどれくらいいるかわからないけど、そういうライブラリが出てきたり(僕は sorbet を使ってみている)、Ruby 3 からは静的型が使えるだのなんだのっていう情報も飛び交っったりしていて、ダックタイピングを前提にコードの設計を考えないといけなかった今までとは若干趣が変わってきたかもしれないなぁと思っている。

ただ不満なのは、静的に型付けしてくれてもすべての型がグローバルだとどこを疎結合にしたいとかどこは凝集度が高いと考えいているかっていうのをコードレベルで表現できないし、最悪いろいろなものをバイパスされてしまう 1 ので、Java で言うところの pckage-private 的な可視性を使いたいなと思った。

module UserPackage
  class User
    def initialize(first_name, last_name)
      @first_name = first_name
      @last_name = last_name
    end
  end

  class FirstName
    def initialize(value)
      @value = value
    end
  end

  class LastName
    def initialize(value)
      @value = value
    end
  end
end

first_name = UserPackage::FirstName.new('Akira') #=> UserPackage 外でインスタンス生成できる
last_name = UserPackage::LastName.new('Suenami') #=> UserPackage 外でインスタンス生成できる
user = UserPackage::User.new(first_name, last_name)

UserPackage::User はいわゆる集約のルートオブジェクトで、その内部に持つ 2 つのバリューオブジェクト UserPackage::FirstNameUserPackage::LastName はルートオブジェクトを通してのみ参照を渡したいとする。

そういう場合、定数のスコープを private にする private_constant というのがあって、それを利用して以下のようにする。

# typed: strong

require 'sorbet-runtime'

module UserPackage
  class User
    extend T::Sig

    sig {params(first_name: FirstName, last_name: LastName).void}
    def initialize(first_name, last_name)
      @first_name = T.let(first_name, FirstName)
      @last_name = T.let(last_name, LastName)
    end

    sig {params(first_name: String, last_name: String).returns(User)}
    def self.factory(first_name, last_name)
      first_name = FirstName.new(first_name)
      last_name = LastName.new(last_name)
      new(first_name, last_name)
    end
  end

  class FirstName
    extend T::Sig

    sig {params(value: String).void}
    def initialize(value)
      @value = T.let(value, String)
    end
  end

  class LastName
    extend T::Sig

    sig {params(value: String).void}
    def initialize(value)
      @value = T.let(value, String)
    end
  end

  private_constant :FirstName
  private_constant :LastName
end

# first_name = UserPackage::FirstName.new('Akira') #=> private constant UserPackage::FirstName referenced (NameError)
# last_name = UserPackage::LastName.new('Suenami') #=> private constant UserPackage::LastName referenced (NameError)
# user = UserPackage::User.new(first_name, last_name)
user = UserPackage::User.factory('Akira', 'Suenami')

こんな感じになる。ついでに sorbet の型注釈もつけてみたのと、 UserPackage の外にはコンストラクタの引数に渡す型( FirstName Lastname )が公開されなくなったのでプリミティブ型からインスタンスを作るための factory メソッドを作った。これでバリューオブジェクトは UserPackage module の外からは完全に見えなくなった感じ。

定数の定義後じゃないと private_constant の宣言は効かないっぽいので、実業務ではファイルのロード順とか気にしとく必要はありそう。ほとんどの場合は AcitveSupport の autoload に任せることになると思うので、該当のバリューオブジェクトを定義したファイルのクラス定義直後に書けば概ね問題ないとは思う。

まとめ

特にない。僕自身も実際のプロダクトではまだやってないので、もし懸念とかあれば誰か教えて欲しい。
感覚的には結構いいのでは?っていう気がしている。

2019/07/24 19:57 追記

なんか ActiveSupport の autoload だとあまりうまくいかなさそうですね…
https://github.com/rails/rails/issues/25813
https://github.com/rails/rails/issues/35898

こちらうまく回避できないですかね…(調べてみる)

  1. そんなやつをチームに入れるなという主張もあると思うが、それは別の問題なのでここでは扱わない。

3
1
0

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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?