食べログでマイクロサービス化チームに所属しています@itumeです。
この記事は 食べログAdventCalendar の11日目の投稿です。
最初に断っておきますと、オチのない話です。
Model/Controller/Viewのどこかに押し込めるとむずむずするものを、どうやって整理したら心地よいのか考え続けたTry&Errorの履歴の一部です。
POROについて
PORO、って誰が使い始めた言葉なのか知らないのですが、Plain Old Ruby Object、ActiveRecordなどを継承していないすっぴんのRubyオブジェクトのことと理解しています。
最初に作ったPOROの話
↓のようなCSVExcelファイル(どこかから手に入れたのか自分で作ったのか...)を画面からアップロードしたら、種類ごとに分けてtech_books, comics, magazinesテーブルに分けて保存したいという案件がありました。
|No.|種類|タイトル|巻|号
|---|---|---|---|---|---|
|1|技術書|たのしいRuby|""|""|
|2|マンガ|逆境ナイン|1|""|
|3|雑誌|Software Design|""|2018年10月号|
「この要件、この設計、ツッコミどころしかないな。。。」と感じられると思いますが、元ネタのコードを元ネタがわからないように改変してたらなんかこうなってしまいましたスイマセン。
この謎のCSVExcelファイルを適切にさばいて、適切なテーブルに保存するためのロジックをTechBook/Comic/Magazineのどれかに持ってもらうのはむずむずするので、謎のCSVExcelファイルをリソースとしていい感じにさばいてくれるPOROを作りました
class MysteriousCsv
class << self
def insert
MysteriousCsv::Parser.new(CSV.read("mysterious.csv")).mysterious_data.map(&:save)
end
end
class Parser
attr_reader :mysterious_data
def initialize(data_arr)
@mysterious_data = parse(data_arr)
end
private
def parse(data_arr)
data_arr.map do |data|
case data[1]
when "技術書"
TechBook.new(title: data[2])
when "マンガ"
Comic.new(title: data[2], volume: data[3])
when "雑誌"
Magazine.new(title: data[2], number: data[4])
end
end
end
end
end
このinsertメソッドは非同期workerの処理の中で呼ばれます。
本物はtransactionかけてたりバリデーションエラー吐かせたり、他の処理からMysteriousCsvのインスタンス自体も使っていたりするのですが、要点だけかいつまむとこんな感じのものを作りました。
元ネタとなったコードは今でも使われていて、元気に動いておりますし、謎のCSVExcelファイルを取り込んでsaveする処理をTechBookに押し込めたりしなくて済んだので、まあ、よかったんじゃないのかなあと思っています。
ViewObjectの話
最初はこういうシンプルなControllerだったのに
class HogeController < ActionControllerBase
def show
@hoge = Hoge.find(params[:id])
end
end
view側で@hogeだけでは判定できない表示制御をしたくて、こういう感じに育ったようです。
class HogeController < ActionControllerBase
def show
@hoge = Hoge.find(params[:id])
@fuga_flg = # なにかの処理によって作られたboolean
@today = Date.today
end
end
Controllerがこういうノリだったので、view側はこういうノリで育ちました。
<%= @hoge.some_attr_1 if @fuga_flg %>
<%= @hoge.some_attr_2 if @today.day < 15 %>
<%= render partial "some_partial" locals: {hoge: @hoge} if @hoge.some_flg && @fuga_flg && @today.day < 15 %>
既に嫌な感じですが、このノリのまま育ち続けると「どういう条件のときに何が表示されるのかよくわからん!」という感じで大きくなります。
viewに複雑な条件分岐があるのも嫌だし、表示内容に対して誰が責任を持っているのかわからない実装を増やしたくなかったで、ViewObjectを試してみることにしました。
class ViewObjects::Hoge::Show
attr_reader :hoge, :some_attr_1, :some_attr_2
def initialize(hoge)
@hoge = hoge
@fuga_flg = # なにかの処理によって作られたboolean
@today = Date.today
@some_attr_1 = hoge.some_attr_1 if @fuga_flg
@some_attr_2 = hoge.some_attr_2 if early_part_of_month?
end
def can_special_offer_week_display?
@hoge.some_flg && @fuga_flg && early_part_of_month?
end
def early_part_of_month?
@today.day < 15
end
end
class HogeController < ActionControllerBase
def show
hoge = Hoge.find(params[:id])
@view_obj = ViewObjects::Hoge::Show(hoge)
end
end
<%= @view_obj.some_attr_1 %>
<%= @view_obj.some_attr_2 %>
<%= render partial "some_partial" locals: {hoge: @view_obj.hoge} if @view_obj.can_special_offer_week_display? %>
これは賛否両論ありました。自分としては
- 謎の条件分岐に名前をつけられた
- 「複雑な表示制御」自体の単体テストが書けた(RSpec)
- viewで使えるもの = view_objectのattr_readerなので何を使っているのかが明確
という良いところがあると思っていたのですが
- view_objectに対する共通認識を形成していなかったので「何このview_objectって」と物議を醸した
- app/view_objects/hoge/show.rb だけぽつんとあるこのディレクトリが気持ち悪い
- これと同じロジックを他のところでも使いたくなったときに、コピペが起きた
ということが起きました。
共通認識を形成できなかったのは単に布教が足りなかっただけなのですが、ディレクトリ構成はなんだかしっくりきていません。。。
皆さんのapp/view_objects以下はどんなノリになってるのでしょうか?
サービスの性質やシステム構成によって最適解は違うと思うのですが、おすすめのやり方があるぜという方もしいたらぜひ教えてください。
他にもいろんなPOROがいるのですが...
元ネタのコードを元ネタがわからないように改変するのがかなり時間かかって、力尽きてしまいました。
明日は@sadashiさんの「Androidアプリの設計 ~My Best Practice~」です。よろしくおねがいします!