RubyでNull Objectパターンを使えるようにするgem「Naught」のすすめ

  • 25
    いいね
  • 0
    コメント

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の使い方は間違えている!」
など、情報・ご指摘あれば是非、コメントに寄せていただければと思います。

最後に。本記事内で紹介したサンプルコードのGistを貼っておきます。(とは言っても、大半は公式のコピペですが......)
https://gist.github.com/muramurasan/77bfe5f9418650876db8706c7aba72fe