リファクタリングの応用 その2
リポジトリをクローン
1 2 |
$ git clone https://github.com/exp-drill/effective_model_sample.git
$ cd effective_model_sample
|
環境構築する
1 2 3 4 5 6 7 8 9 |
ここまでコマンドを実行したら、rails s
できるかどうか確認。
次のような画面が表示されれば、事前準備は完了。
リファクタリング実践
サンプルアプリの仕様確認
事前準備が終わったので、サンプルアプリの仕様を確認していく。
テーブル構造
サンプルアプリにはTasksテーブルが存在している。
テーブル構造は以下のようになっている。
保持している情報 | カラム名 | 型 | 制約 |
---|---|---|---|
タスクの名前 | title | string | null: false |
タスクの内容 | content | string | なし |
タスクの開始日程 | start_at | datetime | null :false |
タスクの終了日程 | finish_at | datetime | null :false |
タスクの種類 | kind | integer | null: false |
タスクが完了したかどうか | finished | boolean | default: false |
コントローラ
コントローラはTasksテーブルに対応したTasksコントローラのみ。index
アクション、create
アクション、update
アクションが存在している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
class TasksController < ApplicationController
def index
@task = Task.new
@tasks = Task.where('start_at > ?', Time.zone.now).order(start_at: :asc)
end
def create
@task = Task.new(task_params)
@task.save
redirect_to tasks_path
end
def update
@task = Task.find(params[:id])
if @task.finished == false
@task.finished = true
@task.save
redirect_to tasks_path
else
render :index, alert: '既にタスク「#{task.title}」は完了しています '
end
end
private
def task_params
params.require(:task).permit(:title, :content, :start_at, :finish_at, :kind, :finished)
end
end
|
ビュー
ビューはtasks/index.html.haml
がルートになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
%nav.navbar.navbar-light{style: :"background-color: #e3f2fd;" }
%a.navbar-brand 登録済みタスク一覧
= form_for @task, html: {class: 'form-group'} do |f|
= f.label :kind
= f.select :kind, [0, 1, 2], {}, class: 'form-control', placeholder: 0
= f.label :title
= f.text_field :title, class: 'form-control', placeholder: "ここにタスク名を入力してください"
= f.label :content
= f.text_area :content, class: 'form-control', placeholder: "ここにタスクの詳細を入力してください"
= f.label :start_at
= f.datetime_select :start_at, ampm: :true, minute_step: 15, class: 'form-control'
= f.label :finish_at
= f.datetime_select :finish_at, ampm: :true, minute_step: 15, class: 'form-control'
= f.submit '作成する', class: "btn btn-primary"
.table-responsive
%h4.r-title-label{style: 'display: inline-block; margin-right: 20px; '}
%table.table.table-striped.b-t.b-light
%thead
%tr
%th 種類
%th タスクの名前
%th タスクの内容
%th 開始時間
%th 終了時間
%th 状態
%tbody
= render @tasks
|
ページ上部の フォームから入力された情報は、Tasksコントローラを通じてTasksテーブルに保存される。Tasksテーブルに登録されている各レコードは、部分テンプレートviews/_task.html.haml
を通じて描画されている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
%tr
%td
- if task.kind == 0
私用
- if task.kind == 1
仕事
- if task.kind == 2
その他
%td
= task.title
%td
= task.content
%td
= task.start_at
%td
= task.finish_at
%td
- if task.finished?
完了済
- else
未完了
%br
= link_to "完了にする", task_path(task), method: :patch, class: "btn btn-primary "
|
現在のコードの問題点
ここまでご覧いただいたコードには、次のような問題がある。
(1)ビューにkindについてのロジックが記述されている
tasks/_task.html.haml
には、kindの値によって表示するビューを変更するif文が記述されている。ビューにif文を書いてしまうと、下記の理由によりコードの保守性を下げてしまう。
- kindの0, 1, 2がそれぞれ何を意味するのか、ビューを見ないと分からない
- kindの種類が増える度、新たにif文を足さなければいけない
(2)@tasksの定義が直観的に分かりづらい
Tasksコントローラのindexアクション内で定義されている変数@tasksの、.where('start_at > ?', Time.zone.now)
という記述は、一目見ただけではどのような絞り込みを行っているのか分かりにくい。また、タスク管理アプリという性質上、現在時刻より開始時刻が先という絞り込み条件は、今後Taskモデル内部のメソッドや、他のコントローラなどでも再利用する可能性が高い。
絞り込みのロジックをTask
モデルに移動させて、メソッドのように名前をつけて、どこからでも再利用できるようにする必要がある。
(3)コントローラにロジックが記述されている
Tasksコントローラのupdateアクションは、特定のタスクのfinishedカラムの値をtrueにする処理を行っている。しかし、特定のカラムのみを更新する処理を表現するのに、Tasksコントローラのupdateアクションを利用してしまうと、updateアクションではTaskに関わる全てのカラムを更新している
と、将来的に誤解を招く恐れがある。
現状、finishedカラムの他に更新したいカラムが存在しないなら、finishedを更新するためだけのコントローラを新たに作成するか、Taskモデルにインスタンスメソッドとして定義しておいて、コントローラで呼び出すように変更することで、Tasksコントローラの保守性・可読性を向上することができる。
enumを使ってkindの表示を綺麗にする
(1)ビューにkindについてのロジックが記述されているという問題は、enum
を利用することで解決できる。
enum
enum
はint型、boolean型で定義されたカラムを、文字列で表現できるようにする機能。
1 2 3 4 5 |
1 2 3 4 |
@drink = Drink.new(kind: 1)
@drink.kind
# => 'coffee'
# 数字ではなくenumで定義したキーワードが返ってくる
|
enumを利用すると、数字と対応した文字列を返り値として出力できるようになる。
これを利用して、タスク管理アプリの(1)ビューにロジックが記述されているの問題を解決していく。
Taskモデルにkindカラム用のenumを定義
1 2 3 |
class Task < ApplicationRecord
enum kind: [:individual, :work, :others]
end
|
部分テンプレートの記述を書き換え
1 2 3 4 5 6 7 |
/if文を削除し、task.kindと書き換える/
%tr
%td
= task.kind
%td
= task.title
/以下省略/
|
フォームの選択肢をenumを使って書き換え
enumを定義すると、[モデル名].[カラム名の複数形] のような形で、そのカラムに設定したenumを全て表示することができる。
1 2 |
Task.kinds
=> {"individual"=>0, "work"=>1, "others"=>2}
|
また、Task.kinds.keysのように記述することによって、enumで設定したkeyの一覧を得ることができる。
1 2 |
Task.kinds.keys
=> ["individual", "work", "others"]
|
これを入力フォームの f.select に引数として渡すことによって、選択肢を[0 ,1, 2]のようにベタ書きすることを避けられる。enumで対応しているkindが4、5と増えたとしても、フォームの選択肢を自動で増やせるメリットもあるので、このタイミングで書き換えてしまう。
1 2 3 4 |
= form_for @task, html: {class: 'form-group'} do |f|
= f.label :kind
= f.select :kind, Task.kinds.keys, {}, class: 'form-control', placeholder: 0
|
ここまで完了したら、改めて http://localhost:3000/ を開いて見る。
「種類」の列に、「work」「individual」などと表示されているのが確認できる。enumを使用する前のビューでは、日本語を使用していたため、i18nを使って日本語化していく。この際、「enum_help」というgemを利用すると、enumの日本語化が簡単に行える。
enum_helpをインストール
1 2 |
# 末尾に追記
gem 'enum_help'
|
1 |
$ bundle install
|
ja.ymlとビューを編集して日本語化する
1 2 3 4 5 6 7 8 9 10 11 |
# ja:の一行下から、enums: ~ others:までの記述を追加
ja:
enums:
task:
kind:
individual: "私用"
work: "仕事"
others: "その他"
activerecord:
# 以下省略
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
1 2 3 4 5 |
enum_help
を導入すると、 Task.kinds_i18n.invert のような書き方ができるようになるが、これは、各enumの日本語化後と元々の値をセットで返してくれるメソッド。
1 2 |
Task.kinds_i18n.invert
=> {"私用"=>"individual", "仕事"=>"work", "その他"=>"others"}
|
enum_help
導入後は、Task.kinds_i18n.invertと記述することによって、セレクトボックスの表示を日本語化することができる。
ここまでできたら、一度Raiilsサーバーを終了させた後、もう一度rails s
を実行。これで、enumの日本語化が完了。
部分テンプレートapp/views/_task.html.haml
には、もう一箇所ロジックが書き込まれてしまっている箇所がある。それは、finishedの値がtrueかfalseかによって、「完了済」と「未完了」の表示を切り替えている部分である。 enum以外にも、「デコレーター」「プレゼンター」などを活用することで、ビューからロジックを切り離すことができる。余裕があれば、finishedについてもロジックの分離に挑戦してみると良い。
scopeを使って検索ロジックをモデルに移す
(2)@tasksの定義が冗長という問題は、モデルにscope
を定義することによって解決できる。
scope
scope
はモデルに対する絞り込みの条件に名前をつけて、メソッドのように呼び出し可能にする機能。よく使う検索ロジックはscope
にしておくことによって、後から簡単に再利用できるようになる。
それでは、scopeを活用して@tasksの定義を読みやすくしていく。
Taskモデルにscopeを定義
今回は「incoming」という名前でscopeを定義する。
1 2 3 4 5 |
class Task < ApplicationRecord
enum kind: { individual: 0, work: 1, others: 2 }
scope :incoming, -> { where('start_at > ?', Time.zone.now) }
end
|
定義したscopeを使って@tasksを再定義
1 2 3 4 5 6 7 8 9 |
class TasksController < ApplicationController
def index
@task = Task.new
# モデルに定義したscopeはメソッドのように呼び出せる
@tasks = Task.incoming.order(start_at: :asc)
end
# 以下省略
|
@tasksをscopeを使った書き方に修正したら、もう一度 http://localhost:3000/ を開く。書き換え前と表示されているタスクの一覧が変わっていないことが確認できるはず。
実は、order(start_at: :asc)
も、scopeとして定義して呼び出すことが可能である。
コントローラに書かれたロジックをモデルのメソッドで書き直す
最後に、(3)コントローラにロジックが記述されているの問題を解決していきましょう。Tasksコントローラのupdateアクション内の「finishedカラムをtrueにする処理」を、Taskモデルのインスタンスメソッドとしてmodels/task.rb
に定義し直す。
Taskモデルにインスタンスメソッドを定義
「finishedカラムをtrueに変更する」処理であることを分かりやすく伝えるために、update_finished_trueというメソッドを作成する。
1 2 3 4 5 6 7 8 9 10 11 |
class Task < ApplicationRecord
enum kind: { individual: 0, work: 1, others: 2 }
scope :incoming, -> { where('start_at > ?', Time.zone.now) }
# finishedカラムをtrueに更新するメソッドを定義
def update_finished_true
self.finished = true if self.finished == false
self.save
end
end
|
定義したインスタンスメソッドを利用してupdateアクションを書き直し
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
class TasksController < ApplicationController
def index
@task = Task.new
@tasks = Task.incoming.order(start_at: :asc)
end
def create
@task = Task.new(task_params)
@task.save
redirect_to tasks_path
end
# 定義しておいたメソッドを呼び出す
def update
@task = Task.find(params[:id])
@task.update_finished_true
redirect_to tasks_path
end
private
def task_params
params.require(:task).permit(:title, :content, :start_at, :finish_at, :kind, :finished)
end
end
|
コントローラ内に記述されていたロジックを、モデルのインスタンスメソッドとして切り出すことで、かなり行数を減らすことができた。また、元々の記述には存在していた「if文による条件分岐」がなくなっていることにも注目すると、元々は「finishedがtrueか、そうでないか」で更新を行うかどうかを決定していたが、「現状finishedがtrueのtaskについて、updateアクションを呼び出す導線がビューにないこと」 「元々値がtrueのカラムにtrueを入れても問題がないこと」を理由に、if文を削除している。
このように、ロジックを適切な箇所に移動させる過程で、不要な記述
が見つかることがある。こまめにコードを整備することによって、より見通しの良いコードを残すことができるので、「ちょっとこの記述読み辛くなってきたな」と感じたら、積極的にモデルを活用してリファクタリングを行う。
今回使用したリポジトリについて
今回クローンしたリポジトリの「refactored」というブランチでは、全ての修正が反映されている。復習などに利用できる。https://github.com/queq1890/effective_model_sample/tree/refactored
参考リンク
・enumリファレンス(英語)
https://api.rubyonrails.org/v5.1/classes/ActiveRecord/Enum.html
https://github.com/zmbacker/enum_help
・ active_decorator Github リポジトリ
「デコレーター」を導入するためのgem。
https://github.com/amatsuda/active_decorator
・Rails ModelのScope(スコープ)の使い方
https://ruby-rails.hatenadiary.com/entry/20140814/1407994568