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::FirstName
と UserPackage::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
こちらうまく回避できないですかね…(調べてみる)
-
そんなやつをチームに入れるなという主張もあると思うが、それは別の問題なのでここでは扱わない。 ↩