Skip to content

Instantly share code, notes, and snippets.

@tatey
Created August 15, 2012 12:31
Show Gist options
  • Select an option

  • Save tatey/3359753 to your computer and use it in GitHub Desktop.

Select an option

Save tatey/3359753 to your computer and use it in GitHub Desktop.
# I've noticed we want to do two different things when something about a model
# changes. Typically you would use ActiveModel::Observers, but the callbacks you
# get only have limited context forcing us to save more state to get it back later.
#
# The idea of this is that we can craft our own callbacks using a pub/sub pattern
# and pass in as much transient context as we want. We also want to bring in our
# analytics (or kiss metrics) on some of these callbacks.
#
# We can make our tests faster because we only care that our controller published
# the notification. Other tests will catch who is subscribed and what they should
# do when they subscribe.
#
# We can also trigger notifications from the CLI, or opt-in and out of them at any time.
#
# Scroll to the comments to see a visual graph.
# Notification queue and registry. There is a default queue that lives in the
# main thread. Just like ActiveSupport::Notifications, it blocks.
module TBD
module Notifications
include Forwardable
def_delegators :notifier, :publish, :subscribe
class Notifier
attr_accessor :subscribers
def initialize
@subscribers = SubscriberMap.new
end
def publish name, *args
subscribers[name].each do |subscriber
subscribe.call *args
end
end
def subscribe name, subscriber
subscribers[name] << subscriber
end
end
class SubscriberMap
attr_accessor :map
def initialize
@map = {}
end
def [] name
map[name] = [] unless map.key? name
map[name]
end
end
def self.notifier
@notifier ||= Notifier.new
end
end
end
# Default notifier is injected into the base controller. We can swap this out
# in testing.
class ApplicationController
attr_writer :notifier
protected
def notifier
@notifier ||= TBD::Notifications.notifier
end
end
# An initializer sets up the subscriptions.
TBD::Notifications.subscribe 'plan.finalize', Subscriber::FeedItem.new
TBD::Notifications.subscribe 'plan.finalize', Subscriber::Member.new
# Trigger the notification from a controller.
class PlansController < ApplicationController
def finalize
notifier.publish 'plan.finalized', plan: {id: @plan.id}
end
end
# Subscribers are a proxy that pass the notification onto the correct
# object.
class Subscriber::FeedItem
def call name, payload
public_send name.gsub('.', '_'), payload # plan.finalized -> plan_finalized
end
def plan_finalized payload
plan = Plan.find payload[:plan][:id]
FeedItem.create notification_type: 'plan_finalized', target: plan
end
end
class Subscriber::Member
def call name, payload
public_send name.gsub('.', '_'), payload
end
def plan_finalized payload
plan = Plan.find payload[:plan][:id]
Service::Member::PlanFinalized.new(plan).deliver
end
end
# Service takes the plan and sends it to the members who have opt-in
# to receive these types of notifications.
class Service::Member::PlanFinalized
attr_accessor :plan
def initialize plan
@plan = plan
end
def deliver
plan.members.each do |member|
next unless member.preferences.email.plan_finalized?
MemberMailer.plan_finalized.deliver
end
end
end
class FeedItem < ActiveRecord::Base
end
class MemberMailer < ActionMailer::Base
end
# Notes
#
# * Notifications do not support patterns. Eg: plan.* versus plan.create
# * Passing an id in the payload and loading the object again could be unnecessary overhead.
# Totally overkill when compared to?
class PostsController
def finalize
# ...
Service::MemberMailer::PlanFinalize.new(@plan).deliver
Service::FeedItem.new(@plan).finalize_plan
# ...
end
end
@tatey
Copy link
Author

tatey commented Aug 15, 2012

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment