Railsで通知管理のためのDSLを設計する4つのステップ
はじめに
Railsでの開発を10年以上やって通知まわりのDSLを設計することが何度かあり、DSLの作り方を完璧に理解したので整理して共有します。
通知システムを作る
通知システムは一般に、複雑で全体像が把握しにくいものになりやすい特徴があります。
サービスのグロースでは通知を飛ばすのは一般的な手法ですが、そのために各チームがたくさん飛ばすことになり、全体が複雑になっていき、そして新しく通知を追加するのが困難になっていきます。
通知を追加したときに変化するものと変化しないものを明確にわけ、変化するもののためにDSLを設計することで、通知を追加しやすくかつ管理しやすいアーキテクチャにすることができます。
Step1: 通知システムのスコープを決める
まず作ろうとしているシステムのスコープを決めていきましょう。
新しく通知を追加するときを想像して、そのときに必要になる要素を並べてみて、どこまでを責務とするのかを考えるのがおすすめです。
例えばデスクトップ通知だとタイトルとメッセージをクリックしたときのURLが必要だなとか、デスクトップ通知だけでなくSlack通知もあるならそれを区別する識別子が必要になりますね。
定期的に送る通知ならその時刻とか回数なども必要になりますが、でもこれは通知システムには組み込まずに呼び出し側で持っておいた方がいいな、なんてことが考えられます。
Step2: 通知を追加するときのためのインターフェースを作る
新しく通知を追加するときには、通知の設定とその通知を呼び出す処理の二つが必要になります。
それぞれについてどんな書き方だと書きやすいかを考慮しながらインターフェースを作っていきましょう。
この作業は実はWebサービスをデザインするときの思考と同じですね。
必要な要素を並べてじっと見つめて配置を考える。そうするとおぼろげながら設計が見えてくる。
見えてこないなら既存のDSLをいくつか読んでみましょう。
RSpec, Bundler, ActionDispatch::Routing, Sinatraなど、身近に参考になるものは多いですね。
例えば通知を設定するインターフェースはこんな感じでどうでしょうか。
class NotificationProvider
notice :test_notification, to: [:desktop] do
@title = params[:title]
@message = 'メッセージ'
@url = Rails.application.routes.url_helpers.root_url
end
end
呼び出し方はRailsのルーティングを意識しつつ、ブロック内ではインスタンス変数を定義して終えるというRailsのcontrollerのような書き方になっています。
ここで定義した@title
や@message
が、実際にユーザに送られるイメージですね。
設定した通知を呼び出すときは以下のようにしてみましょう。
def index
NotificationProvider.test_notification.new(title: 'タイトル').send_to(current_user)
end
追加した通知のインスタンスを作ってそのsend_to
を呼ぶという、シンプルな形に落ち着きました。
Step3: メタプログラミングによって値やメソッドを定義する
インターフェースを作ったら、それが動くようにメソッドを実装していきましょう。
まずはシステム部分のクラスを切ってそれを継承してやるようにします。
こうすることで、新しい通知を追加するときにはシステムの実装を見ずに済む、つまりそのとき行いたい作業に集中できるようになります。
class NotificationProvider < NotificationProviderEngine::BaseProvider
...
end
システム部分の実装は以下のような形ですね。
module NotificationProviderEngine
class BaseProvider
def self.notice(event_type, options = {}, &)
define_singleton_method(event_type) do |params = {}|
value = Value.new(params, options[:to])
value.instance_exec(&)
Messenger.new(value)
end
end
end
end
module NotificationProviderEngine
class Value
attr_reader :params, :medias, :title, :message, :url
def initialize(params, medias)
@params = params
@medias = medias
end
end
end
値を管理するValue Objectを作っておくと、インスタンス間で値を取り回せるので便利です。
それとinstance_exec
を使うと、ブロックをそのインスタンスのコンテキストで実行することができます。
これを使うことで、ブロック内で定義されているインスタンス変数をそのインスタンスに持たせることができます。
Step:4 通知を送信する処理を実装する
あとは実際に通知を送信する実装をしていきましょう。
例えばデスクトップ通知とSlack通知をサポートしているならこんな感じでしょうか。
デスクトップ通知ならMessenger::Desktop
に、Slack通知ならMessenger::Slack
に処理を委譲します。
module NotificationProviderEngine
class Messenger
def initialize(value)
@value = value
end
def send_to(user)
if @value.medias.include?(:desktop)
Messenger::Desktop.new(@value).send_to(user)
end
if @value.medias.include?(:slack)
Messenger::Slack.new(@value).send_to(user)
end
end
end
end
module NotificationProviderEngine
class Messenger
class Desktop
def initialize(value)
@value = value
end
def send_to(user)
...
end
end
end
end
作り終わったら
さぁ通知システムの実装が終わりました。ここからが大変なフェーズです。
他のメンバーに日常的に使ってもらわなくてはいけません。
Slackを開いてPull Requestとともに使い方を貼って、いかに使いやすいシステムになったかを声高に宣伝しましょう!
そしてメンバーのフィードバックをもらいながら、また次のアーキテクチャへの旅を続けましょう。
Discussion