🐥

Railsで通知管理のためのDSLを設計する4つのステップ

2023/07/19に公開

はじめに

Railsでの開発を10年以上やって通知まわりのDSLを設計することが何度かあり、DSLの作り方を完璧に理解したので整理して共有します。

通知システムを作る

通知システムは一般に、複雑で全体像が把握しにくいものになりやすい特徴があります。
サービスのグロースでは通知を飛ばすのは一般的な手法ですが、そのために各チームがたくさん飛ばすことになり、全体が複雑になっていき、そして新しく通知を追加するのが困難になっていきます。

通知を追加したときに変化するものと変化しないものを明確にわけ、変化するもののためにDSLを設計することで、通知を追加しやすくかつ管理しやすいアーキテクチャにすることができます。

Step1: 通知システムのスコープを決める

まず作ろうとしているシステムのスコープを決めていきましょう。
新しく通知を追加するときを想像して、そのときに必要になる要素を並べてみて、どこまでを責務とするのかを考えるのがおすすめです。

例えばデスクトップ通知だとタイトルとメッセージをクリックしたときのURLが必要だなとか、デスクトップ通知だけでなくSlack通知もあるならそれを区別する識別子が必要になりますね。
定期的に送る通知ならその時刻とか回数なども必要になりますが、でもこれは通知システムには組み込まずに呼び出し側で持っておいた方がいいな、なんてことが考えられます。

Step2: 通知を追加するときのためのインターフェースを作る

新しく通知を追加するときには、通知の設定とその通知を呼び出す処理の二つが必要になります。
それぞれについてどんな書き方だと書きやすいかを考慮しながらインターフェースを作っていきましょう。

この作業は実はWebサービスをデザインするときの思考と同じですね。
必要な要素を並べてじっと見つめて配置を考える。そうするとおぼろげながら設計が見えてくる。

見えてこないなら既存のDSLをいくつか読んでみましょう。
RSpec, Bundler, ActionDispatch::Routing, Sinatraなど、身近に参考になるものは多いですね。

例えば通知を設定するインターフェースはこんな感じでどうでしょうか。

lib/utils/notification_provider.rb
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が、実際にユーザに送られるイメージですね。

設定した通知を呼び出すときは以下のようにしてみましょう。

some_controller.rb
  def index
    NotificationProvider.test_notification.new(title: 'タイトル').send_to(current_user)
  end

追加した通知のインスタンスを作ってそのsend_toを呼ぶという、シンプルな形に落ち着きました。

Step3: メタプログラミングによって値やメソッドを定義する

インターフェースを作ったら、それが動くようにメソッドを実装していきましょう。

まずはシステム部分のクラスを切ってそれを継承してやるようにします。
こうすることで、新しい通知を追加するときにはシステムの実装を見ずに済む、つまりそのとき行いたい作業に集中できるようになります。

lib/utils/notification_provider.rb
class NotificationProvider < NotificationProviderEngine::BaseProvider
  ...
end

システム部分の実装は以下のような形ですね。

lib/utils/notification_provider_engine/base_provider.rb
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
lib/utils/notification_provider_engine/value.rb
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に処理を委譲します。

lib/utils/notification_provider_engine/messenger.rb
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
lib/utils/notification_provider_engine/messenger/desktop.rb
module NotificationProviderEngine
  class Messenger
    class Desktop
      def initialize(value)
        @value = value
      end
      
      def send_to(user)
        ...
      end
    end
  end
end

作り終わったら

さぁ通知システムの実装が終わりました。ここからが大変なフェーズです。
他のメンバーに日常的に使ってもらわなくてはいけません。

Slackを開いてPull Requestとともに使い方を貼って、いかに使いやすいシステムになったかを声高に宣伝しましょう!
そしてメンバーのフィードバックをもらいながら、また次のアーキテクチャへの旅を続けましょう。

株式会社スタメン

Discussion