よくあるデータ出力機能を二段階に分けて作ったら機能の汎用性がとても高くなったはなし


この記事は[食べログ Advent Calendar 2019](https://qiita.com/advent-calendar/2019/tabelog) 7日目の記事です。
よくあるデータ出力機能をふたつに分けて作ったら、機能の汎用性がとても高くなったはなしをします。

## おそらくよくあるデータ出力処理

ふだん、データ出力ってどのように作りますか？

自分は、こんな感じで
レコードの絞り込みとデータの取得を同時に行なって
１レコードずつ出力したい順に情報を並べ替えてファイルに出力
することが多いです。

```ruby:おそらくよくあるデータ出力実装.rb
records = Employee.select(:age, :first_name, :last_name).where(job: :engineer)
CSV.open("engineer.csv") do |csv|
  csv << [:first_name, :last_name, :age] # headers
  records.each do |record|
    csv << [record.first_name, record.last_name ,record.age]
  end
end
```

## いつものようにしたけれど、コードが長くなっちゃった

その日は出力したい情報の種類が多いデータ出力を作っていました。
いつものように作ってみたけれど、データの取得先だけで5種類もあってコードがとても長い、、、
なんとかしたい。
そのときふと
出力したいレコードの絞り込みと、出力したいデータの取得処理を
分けてみたらどうだろうと思いました。

## レコードの絞り込みと、データの取得をわけてみた

そうして出来上がったのがこちらです。

欲しいレコードの検索条件と
知りたいデータの項目名を定義したファイルを渡します。

| id | name | tel | address | station | url | reviews_url | yoyaku |
| --:|:--   |:--  |:--      |:--           |:--           |:--          |:--         |

するとまず、レコード絞り込み機能が
レコードの特定条件で絞り込み検索をして出力対象レコードのidのみを出力します。

|<font color="green">id</font>| name | tel | address | station | url | reviews_url | yoyaku |
| --:|:--   |:--  |:--      |:--           |:--           |:--          |:--         |
|100||||||||
|200||||||||
|300||||||||

続いて、このファイルがデータ取得機能に渡されて

| id |<font color="orange">name</font>|<font color="orange">tel</font>|<font color="orange">address</font>| <font color="red">station</font>|<font color="purple">url</font>|<font color="blue">reviews_url</font>|<font color="deepskyblue">yoyaku</font>|
| --:|:--   |:--  |:--      |:--           |:--           |:--          |:--         |
|100|カフェ|03-0000-0000|恵比寿南|恵比寿駅|/100/|/100/dtlrvwlst|なし|
|200|ラーメン屋|03-0000-0000|恵比寿南|恵比寿駅|/200/|/200/dtlrvwlst|なし|
|300|イタ飯屋|03-0000-0000|恵比寿南|恵比寿駅|/300/|/300/dtlrvwlst|あり|
idをキーにデータを取ってきて、項目名列に対応した情報を付加してくれます。

表の項目名は、データ取得先ごと色分けしてみました。
データ取得先はDBだったりAPIだったりするので、単純に結合もできず
特にAPIはデータを取るときにページングや並列処理をしたりもするのでわりと手間がかかります。

# データ取得機能の汎用性がとてもよかった

長いコードを整理するために機能を分割したのですが
思いがけず、データ取得機能の汎用性がとても高いことに気づきました。

たとえば
レコードを絞り込む条件は違うけど、出力したいデータが似ている機能を新しく作りたいときは、
**レコードの絞り込み部分だけ作れば、あとはデータ取得機能に渡すだけ！**

あるいは
既にレコードは特定できていて、詳細情報を知りたいとき
**id列を埋めたファイルをデータ取得機能に渡すだけ！**

**渡すだけ！**これはなかなか汎用性があるぞう。

単純なデータ出力ではあまり恩恵はなさそうですが
今回ご紹介したように、データ出力のために用意する種類が多くて手間がかかるケースで
それを利用したいシーンが複数あると、たくさんの恩恵を受けられそうです。

### ドメイン間は疎結合のまま、情報満載なデータ出力ができそうな予感

今回はひとつのドメインのデータ取得機能だけ作りましたが、別のドメインにもデータ取得機能を作り
知りたいデータの項目定義ファイルに、いろんなドメインのデータ取得機能を旅させることで
情報満載なCSVファイルを作ることもできそうです。

## 感想
ふだん何気なくやっていたことを少し変えてみたら新しい発見につながったので
今後も何気ないことを改めて考えてみて、いろんな発見をしていきたいですね！

明日は @hiroteru_ さんによる「UIViewの角丸と影のおはなし」です。
お楽しみに！！

最後までご覧いただきありがとうございました！

# おまけ
データ出力機能を作るときに役立ちそうなコード集です。
### 最近のExcelってUTF-8を理解できるんですって（ただしBOMつきに限る）
最近のExcelは、UTF-8でもBOMつきであれば文字化けせずにcsvファイルを開いてくれるようです。

```ruby:BOMつきUTF-8のCSVを作る
File.write(filepath, "\xEF\xBB\xBF") # excelで直接UTF-8を開けるようにするためにBOMをつける
CSV.open(filepath, 'a') do |csv|     # 追加書き込みモードで開く
```
これで「ファイルをインポート」機能とお別れできます。

この方法で作成したCSVを読み込むときは、BOMついてるかもよ。と教えてあげる必要があります。

```ruby:BOMつきUTF-8のCSVを読み込む
CSV.open(filepath, encoding: "BOM|UTF-8") do |csv|
```
### データ取得機能で活躍した遅延ロード
不必要なデータまで取得していると、処理に無駄な時間がかかってしまいます。
１つの取得先のデータだけあれば済むときに、５つの取得先からデータを取得したくないですよね。
こんなときに遅延ロードが大活躍しました。

```ruby:
class Hoge
  def initialize(id)
    @id = id
  end

  def name
    data.name
  end
  
  def tel
    data.tel
  end

  private

  def data
    return @data if defined? @data # メモ化
    @data = HogeData.select(:name, :tel).where(id: @id).take
  end
end

header = csv.headers # 出力したいと指定されている項目名を取得
-> ["name", "tel"]

hoge = Hoge.new(id)
header.each do |column|
  hoge.public_send(column.to_sym) # 初回にnameが呼び出されたときに、中のdataメソッドがデータを取得してくれる
  # 続いて tel が呼び出されたときは、メモ化された@dataを見るので改めてデータ取得はしない
end

```
このように、nameの情報を出力したい。と呼び出されたときにはじめてデータを取得することで不必要なデータは取得せずに済むようになりました。

### あるクラスのメソッドをまとめてdelegateに定義する
delegateって明示的にメソッド名を定義する必要があって、面倒ですよね。
Hogeクラスのメソッド全部をdelegateしたいときは、こんな書き方で定義できました。

```ruby:
delegate *Hoge.instance_methods(false), to: :hoge
```
データ取得機能では、データの取得先毎にクラスをわけてオブジェクトを作っています。
メソッドが呼び出されたら、そのデータを持っているオブジェクトにdelegateするように、この方法で実現しています。