fv17の日記 - Coding Every Day

Webエンジニアの備忘用ブログです。主にWeb界隈の技術に関して書いています。

古典的記事「Skinny Controller, Fat Model」を読む

こちらの記事
Buckblog: Skinny Controller, Fat Model

この記事を読んで得られる知識、技術

  • Railsにおいて、どこに何を書くのか?
  • 特にコントローラに何を書くか、モデルに何を書くか、の基礎となる指針

どんな記事か?

18 October 2006 と古い記事だが、Railsにおける設計の古典的、かつ著名な記事。

2018年現在では、Fat Modelに関する問題点、そして解決法も出てきた。

しかし、記事内で指摘されていることは、そっくりそのまま「Railsチュートリアル」や「every Rails RSpecによるRailsテスト入門」にも登場する実装例で、現在も有効な考え方。簡単でサクサク読めて分かりやすいので一読の価値あり。

具体的な内容は?

最終的に、一目見ただけで理解できる、メンテナンス性の高いコード!ができるよというもの。
「Skinny Controller, Fat Model」のタイトル通り、Modelが太っていく様子が見られる。

英語読むの面倒だから簡潔にまとめて?

Viewのリファクタリング

before

問題点はViewの中でガチャガチャ文字列を合わせたり計算したり、ノイズがあること。

<% @people.each do |person| %>
  <div id="person">
    <span class="name">
      <!-- 改善できる箇所その1 -->
      <%= person.last_name %>, <%= person.first_name %>
    </span>
    <span class="age">
      <!-- 改善できる箇所その2 -->
      <%= (Date.today - person.birthdate) / 365 %>
    </span>
  </div>
<% end %>
After

よく使われる処理を、抽象的で分かりやすいデータをViewに提供する責務があるモデルに集約する。

<% @people.each do |person| %>
  <div id="person">
    <span class="name">
      <%= person.name %>
    </span>
    <span class="age">
      <%= person.age %>
    </span>
  </div>
<% end %>

Person Model

class Person < ActiveRecord::Base
  # ...

  # これ、everyday Railsで出てきた!
  def name
    "#{last_name}, #{first_name}"
  end

  def age
    (Date.today - person.birthdate) / 365
  end
end

Controllerのリファクタリング

before

問題点は、一目見た時に何をしているのか理解できない。そのため、可読性や保守性が著しく低い。

class PeopleController < ActionController::Base
  def index
    @people = Person.find(
      :conditions => ["added_at > ? and deleted = ?", Time.now.utc, false],
      :order => "last_name, first_name")
    @people = @people.reject { |p| p.address.nil? }
  end
end
after

分かりやすいメソッドに抽出することで、一目で理解できる
find_recentではなくfind_activeやfind_activatedの方が良さそうな気もするが主目的によるか...

class PeopleController < ActionController::Base
  def index
    @people = Person.find_recent
  end
end

Person Model
クラスメソッドで定義する

class Person < ActiveRecord::Base
  def self.find_recent
    people = find(
      :conditions => ["added_at > ? and deleted = ?", Time.now.utc, false],
      :order => "last_name, first_name")
    people.reject { |p| p.address.nil? }
  end

  # ...
end