POROと大人になった僕

食べログでマイクロサービス化チームに所属しています@itumeです。

この記事は 食べログAdventCalendar の11日目の投稿です。

最初に断っておきますと、オチのない話です。
Model/Controller/Viewのどこかに押し込めるとむずむずするものを、どうやって整理したら心地よいのか考え続けたTry&Errorの履歴の一部です。

## POROについて
PORO、って誰が使い始めた言葉なのか知らないのですが、Plain Old Ruby Object、ActiveRecordなどを継承していないすっぴんのRubyオブジェクトのことと理解しています。

## 最初に作ったPOROの話

↓のような~~CSV~~Excelファイル(どこかから手に入れたのか自分で作ったのか...)を画面からアップロードしたら、種類ごとに分けてtech_books, comics, magazinesテーブルに分けて保存したいという案件がありました。

|No.|種類|タイトル|巻|号
|---|---|---|---|---|---|
|1|技術書|たのしいRuby|""|""|
|2|マンガ|逆境ナイン|1|""|
|3|雑誌|Software Design|""|2018年10月号|

「この要件、この設計、ツッコミどころしかないな。。。」と感じられると思いますが、元ネタのコードを元ネタがわからないように改変してたらなんかこうなってしまいましたスイマセン。

この謎の~~CSV~~Excelファイルを適切にさばいて、適切なテーブルに保存するためのロジックをTechBook/Comic/Magazineのどれかに持ってもらうのはむずむずするので、謎の~~CSV~~Excelファイルをリソースとしていい感じにさばいてくれるPOROを作りました

```rb:app/models/mysterious_csv.rb
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のインスタンス自体も使っていたりするのですが、要点だけかいつまむとこんな感じのものを作りました。
元ネタとなったコードは今でも使われていて、元気に動いておりますし、謎の~~CSV~~Excelファイルを取り込んでsaveする処理をTechBookに押し込めたりしなくて済んだので、まあ、よかったんじゃないのかなあと思っています。

## ViewObjectの話

最初はこういうシンプルなControllerだったのに

```rb:app/controllers/hoge_controller.rb
class HogeController < ActionControllerBase
  def show
    @hoge = Hoge.find(params[:id])
  end
end
```
view側で＠hogeだけでは判定できない表示制御をしたくて、こういう感じに育ったようです。

```rb:app/controllers/hoge_controller.rb
class HogeController < ActionControllerBase
  def show
    @hoge = Hoge.find(params[:id])
    @fuga_flg = # なにかの処理によって作られたboolean
    @today = Date.today
  end
end
```

Controllerがこういうノリだったので、view側はこういうノリで育ちました。

```:app/views/hoge/show.html.erb
<%= @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を試してみることにしました。

```rb:app/view_objects/hoge/show.rb
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
```
```rb:app/controllers/hoge_controller.rb
class HogeController < ActionControllerBase
  def show
    hoge = Hoge.find(params[:id])
    @view_obj = ViewObjects::Hoge::Show(hoge)
  end
end
```
```:app/views/hoge/show.html.erb
<%= @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~」です。よろしくおねがいします！