LoginSignup
32
22

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-07-15

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

32
22
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
32
22