Null Objectパターンの実装を支援するgemとして、Naught
というgemがあるのですが、Qiitaでは、業界のjoker1017さんの 俺がGitHubでスターを付けたリポジトリ一覧 という記事ぐらいしか見つからなかったので、その使い方と有用性をざっくり説明したいと思います。
Null Objectパターン is 何?
私も最近になって知ったのですが、
こちらの記事 NullObjectパターン によると、
あるオブジェクトが nil でなければ、メソッドを呼び出す
こういうパターンが頻出する場合、obj に nil の代わりに何もしないメソッドを持つオブジェクト を格納しておくインターフェースだけ持って、何もしないオブジェクトを用意することで、
そのオブジェクトを使う側は、オブジェクトが生成されなかった場合を気にする必要がなくなる
→ オブジェクトの状態を意識する必要が無い
→ 結合度が下がる
とのことです。なるほど、コード書いているとよく遭遇する「あるある」ですね。
Naught is 何?
A toolkit for building Null Object classes in Ruby
https://github.com/avdi/naught
「Null Objectパターンの実装を支援するgem」とのことですが、具体的にはどんな使い方ができるのでしょうか?
ちょっと、代表的なものを抜粋して紹介します。
オブジェクトのメソッド呼び出しは全てnilを返す NullObject
Naught.build
で、NullObjectを生み出すことができます。
NullObjectは、当該オブジェクトへのメソッド呼び出しに対して、すべてnil
を返してくれます。
require 'naught'
NullObject = Naught.build
null = NullObject.new
null.foo # => nil
null.bar # => nil
関連先だろうがメソッドチェーンだろうが全てを飲み込む black_hole
NullObjectは大変便利なものですが、場合によっては無効化したいオブジェクトについて、
null.foo.bar.baz
などと関連先のメソッドを呼び出していたり、メソッドチェーンが続いていたりして、「そのオブジェクト(null
)の向こう側も無効化しておきたいんですけど!」というケースもあると思います。
null
から先のオブジェクトをすべてNullObjectにしておかなければならないのでしょうか?
否。そんなケースにもNaught
はCOOLに対応していて、以下のようにblack_hole
というものを使うことができます。
require 'naught'
BlackHole = Naught.build do |config|
config.black_hole
end
null = BlackHole.new
null.foo # => <null>
null.foo.bar.baz # => <null>
null << "hello" << "world" # => <null>
そのクラスに定義されたメソッドだけnilを返すようにする mimic
black_hole
は大変便利なものですが、一方でちょっとやりすぎ感が拭えません。
「black_hole
だと、本来そのクラスに定義されていないメソッドまで無効化される。定義されていないメソッド呼び出しはエラー検知できるようにしておきたい」
ということもあると思います。
そんなケースにもNaught
はCOOLに対応していて、以下のようにmimic
というものを使うことができます。
require 'naught'
NullIO = Naught.build do |config|
config.mimic IO
end
null_io = NullIO.new
null_io << "foo" # => nil
null_io.readline # => nil
null_io.foobar # =>
# ~> -:11:in `<main>': undefined method `foobar' for
# <null:IO>:NullIO (NoMethodError)
また、Naught.build
のブロック内では、メソッドを定義できるようになっていて、関連先をNullObjectに差し替えたり、特定のメソッドだけ返り値を偽装する、ということもできます。
require 'naught'
NullIO = Naught.build do |config|
config.mimic IO
def relation
true
end
def readline
"This is mimic!"
end
end
def mimic
null_io = NullIO.new
null_io << "foo" # => nil
null_io.relation # => true
null_io.readline # => "This is mimic!"
end
Naughtを活用した実装例
外部サービス(API)へのアクセスを封鎖する
記事表題に反して、Null Objectパターンを活用した例ではなくて申し訳ないですが......
「productionでは実際のAPIにアクセスさせているが、sandbox環境が用意されていない、あるいは利用制限があるなどの事情があってstagingやdevelopmentではAPIにアクセスさせないようにしたい」
という状況があったとします。あったとさせてください。(少なくとも私は遭遇しました)
testではWebMockを使えば良いと思うのですが、stagingやdevelopmentでもWebMockを使うか? というと、そこまで厳密にスタブしなくても良いんだよなー、APIのインタフェースさえ無効化できれば十分なんだよなー、ということもあるかと思います。あるとさせてください。
そこで! Naught
を使って、APIクラス生成時にMimicに変えてしまうというアプローチを取ることができます。
例をあげてみます。
Hogehoge::API
という実際に外部にアクセスしてしまうクラスと、そのAdapterがあったとします。
この時、production環境以外では、Adapterのコンストラクタ内で保持するオブジェクトをMimicの方に差し替えてしまうことができます。
class HogehogeApiAdapter
NullObject = Naught.build { |config| config.singleton }
HogehogeApiMimic = Naught.build do |config|
config.mimic Hogehoge::Api
end
def initialize(init_params)
@api = Rails.env.production? ? Hogehoge::Api(init_params).new : HogehogeApiMimic.new
end
# Hogehoge::Apiに生えているメソッドは全て握り潰されるので、APIクラスの先にある実装を意識する必要がない
def insert(args)
record = Hogehoge::Type::Record.new(args)
@api.register(record)
end
def bulk_insert(args_array)
records = args_array.map { |args| Hogehoge::Type::Record.new(args) }
@api.bulk_register(records)
end
end
Rails.env.production?
を使っているので、これはRailsの例ですが、別にNaught
はRailsに限ったgemではないので、Rubyで書かれたコードには何にでも適用することができます。
また、Mimicにすることにこだわりがなければ、black_hole
で全て吸収してしまえば良いと思います。
なお、本例ではAPIクラスとの間にAdapterを噛ませていますが、噛ませない場合でも、呼び出し元をMimicに差し替えれば良いです。
が、それだとMimicに差し替えるコードが散らばるので、実際にはAdapter的なものを一個噛ませておいて、その中でMimicに差し替えた方が楽かなと思います。
おわりに
以上です。
Naught
の手軽さ、有用性を少しでも実感いただけたでしょうか?
実装例を一つだけ紹介しましたが、私の引き出しが足りないだけで、実際にはもっと有用なNaught
の使い方が考えられると思います。
「自分のプロジェクトではこんな使い方をしている」
「お前のNaught
の使い方は間違えている!」
など、情報・ご指摘あれば是非、コメントに寄せていただければと思います。