ジャバ・ザ・ハットリ
Published on

RubyのリファクタリングでNull Objectを使ってコードをスッキリさせる方法

Authors
  • avatar
    ジャバ・ザ・ハットリ

前回、Ruby のリファクタリングでイケてないコードを美しいオブジェクト指向設計のコードへ改良するための方法という記事を書いて、いい反響をいただいたので第2弾を書いた。

Ben Orenstein 氏の講演で話されていた前回のとはまた別のリファクタリング方法。元ネタはこちら。

Refactoring from good to great

【リファクタリング前のコード】

class JobSite
  attr_reader :contact

  def initialize(location, contact)
    @location = location
    @contact = contact
  end

  def contact_name
    if contact
      contact.name
    else
      'no name'
    end
  end

  def contact_phone
    if contact
      contact.phone
    else
      'no phone'
    end
  end

  def email_contact(email_body)
    if contact
      contact.deliver_personalized_email(email_body)
    end
  end
end

class Contact < OpenStruct
  def deliver_personalized_email(email)
    email.deliver(name)
  end
end

これはどんなプロジェクトでもよく散見される種類のコードだと思う。インスタンスを初期化して作成した後、それぞれの要素があるかどうか判断して、ある場合は A を返す。無い場合は B を返す、というパターン。
コードで言うとここ。

  def contact_name
    if contact
      contact.name
    else
      'no name'
    end
  end

contact_name では if contact として contact が入っていたら name を返し、もし無ければ'no name'としている。同じように contact_phone、email_contact があって、そのメソッドの中身は if 文で分岐されている。

Rails なんかでもよくあるのが cuurent_user の有り無しによって分けるパターン。
例えばこういうの。

if current\_user
  AAA
else
  BBB
end

で、そこは前回と同じで「聞くな、言え」の法則に反している。
毎回 contact があるかどうかを「聞いて」から処理をするのではなく「ただ言う」だけにした方がいいですよ、と。

その方法が Null Object になる。まずは初期化のところを変更する。
【変更後】

  def initialize(location, contact)
    @location = location
    @contact = contact || NullContact.new
  end

initialize に Null Object を追加した。名前は NullContact。したがってもし引数の contact が入ってなければ@contact には NullContact のインスタンスが入ることになる。

その NullContact の定義がこれ。
【変更後】

class NullContact
  def name
    'no name'
  end

  def phone
    'no phone'
  end

  def deliver_personalized_email(email)
  end
end

つまり「もし contact が無ければ返していたモノが全部入っているクラス」になる。

後はリファクタリング前のコードには何回も入っていた if 文を全て取り除く。

【リファクタリング後の全体像】

class JobSite
  attr_reader :contact

  def initialize(location, contact)
    @location = location
    @contact = contact || NullContact.new
  end

  def contact_name
    contact.name
  end

  def contact_phone
    contact.phone
  end

  def email_contact(email_body)
    contact.deliver_personalized_email(email_body)
  end
end

class NullContact
  def name
    'no name'
  end

  def phone
    'no phone'
  end

  def deliver_personalized_email(email)
  end
end

class Contact < OpenStruct
  def deliver_personalized_email(email)
    email.deliver(name)
  end
end

すごいシンプルで読みやすい。全10行もあった if 文の分岐が全て無くなり、ただ「言う」だけで処理が完結している。
さらに変更の容易さも上がっている。リファクタリング前のコードでは contact が無い場合の処理が分散していた。例えば「contact が無い場合の表示が no name とか愛想が無さすぎだから、もうちょっとマシなのに変えようかな」となったとする。するとそれぞれの if 文の else の箇所を探して、そこだけを変更していかなければならなかった。

ところがリファクタリング後のコードは NullContact クラスに定義がまとめられているので「NullContact を変更だけすれば Ok」となっている。

単一責任の小さなクラスを実装、というオブジェクト指向デザインの基本そのままになっている。

ということで「Ruby のリファクタリングで Null Object を使ってコードをスッキリさせる方法」でした。

ここに書いたリファクタリングの解説とオブジェクト指向デザインの内容はほとんど「Practical Object-Oriented Design in Ruby」の受け売り。

Practical Object-Oriented Design in Ruby: An Agile Primer (Addison-Wesley Professional Ruby)
Practical Object-Oriented Design in Ruby: An Agile Primer (Addison-Wesley Professional Ruby)
作者: Sandi Metz
出版社/メーカー: Addison-Wesley Professional
発売日: 2012/09/14
メディア: ペーパーバック

オブジェクト指向に沿ったイケてるコードを書きたい全てのエンジニアにとって「買っても絶対に損は無い」と断言できるオススメの良書。詳しくはこちらの記事に書いた。

英語とプログラミングを同時に勉強するなら「Practical Object-Oriented Design in Ruby」の一択

関連記事