Subscribed unsubscribe Subscribe Subscribe

ジャバ・ザ・ハットリの日記

日本→シンガポール→ベルリンへと家族と共に流れ着き、ベルリンのスタートアップで働くソフトウェアエンジニアの日記

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

Rails Ruby プログラミング リファクタリング

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

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

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

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)

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


tango-ruby.hatenablog.com

tango-ruby.hatenablog.com

tango-ruby.hatenablog.com