Webサービスを作ってると、メール通知とかアプリのプッシュ通知のような機能が求められることは非常によくある。 「ユーザがいちいちWebページを見に来なくてもいいように通知してあげないといけない」というのは誠にそのとおりだ。
さて、とりあえずRailsで作ったWebサービスがあるとしよう。どうやって通知する?
とりあえず購入時に購入内容をメール通知しよう
「最初からいろんな通知方法を考えて設計するのは無駄だし、とりあえずあとで必要になったらリファクタリングしよう。まずは必要な場所にぺぺって書いてしまおう」
class PaymentRecordController def create if record = PaymentRecord.create(payment_record_params) redirect_to dashbord_path send_notification(record) else flash[:error] = record.errors.full_messages.join("\n") end end private def send_notification(payment_record) UserMailer.item_purchased(payment_record) end end
はい。
自分が書いた商品レビューに「いいね!」がついたらメール通知してほしいな
「ん、それってユーザ的に本当に必要な"通知"なんだっけ?」という若干の疑問をもちつつ、コントローラに書いて複雑化するレベルには達していないので、とりあえずコントローラに同様に書きます、と。
class LikeController def create if like = Like.create(like_params) send_notificatioin(like) ... private def send_notification(like) target_user = like.likeable.owner from_user = like.user UserMailer.review_liked(target_user, from_user, like.likeable) end end
はい。
アプリを作ることになったので、スマホアプリのプッシュにも対応してほしいな
「まじか!!でも確かに必要だよね。」「でも、いいね!が付いたくらいで通知がくるのはうざいよね。」うーん、コントローラに・・・書く・・・?
class PaymentRecordController ... private def send_notification(payment_record) UserMailer.item_purchased(payment_record) PushNotification.send_item_purchased(payment_record) end end
class LikeController ... private def send_notification(like) target_user = like.likeable.owner from_user = like.user UserMailer.review_liked(target_user, from_user, like.likeable) end end
はい・・・だんだん危険な香りがしてきましたね。
プッシュ通知は消したら見えなくなるから、アプリ上で "通知した内容一覧" が見れるようにAPIをつくりたい
「なん・・・だと・・・?!」
詰みました・・・。
というのも、通知というのは今までは「投げっぱなし」とおもっていました、メール通知もプッシュ通知も。なので、Rails上で通知に関するモデルは作っていません。つまり、今まで通知した内容はどこからも取得ができません。
「とりあえず今までの内容は取れないのでそこは許してください!」
心機一転、通知まわりの機構を1から作り直せることになりました。さあどうする?
要件としては
- メール通知
- プッシュ通知
- 通知したもの一覧API
です。
# id: integer primary key # title: string not null # body: string not null # created_at: datetime not null # notifiable_type: string # notifiable_id: integer class NotificationItem < ApplicationRecord belongs_to :notifiable, polymorphic: true after_create_commit :send_notification private def send_notification(record) NotificationService.new(record).execute end end class NotificationService def initialize(notification_item) @notification_item = notification_item end def execute # ここでメール通知とプッシュ通知のロジックがいろいろ end end
いろいろやり方は有ると思います。たとえば、こんな感じでしょうか?
「あれ、これだと通知一覧が作れない?」
そう、各ユーザにどういう通知を届けたのか、という履歴がないと通知一覧は作れませんね。
# id: integer primary key # notification_item_id: integer foreign_key # user_id: integer foreign_key # created_at: datetime not null class NotificationHistory < ApplicationRecord belongs_to :notification_item belongs_to :user end
class NotificationService def initialize(notification_item) @notification_item = notification_item end def execute # ここでメール通知とプッシュ通知のロジックがいろいろ # 通知した内容を保存する NotificationHistory.create!( user: target_user, notification_item: @notification_item ) end end
class Api::User::NotificationHistoriesController < Api::ApplicationController def index @notification_histories = NotificationHistory.where(user: current_user) .order(created_at: :desc) .includes([:notification_item]) end end
json.notifications @notification_histories do |notification_history| json.partial! "notification_item", notification_history.notification_item json.sent_at notification_history.created_at end
みたいなかんじでしょうか。だいぶ端折ってますが
通知文言はユーザのセグメントごとに変えたいな
「む。NotificationItem側にtitle/bodyがあるとダメじゃん!」「じゃあ、NotificationHistory側に・・・とおもったけどこれは履歴テーブルだ」「うーん、結局タイトルとかを入れるのはNotificationService・・・?」
そろそろ辛くなってきましたね。
さて、擬似サンプルコードも書き疲れたのでようやく本題に。
結局 "通知" ってなんなの?
通知って「投げっぱなし」で実装しがちなんですが、大抵の場合はそうではなくて、「リアクティブなイベント購読」と考えるべきなんじゃないかと最近感じ始めてます。
- 通知が必要かもしれない イベント がある(Pub)
- イベントを 適宜フィルタ/変換しながら購読する サブシステムがある(Sub)
- 投げっぱなしに見える通知というのは、Subの create/updateトリガーである
ようは、Railsのコントローラとかでやるのは、「ユーザAがアイテムXを購入した」とか「ユーザBがアイテムYにレビューRを書いた」とか「ユーザAがレビューRにいいね!を付けた」とかそういうイベントをPublishするまで。個々の通知実装はその先で、たとえば
- Event.where(type: item_purchased).on_new_record(:send_item_purchase_confirmation_notification)
- Event.where(type: review_liked).on_new_record(:send_review_liked)
みたいな感じで、いい感じにsubscribeする。
そうすると、たとえばアプリのプッシュにくわえてブラウザプッシュも対応したいとかなっても、NotificationHistoryからエスパーして履歴を作ったりしなくても済む。
まとめ
とりあえず「通知」っぽいものを作るときには、今すぐ必要でなくても「イベント」と「通知履歴」テーブルは作っておいたほうがいいんじゃないかな。とおもった。以上。
(どうやって実装すればいいのかはまだよくわかっていない