目次
はじめに
こんにちは!STORES 予約本部でエンジニアをしている osd です。業務では予約事業に関するプロダクトをフロント/バックエンド垣根なく開発をしています。 開発しているシステム概要は予約システム STORES 予約にまとめられているので覗いてみてください。
予約システム内の一つの機能に、ある対象の予約がどのタイミングで受付を開始し、締切るのかを設定できる予約受付設定があります。 10 月末に予約受付設定の改善リリースを行ったので、今回はその開発の中での知見と反省を振り返りたいと思います。
受付期間改善 概要
改善点
まずは今回の 受付期間改善施策 で追加/削除した機能性について紹介します。
今回の施策では大きく分けて3つの改善を行いました。
1. 設定ページのフロントエンドの Next 化
予約チームでは従来 Rails を用いて主要なフロントエンドの実装を行っていましたが、STORES ブランドとして実現したい UI/UX 体験を実装できなくなっていたことから、 2021 年 4 月ごろからフロントエンドの実装を Next.js に移行を始めました。 今回の受付期間設定に関しても UI/UX の改善などを目的として設定ページを Next 化しました。
2. 設定タイプの追加/削除
従来の設定方法では「予約日時の時間差分からしか予約開始ができない」であったり「受付締切が最小 1 時間前に締め切られてしまい、5 分前などの設定ができない」といった課題がありました。これらを解決するために図のように設定タイプの追加/削除を行いそのロジックの実装をしました。
3. デモ機能の追加
今回追加した設定項目は、文言を見て具体的なイメージがしにくいことから、設定時の時間を基準としたデモ機能を実装しました。
table 設計
本 プロジェクト における table 設計は以下の通り作成しました。
従来では予約ページの設定関連を担う table にカラムを持ち、そのカラムの値を参照することによって受付期間を決定していましたが、本プロジェクトでは新しく受付期間の設定のみを担う table を作成し設定タイプを担うカラムを参照し、それに紐づくカラムの値のみがレコードに作成されます。
また、今回単一テーブル継承(STI)を採用するか設計段階で議論しどちらにでも倒せるという結論になり STI を採用しない結果となりました。
さらに、これらのリリースに向け現在登録されている予約受付期間のデータを after_save
callback を用いて、従来の table のカラム保存時に新 table に duplicate する処理を追加し、 batch を用いて全データに対して double write を走らせました。
デモ機能
改善点で紹介したデモ機能についての詳細をここでは紹介します。
機能的には図の右側のフロー通りになっています。まずはページ閲覧時の日の 10:00 にイベント実施日を固定し、そこからバックエンド側での日付処理と同じロジックでの計算を行い日時の表示を行います。また、この際開始/締切日共に、計算結果の受付期間開始/締切日時が設定されたイベント日時よりも未来にあった場合受付ができない旨の文言を表示します。
反省と知見
ここからは本 blog のメインである今回の プロジェクト を通して得た個人/チームとしての知見と反省を紹介していきます。
double write/callback を行う際に気にかけること
今回前述したように after_save
を用いて double write を実行していましたが、その callback の作用で意図していなかった挙動がリリース後起こり修正を加えました。
この挙動が起こったロジックとしては以下のようになります。
まずリリース後に受付期間設定のレコード変更が走るのは新しい受付期間設定の table なので、特にリリース後影響しないと思いどこかのタイミングで double write の処理を掃除する方針でした。 しかし移行前の table は予約ページの設定関連を担う table になっており、他の設定を変更した際に新しい予約受付期間を担うテーブルのレコードが上書きされてしまいました。
今思えば当たり前の挙動なのですが、設定時は気づけず after_save
のような callback を扱う際には、意図した範囲で処理がトリガーされるのかどうかを一度立ち止まって精査する習慣をつけるいい学びとなりました。
issue を積むことの重要性
こちらは先ほどの 「double write/callback を行う際に気にかけること」に続く話なのですが、問題発生後の振り返りとして、いつかやる issue をその場で積んでおけば防げた事象であったかもしれないと思っています。
予約チームでは週 1 で sprint の planning を行なっておりそこで issue の確認をするのですが、この issue が積まれていた際に同じチームの方と issue の内容を確認すれば「他の設定によって上書きがトリガーされる可能性」に気づいていただいたり、自身で issue を作成する際に気付けたかもしれない...と感じています。
また、基本的に後で思い出してやろうはやらないので目に見える形でタスクを積んでおくことは習慣付けたいと再確認しました。
planning の質
今回実装を進めていくにあたって必要な実装や工数が増し、複数回リリースを延期する判断をしました。 この延期に関しては実装を進めていくうちに気づける部分はあるので仕方のないところも多いと思うのですが、チームとしてスプリントの planning で実装内容がある程度具体的になるまで質を上げることにしました。リリース日の延期をしない(見積もりで point 数を見誤らない)ことはもちろん、開発の見通しが良くなることによって、要件を見直し機能提供を早める判断がしやすくなることにもメリットがあると思っています。
命名のトレードオフ
今回の table 設計において、 relative_month_type_seconds_from_base_months
カラムがあるようにかなり命名が長くなってしました。これは今回の設定概念がかなり複雑で
- どの指定タイプなのか ->
relative_month_type
- 実態は何なのか ->
seconds
- どこからの相対的指標なのか ->
from_base_month
のように3つの概念が入り込んでいるからでありどれも落とせない要素でした。長いとはいえ個人的にはパッとみて一応理解はできるという判断の下実装を進めていましたが、実装が進むにつれてこの命名による認識コストの増加がじわじわ効いてくることになりました。
今回の table でのデータの持ち方は単純ですが、これらのレコードから実際に受付期間を算出するのはかなり複雑でしっかりとロジックを読み込まないと理解できないものになっていました。
例で言うと、 3ヶ月前の30日の12:00 から受付を開始する
設定時に 3 ヶ月前が 2 月だった場合、単純に日数を足し合わせると 3 月の 2 日に予約が開始されてしまうことになります(閏年でない場合)。これらに対応するために月末の丸め込み処理を実装しています。また、現在時刻から受付期間設定を用いて受付可能になっている予約を逆検索するロジックなども存在しどれもレビュワーの理解コストが大きいものばかりでした。
もちろん命名は単純で誰がみてもわかりやすいものであるべきなことは理解しているつもりでしたが、命名に際して悩んだ際に、そのカラム名を用いたロジックの複雑性にも命名のトレードオフは左右されることを学びました。
利用者側に立って機能を実装する
今回この プロジェクト を通して一番学びになったことは「利用者側に立って機能を実装する」ことです。前述したとおりデモ機能を実装したのですが、こちらのフロントでのバリデーションを考える際にかなり時間をかけていました。この理由としては、受付開始と締切で指定タイプが異なる際に予約できない状態が複雑な組み合わせで発生するので、どうバリデーションで表現するかで悩んでいたためです。
数回のミーティングを通して方針を報告して議論をしましたが、チーム内でそもそも予約ができない予約受付期間をバリデーションで弾く必要があるのかを指摘され、予約受付ができない状態が勝手にユーザーの意図しない挙動であるという前提を持っていることに気づきました。 そこで、予約できない受付期間設定に関しては「設定できない」状態であるべきではなくユーザーが意図した動きをすることが重要であると結論づけ、バリデーションでは同じタイプ同士の受付開始/締切期間のものだけを弾くようにし、その他に関してはデモ機能でユーザーが正しく仕様が認識できるような設計にしました。
プロダクト開発においてユーザー目線に立つことは重要で弊社でも特に重要視していますが、それを再認識できる良い機会だったと思います。
さいごに
今回は自分/チームにおいて予約受付期間 プロジェクト を通して学んだ知見と反省を書き出してみました。色々な反省があり、知っている知識であっても、実際にやってみるとさらにその奥が知れたりといい経験ができたと個人的には思っています。
ここまで読んでいただきありがとうございました!