RailsでModelのカラムに20XX年3月31日という特殊な初期値をセットするには（卒業日とか）（メタプログラミング）

弊社の先輩方のリファクタリングが、僕の目には鮮やかすぎて感動したのでメモします。

## やりたいこと
いろんな学校に通ういろんな学生を、DBのテーブルに登録します。
そのいろんな学校のマスターテーブルも作って、卒業年ごとに登録できるようにしたいです。
例）`福岡高校（2022/3/31卒業予定）`　`福岡高校（2023/3/31卒業予定）`

UXのために、**卒業予定年月日は、日本の大半の学校の卒業月日である3/31を初期値としてセットしてあげて、YYYYは現時点から直近の年をセットしてあげたい**です。
例）作成日が2021/8/28なら2022/3/31、2021/1/1なら2021/3/30。

学校のマスターテーブルを管理するモデルはこんなです↓

```
# == Schema Information
#
# Table name: schools
#
#  id              :bigint           not null, primary key
#  name            :text             not null
#  graduation_date :date             not null
#  created_at      :datetime         not null
#  updated_at      :datetime         not null

```

自分の提出したコードと、先輩の鮮やかな手直しを順番に書いていきます。

### 環境
ruby 3.0.0
Rails 6.0.3

## Ruby歴8ヶ月の人の実装例
decoratorで実装してみたです。

newとeditで処理を分けるために、`is_new_record`という変数をformパーシャルに渡しています。newなら今回実装する初期値をセットしてあげればいいですが、editの時は既に登録された卒業日をDBから引っ張ってきてセットしてあげたいので分ける必要があると考えました。`graduation_date`は`null: false`なので、editの時にNULLを考慮する必要はないです。

```erb:new.html.erb
<%= render 'form', url: school_path, method_name: :post, is_new_record: false %>
```

```erb:_form.html.erb
<%= f.text_field(:graduation_date,
                 value: "#{@school.decorate.default_date(is_new_record)}") %>
```


```ruby:school_decorator.rb
 def default_date(is_new_record)
    if is_new_record
      today = Time.current
      # 登録時点が4/1以降であれば「来年」をセットする
      target_year = if today.month >= 4
                      today.year + 1
                    else
                      today.year
                    end
      "#{target_year}/03/31"
    else
      graduation_date
    end
  end
```

何はともあれ、まずはプログラムそのもののリファクタリングをしましょう...

### 何はともあれリファクタリング

```ruby:school_decorator.rb
 def default_date(is_new_record)
    if is_new_record
      today = Time.zone.today　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　　# 1
      target_year = today.year + (today.month >= 4 ? 1 : 0) # 2

      "#{target_year}/03/31"
    else
      graduation_date
    end
  end

```

ポイント

1. Time.currentをTime.zone.todayにしました。Time.currentで得られる値は時間であり、今回hour, minuteは不要なのであまり適切な選択ではなかったですね。それにしても、Time.zone.todayって何か違和感ありませんか？DateクラスではなくTimeクラスに今日の情報を尋ねるなんて。ここも意外に沼だったので、別の記事にまとめました→（[Time.zone.todayってTimeクラスに話しかけたのになぜDateクラスが返ってくるのか？](https://qiita.com/kakudaisuke/items/8b01d1328f203bcfcdd3)）
2. こんくらいの条件分岐処理なら、三項演算子に。



## つよつよエンジニアの鮮やかな実装
いよいよです。
Modelの`after_initialize`とオープンクラスの合わせ技で実装する。

### `after_initialize`を使う

```ruby:school.rb
after_initialize do
  self.graduation_date ||= Time.zone.today.end_of_term
end
```

ポイントは以下。

- `after_initialize`とは？
  - https://railsguides.jp/active_record_callbacks.html#after-initialize%E3%81%A8after-find
  - https://railsdoc.com/page/after_initialize

>after_initializeコールバックは、Active Recordオブジェクトが1つインスタンス化されるたびに呼び出されます。インスタンス化は、直接newを実行する他にデータベースからレコードが読み込まれるときにも行われます。

- 既にgraduation_date（卒業日）が入っていればそれをフォームにセットし、なければ`Time.zone.today.end_of_term`でセットしてあげる。
- `end_of_term`（期末）なんていうビルトインメソッドはRubyにもRailsにもないので、自分でRubyのDateクラスをオープンクラスして作ってしまいます。メタプロですね！

### Dateクラスをオーバーライド（オープンクラス）

```ruby:config/initializers/open_class/date.rb
class Date
  def end_of_term
    target_year = self.year + (self.month >= 4 ? 1 : 0)
    Date.new(target_year, 3, 31)
  end
end

```

### View

モデル側での処理により、Viewで`is_new_record`という変数を持たせて制御する必要がなくなりました！

```erb:new.html.erb
<%= render 'form', url: school_path, method_name: :post %>
```

これで完了です。
上のdecoratorでも同じことはできますが、レコードの初期値をセットするという機能から考えるとModelでした方が適切と考えられます。

## 最後に
何かご指摘などあれば、ぜひお願いします！

そういえば先日、この記事みたいな感じで自分の実装を先輩が手直ししてくれるという話を、未経験エンジニア転職同期に話したら「いいな〜」と羨望のリアクションをされて驚きました。

福岡のIT企業で働かせていただいております。

https://fusic.co.jp/