KitchHike Tech Blog

KitchHike Product, Design and Engineering Teams

苦しめられてやっと理解できたRailsコールバックの使い方

f:id:sfujisak:20180701092525p:plain

Railsのコールバックが辛いって本当?実際にハマって、学んだこと

チーム開発での経験は、一人で開発していた時とは全く別ものでした。Railsのコールバックは、 書いた本人ではなく他のメンバーが辛くなる ことが多いということを実体験を通して学びました。コールバックで苦しんだ経験を実際のシーンを元に書いてみました。

はじめに

こんにちは。KitchHikeインターンエンジニアのタクです。

社員エンジニアの方から「Active Recordのコールバックは使い方を気をつけよう。」という趣旨のことを、入った頃から何回も言われていました。しかし、開発経験の浅い自分にはなぜ気をつけないといけないのかあまりピンときていませんでした。

たしかにインターネット上にはアンチコールバックの記事が多いのですが、どの記事を読んでも実体験がないからか、心から納得することはできませんでした。

「なぜそんなにもコールバックを嫌うのだろう...?」

このころの私は、コールバックについて何も知りませんでした。他のメンバーが過去に実装したコールバックに、自分が苦しめられることになるまでは。

この記事では、自分が苦しめられてやっと理解できたコールバックの使い方と苦しんだシーンについて紹介します。

Active Record コールバックの特徴と有効なシーン

Active Record コールバックは、「ライフサイクルにフックをかけて実行される処理」のことを示します。例えば before_save に追加すると save メソッド前に毎回実行されます。一方で、コールバックに条件分岐を入れ始めるとメンテナンス性や処理の透明性が落ちるという特徴を持ちます。

Ruby on Railsチュートリアルで紹介されていますが、Emailアドレスをデータベースに保存する際に before_save というコールバックを使って、メールアドレスを小文字にするという処理が実行されています。

class User < ApplicationRecord
  before_save { self.email = email.downcase }
  validates :name,  presence: true, length: { maximum: 50 }
  VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
  validates :email, presence: true, length: { maximum: 255 },
                    format: { with: VALID_EMAIL_REGEX },
                    uniqueness: { case_sensitive: false }
end

これはコールバックを有効に使っている例でしょう。メールアドレスを小文字にする処理を、ある特定の場合に除外したり、変更したりする可能性が低いからです。

このように、コールバックを上手く使うと強力な武器となります。 一方で、コールバックの特徴をしっかりと理解せずに実装してしまうと、後の自分や他の開発メンバーが苦しめられることになります。

ここからは自分が実際に苦しんだシーンを共有します。

苦しんだシーン1: 管理者ページからキッチンのエリア情報を変更できない

KitchHikeでは、キッチンが登録される際に「ユーザが自由入力した住所」と「住所とエリアとマッピングしたファイル」を付き合わせて、ユーザーが入力した住所がどのエリアに位置するかを判断していました。

例: ユーザー入力が 台東区東上野4丁目 なら上野・浅草エリア

このエリアマッピング処理で保存されたエリア情報を、管理者ページから変更したいという要望があり、私が担当者になりました。

実装して動作確認をしてみると、意図通りに動きません。任意のエリアを指定してキッチンの情報を更新しても、保存はエラーなく成功します。しかし保存後に表示されるのは指定したエリアではなく、エリアマッピング処理で保存されたエリアでした。任意のエリアを保存しても いつの間にか元に戻ってしまうのです。

コードを調査したところ、エリアマッピング処理を before_save コールバックに追加されていたことがわかりました。管理者ページからエリアの変更を試みても、保存される直前でエリアマッピング処理が実行されてしまい、変更したかったエリアは保存されていなかったという仕組みでした。

いまになって考えれば「コールバック処理があやしい」と思えるのですが、当時は初めて体験する理解できない挙動を前に冷静な調査ができず、 見当違いの箇所を何回も確認して時間を潰す という苦い経験となりました。 before_save の処理を発見した時には「コールバックだったのか〜」という脱力感と、原因がわかった安心感が混ざったなんとも言えない感覚になったのを覚えています。

コールバックで処理されているとわかったので、管理者ページからの変更時にはコールバックしないというアプローチを考えました。しかし、コールバックにif文を追加するのはさらなるアンチパターンに陥るというメンバーからのアドバイスもあり、やめました。コールバックの辛さを初めて感じた夜になりました。

シーン1の考察と学び: コールバック処理を実装する際の検討項目

まず学んだのは、 保存はエラーなく成功しているが想定外の値になる 場合はコールバックを疑うことです。コールバックがフックできるのは save だけではないので、同様に destroy やその他のフックポイントについても同様です。

またこのシーンでは、将来的に管理者ページからエリアを変更できるようになる拡張性を考慮していれば、 before_save 以外での実装を検討した可能性があります。コールバックを条件分岐させてしまうとメンテナンス性が下がるためです。コールバック処理を実装する際には、その処理が常にアクションとセットになってコールされる必要性を慎重に検討する必要があります。

次はコール回数で従量課金されるAPIをテストで何回もコールしてしまったシーンを共有します。

苦しんだシーン2: テストで何度もAPIをリクエスト

Googleが提供しているGeocoding APIを導入した時の話です。Geocoding APIを使って取得した住所を使って、エリアマッピング処理を行うように修正しました。エリアマッピング処理の単体テストを作成するにあたり、テストでGeocoding APIをリクエストする必要はありません。リクエスト部分をスタブして、テスト用の値を返すようにしました。

リリースして数日後、Google APIのコンソールをのぞいてみると、Geocoding APIの1日のリクエスト回数が 無料上限の2,500コールをはるかに超過してしまっていること に気がつきました。

エリアマッピング処理が実行されるタイミングは1日に多くとも数十回です。Geocoding APIのリクエスト部分もスタブしたはずが、なぜ上限を超過しまったのか。

調査してみると、Geocoding APIをコールするエリアマッピング処理が before_save で実行されていることが原因だとわかりました。エリアマッピング処理の単体テストに限らず、全てのテストケースでキッチンが登録される度に、Geocoing APIへリクエストしていました。その数、テスト1回につき300コールでした。

f:id:yamataku3831:20180630230426p:plain
実際にコール回数がスパイクした時

シーン2の考察と学び

このシーンでは、私が他のエンジニアメンバーと同じレベルで、コールバックを理解していれば、テスト時にGeocoding API へリクエストが送られてしまうことに、いち早く気づけたかもしれません。チーム開発においては、 チームでコールバックに対する認識を合わせること が大切で、チームメンバーの一員である自分も認識を合わせる必要があると感じました。

チーム開発を経験しないとコールバックの怖さは理解できない

自分はこれまでに「コールバック辛い」の記事をいくつも読み、社員エンジニアからもコールバックには気をつけるように言われてきましたが、正直ピンときていませんでした。今回の経験で、過去の自分がなぜコールバックの怖さについて理解できなかったのかを振り返ったところ、それは チーム開発を経験していなかったから でした。

一人で開発するプロダクトでは、多くの場合、ほとんどの箇所を把握できます。コールバック処理を実装したことを忘れることはあっても、想定外の挙動でコード調査しているうちに思い出すことが多いでしょう。

一方でチーム開発では違いました。私にとってKitchHikeが初めてのチーム開発だったのですが、「まさか before_save が設定されているとは思ってもみなかった!」という体験になりました。自分のコールバックに関する理解が低かったのが原因ですが、多くのアンチコールバック記事が書かれている理由を実体験を通して理解しました。コールバック辛いです。

いま振り返ってみれば「saveが成功しているのに想定外のデータになっている」ことが「もしかしてbefore_saveなどでコールバック処理が追加されているかも?」につながるのですが、当初は想定外の現象に焦っていたこともあり、全く思いつきませんでした。

この背景にあるのは、Active Record コールバックの問題では当然なく、 コールバック処理が実行されるまで把握しにくい (マジック化してしまう)ことにあると思います。例えば、エディターで選択しているアクション、例えばsave、をフックしているコールバックを表示する機能があれば、把握が容易になるのではと考えました。

コールバック辛いは一度体験してみると心から納得できます。コールバックで苦しむのは 他のメンバーの可能性が高い です。これは一人開発ではできない、チーム開発でのみ得られる経験値となりました。

まとめ

今回の経験を通じて、Active Record コールバック処理を追加する際は次の2つのことに注意するべきだと実感しました。

  1. コールバックの特徴をしっかり理解して使いどころを見極めること
  2. コールバックに対するチーム全体で認識を合わせること

特に後者の気づきはチームで開発していなければ、経験することはありませんでした。チーム開発に参加して1年になりますが、一人で開発していては気づけないことが非常に多いです。 経験値の高いエンジニアがいるチームでの開発に参加すること が、エンジニアの成長としてとても大切なことだと感じています。まだの人は、少しでも早くチーム開発に参加するのをお勧めします。

いっしょにKitchHikeを開発しよう

KitchHikeでは、React Nativeエンジニア・Railsエンジニア・フロントエンドエンジニア・インターンエンジニアを募集中です!

www.wantedly.com

www.wantedly.com

www.wantedly.com

www.wantedly.com