abstract_notifierで通知を実装する - komagataのブログ

ここでminitestでのテストが書きづらいのでPRするって書いてましたが、それがマージされたバージョン0.3.2がリリースされました。

READMEに書いてありますがminitestではこういうふうに書くといい感じです。

require 'abstract_notifier/testing/minitest'

class EventsNotifierTestCase < Minitest::Test
  include AbstractNotifier::TestHelper

  test 'canceled' do
    assert_notifications_sent 1, identify: "123", body: "Alarma!" do
      EventsNotifier.with(profile: profile).canceled(event).notify_now
    end

    assert_notifications_enqueued 1, identify: "123", body: "Alarma!" do
      EventsNotifier.with(profile: profile).canceled(event).notify_later
    end
  end
end

去年MacBook Pro買ったときからハードウェアに関する物欲がスッと消えてる気がする。

MacBook Pro16インチ買った - komagataのブログ

MacBook Proが良すぎる!ベタ惚れ!って感じではなく、

「イラつく欠点が無い」「困る点が全くない」

という感じでハードのことを考える時間が減った。

このMacBook Proが特別というより、俺のパソコンでやる作業の用途・OSのこなれ感・パソコンの処理速度・ディスプレイ・キーボード・トラックパッドが調和したタイミングハードへ意識を向けることが少なくなり、単なる道具化するんだと思う。

昔もこういうタイミングあったな〜と思い出すのは、ThinkPad X61でWindows XPとcoLinuxを使ってた2008年ごろのセットアップ。

この時もOSもこなれてたし、ディスプレイ・キーボード・ポインティングデバイスに不満が一切なくてハードへの興味が薄れたなぁ。

ソフトウェアに集中できるのと良い状態だと思います。

Callbacksの問題点についてはこちら。 ActiveRecordのObserversやCallbacksの問題点 - komagataのブログ

Callbacksの問題点を解決する薄いpub/subライブラリのnewspaper gemを作りました。

こういうのを

class User
  after_create UserCallbacks.new
end

class UserCallbacks
  def after_save(user)
    # do something
  end
end

@user.save

こう書き換えられます。

# app/models/sign_up_notifier.rb
class SignUpNotifier
  def call(payload)
    # do something
  end
end

# config/initializers/newspaper.rb
Newspaper.subscribe(:user_create, SignUpNotifier.new)

# app/controllers/users_controller.rb
Newspaper.publish(:user_create, payload)

SubscriberはただのRubyオブジェクトなのでテストし易いです。

既存のpub/subライブラリのように非同期実行する機能などはありません。また、ちゃんとしたPublisher / Subscriberパターンのようなクラス構成にはなっておらず、簡単な構造になっています。

このgemを作ってるところの動画

僕らがやっているプログラミングスクールのフィヨルドブートキャンプでは最近ペアプロやモブプロが盛んに行われています。もっと敷居を下げるために「このgemを作っているところ」をモブプロでやって録画しました。

オープソースで公開しているフィヨルドブートキャンプ本体のCallbacks をこのgemに置き換えるところもやりました。

簡単なgemなのでRubyプログラマーの人にはあまり面白く無いかもですが、今Ruby入門中の人やgemを作りたい人の参考になれば幸いです。

2点あると思う。

  1. Callbackを呼ばれて欲しくない時が結構ある。暗黙的にそれが起こるとキツイ。
  2. 複数の処理を一箇所に書いちゃいがち。複数の処理に依存関係があるとキツイ。

Serviceクラスの問題点

明示的に呼ぶので1をクリアしてる。 2をクリアしてない。

しっかり処理毎にクラスに分けてServiceクラスから呼び出してればいいが、Serviceクラスの粒度がCallbackのエントリポイントと変わりない場合が多いので複数の処理を一箇所に書いちゃいがち。

pub/subを使う

明示的に呼ぶので1をクリアしている。 いかにも処理毎にクラスを作るようにできてるので2をクリアしている。

既存のpub/subライブラリ

これでいいんだけど、この用途には非同期で実行する機能はオーバースペックに感じる。 (特によくあるユースケースの通知などではそちらの方で非同期の仕組みを備えているため)

薄いpub/subライブラリ

それを解決するために、非同期機能を持たず、イベントに文字列(シンボル)を使うpub/subライブラリを作りました。

komagata/newspaper

newspaperについて詳しくは下記エントリーに書きました。

newspaperでActiveRecordのCallbacksを置き換える - komagataのブログ

active_delivery gemがいい感じ。

active_deliveryはActionMailer 的な処理 をまとめるWrapperです。

例によってWeb上にはREADMEと作者の方のブログエントリーしか見当たらなかったので、使い方を知るにはソース読んだ方が早いです。僕が代わりに読んでおいたのでちょっとしたプラス情報を書いておきます。

使い方

def after_signup
  ActivityMailer.user_signup.deliver_later if user.receive_emails?
  DiscordNotifier.user_signup.notify_later if user.receive_discord?
end

こんな感じに書いてたのを

def after_signup
  ActivityDelivery.notify(:user_signup, user)
end

こんな感じに描けるようになります。

ActivityDelivery.notify!(:user_signup, user)

また、デフォルトでは非同期の方を呼び出しますが、notify!の方だと同期版を呼んでくれます。

ActivityDelivery.with(user: user).notify(:user_signup)

そして、ActionMailerのようにParameterizedな呼び方もできます。

概要

直接使うDeliveryクラスと実際に動くMailerクラス(やその他の独自のDeliveryクラス)、その間をつなぐLineクラスがあります。

Image from Gyazo

デフォルトで使われるActiveDelivery::Lines::Mailerを見るとどうやって間を繋いでいるのかがわかります。

実装

これを用意しておくだけで既に、

# app/deliveries/activity_delivery
class ActivityDelivery < ActiveDelivery::Base
end

下記が呼べてメールが送信されます。

ActivityDelivery.notify(:user_signup, user)

何故かとういうと、デフォルトでこういうクラスのメソッドを探してメソッドを呼んでくれるからです。

class_name.gsub(/Delivery$/, "Mailer")

Mailerはデフォルトで呼ぶんですね。

class ActivityDelivery < ActiveDelivery::Base
  unregister_line :mailer
end

こうしておけばデフォルトのMailerを呼ぶのもなしにできます。

独自のDeliveryクラスを使うには自分でLineクラスを書く必要があります。

しかし、abstract_notifierを使って作った通知クラスがあれば、そちらにActiveDelivery::Lines::Notiferクラスが同伴されているので自分で書く必要はないです。便利!

abstract_notifierについてはこちらのエントリーを見てください。

abstract_notifierで通知を実装する - komagataのブログ

class ActivityDelivery < ActiveDelivery::Base
  register_line :discord,
                ActiveDelivery::Lines::Notifier,
                resolver: ->(_) { DiscordNotifier }
end

これでActivityMailerとDiscordNotifierのメソッドを一緒に呼んでくれるようになります。

テスト

テストはこんな感じで同期版・非同期版、普通版・Parameterized版をそれぞれテストしておけば安心だと思います。

require 'test_helper'

class ActivityDeliveryTest < ActiveSupport::TestCase
  test '.notify(:user_signup)' do
    params = {
      body: 'user signup!'
      user: users(:foo)
    }

    assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 2 do
      ActivityDelivery.notify!(:user_signup, **params)
    end

    assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 2 do
      ActivityDelivery.notify(:user_signup, **params)
    end

    assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 2 do
      ActivityDelivery.with(**params).notify!(:user_signup)
    end

    assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 2 do
      ActivityDelivery.with(**params).notify(:user_signup)
    end
  end
end

abstract_notifier gemがいい感じ。

abstract_notifierはPush Notificationのようなテキストベースの通知をActionMailerと同じような感じに実装するためのgemです。色々やってくれるというより、名前の通り枠組みを提供する感じです。

実装

Image from Gyazo

要するにabstract_notifierに従って実装しておくと、

DiscordNotifier.user_signup(user).notify_now

DiscordNotifier.user_signup(user).notify_later

こんな感じにActionMailerのように同期版、非同期版を選んで実行できたり、

DiscordNotifier.with(user).user_signup.notify_later

こんな感じにActionMailerのようにParametarizedな呼び方ができます。

弊社のフィヨルドブートキャンプアプリではサイト内通知とDiscord通知をこれを使うものに置き換えました。

class DiscordNotifier < AbstractNotifier::Base
  self.driver = DiscordDriver.new
  self.async_adapter = DiscordAsyncAdapter.new
end

実装は、AbstractNotifier::Baseを継承したクラスで同期処理用のdriverと非同期用のasync_adapterを実装してセットしておくとOKです。

普通のRubyクラスでOKですが、それぞれcallメソッドとenqueueメソッドを実装している必要があります。両方ともAbstractNotifier::Baseが持ってるnotificationを経由して呼び出されます。

普通は通知でやりたいことは共通だと思うので、同期用のdriverは普通に書いて、非同期用のasync_adapterはActiveJob経由でdriverを呼び出すように書いておけば実装が共通にできていいと思います。

driverはこんな感じ。

class DiscordDriver
  def call(params)
    Discord::Notifier.message(
      params[:body],
      username: params[:name],
      url: params[:webhook_url]
    )
  end
end

async_adapterはこんな感じ。 Parameterized版はコンストラクタが呼ばれるのでenqueueでもそれが使われるように書いておく必要があります。

class DiscordAsyncAdapter
  def initialize(params = {})
    @params = params
  end

  def enqueue(_, params = {})
    params.merge!(@params)
    DiscordJob.perform_later(params)
  end
end

jobはこんな感じにしておくだけで非同期にできて便利ですね。

class DiscordJob < ApplicationJob
  queue_as :default

  def perform(params)
    DiscordDriver.new.call(params)
  end
end

テスト

テストは同期版、非同期版、Parameterized版をそれぞれテストしておくと安心です。gemにはrspec向けのhelperしかないですが、AbstractNotifier::Testing::Driver.deliveriesAbstractNotifier::Testing::Driver.enqueued_deliveriesにそれぞれdeliverしたものが入るようになっていたのでminitestではこれらを直接見る様に書くといいんじゃないかと思います。

長いのでassert_deliveryとかあった方が便利そうなのでminitest_helperを後でPRしようかなと思います。

2022年06年04日:マージされました。

abstract_notifierをminitestでテストする - komagataのブログ

require 'test_helper'

class DiscordNotifierTest < ActiveSupport::TestCase
  setup do
    @params = {
      body: 'new user signup!',
      sender: users(:bob),
      name: 'bob',
      webhook_url: 'https://discord.com/api/webhooks/0123456789/xxxxxxxx'
    }
  end

  test '.user_signup' do
    notification = DiscordNotifier.user_signup(@params)

    assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do
      notification.notify_now
    end

    assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do
      notification.notify_later
    end

    # Parameterized
    notification = DiscordNotifier.with(@params).user_signup

    assert_difference -> { AbstractNotifier::Testing::Driver.deliveries.count }, 1 do
      notification.notify_now
    end

    assert_difference -> { AbstractNotifier::Testing::Driver.enqueued_deliveries.count }, 1 do
      notification.notify_later
    end
  end
end

ちなみにこのgemに関する情報は作者本人のブログエントリーとREADMEぐらいしか見当たらなかったのでコード読んだ方が早いです。

その後

「別に同期・非同期切り替える必要ないし、似たような実装で今動いてるから置き換えるまではいかないかな〜」

と思うかもですね。

しかし僕はこれで通知という視点で抽象化するというアイデアを知るまではDiscord通知とメール通知を同じものだと思ってなかったので共通化してスッキリしました。他のメンバーに通知を実装してもらう時もオリジナル実装よりもこういうgemになっている方がルールが明確でよさそうに思います。

そして真の目的はこれに対応させておくと後でエントリーを書こうと思っているactive_deliveryでシュッと使えるようになる点です。

つづく

active_deliveryで通知をまとめる - komagataのブログ

Image from Gyazo

PiroさんからITエンジニア1年生のためのまんがでわかるLinuxをご献本いただきました。ありがとうございます!

こちらは僕らがやっているフィヨルドブートキャンプでも参考書籍にさせていただいている「まんがでわかるLinux シス管系女子」にLinuxの本当の入門者用の内容を追加して新しくした感じの本になっています。(今から買うなら今回の新しい方一択です)

完全に「新人教育する人の目線」になっちゃうんですが、Linuxに入門する人にとってより入りやすくなっていて素晴らしかったです。

弊社スクールでもLinuxを入門するときに、

「Terminalをなぜ使うのかわからない」 「どういうシチュエーションでみんな使うのかわからない」

などイメージがしづらいという部分でつまづく人が多くいます。

新人が入社したところから仕事で必要になって一つづつ学んでいくというストーリーがまんがで描かれることによってその辺りのイメージがとっても掴みやすくなっていると思います。

Image from Gyazo

Next.jsと組み合わせる永続化のAPIって何が一般的でしょう?

僕が開発するなら何でもいいんですが、フィヨルドブートキャンプのJavascriptコース最終盤のカリキュラムで生徒の方もスクラムで開発するものになるので、そこで使う技術はカリキュラムとしても用意する必要があります。

考え方としては、Javascriptのプログラマーとして就職したら遭遇するよくある構成がいいなと思っています。

Next.jsを使うというのは決定していて、DB・API部分の選択肢が豊富で迷っちゃいます。

  • Firebase(ちょっとしたサイトならいいが、しっかりしたサービスだと避けるイメージ)
  • OpenAPI(サーバーサイドがガッツリし過ぎ感)
  • GraphQL(最有力か?)

みなさんは何を使ってますかね〜?

ヘッドホンを伸ばしたらバキッと一部壊れちゃいました。

妻が目立たないテープで縛ってくれたので騙し騙し使っていきます…。

Image from Gyazo

最近のrailsでのパスワード認証gemって何使ってます?

The Ruby Tool BoxのWeb Authentiationカテゴリーを参照。

devise

やってくれることは多いがカスタマイズしづらいので嫌う人も多い。俺はこれ使ってる。 しかし登場から時間が経ち、railsの仕様に追従していくにつれて初期の設計に無理がきてる感。特にrails7からはさらに。

sorcery

やってくれることは少ないが、カスタマイズしやすい。bootcampのアプリはこれを使ってる。(確か @hrysd が入れた) リニューアル版sorcery(sorcery-rework)が別リポジトリで進んでるが、当分完成しそうにな。

authlogic

使ったことないけどちょっと古い感?

clearance

使ったことないけどちょっと古い感2

railsの基本機能を使って自作

これが多いのかも?

個人的には認証などのセキュリティが大きく絡むところはなるべくgemなどみんなが見ている(セキュリティパッチがすぐ入りやすい)ところのを使いたいな〜と思っております。

みなさんのプロジェクトではどんな感じでしょう? 温度感が知りたい感。