This document discusses replacing ActiveRecord callbacks with a publish-subscribe model using Wisper. It outlines some of the issues with callbacks, such as tight coupling, brittle tests, and synchronous operations. Wisper provides an alternative by decoupling business logic from models using an event-driven publish-subscribe approach. This improves testability and allows asynchronous background processing. The document also briefly covers Wisper features like broadcasting lifecycle events and integrating with ActiveJob for asynchronous jobs.
4. Business logic that you want
- before or after something happens
- tied to the lifecycle of the model
What are callbacks?
5. Business logic that you want
- before or after something happens
- tied to the lifecycle of the model
class Post < ActiveRecord::Base
after_commit :notify_editors, on: :create
private
def notify_editors
EditorMailer.send_notification(self).deliver_later
end
end
What are callbacks?
6. 4
Creating a record Updating a record Deleting a record
BEFORE VALIDATION
before_validation before_validation
before_validation_on_create before_validation_on_update
AFTER VALIDATION
after_validation after_validation
before_save before_save
before_create before_update before_destroy
AFTER CRUD ACTION
after_create after_update after_destroy
after_save after_save
after_commit, on: :create after_commit, on: :update after_commit, on: :destroy
7. 4
Creating a record Updating a record Deleting a record
BEFORE VALIDATION
before_validation before_validation
before_validation_on_create before_validation_on_update
AFTER VALIDATION
after_validation after_validation
before_save before_save
before_create before_update before_destroy
AFTER CRUD ACTION
after_create after_update after_destroy
after_save after_save
after_commit, on: :create after_commit, on: :update after_commit, on: :destroy
9. 6
class Message < ActiveRecord::Base
after_create :add_author_as_watcher
after_create :send_notification
private
def add_author_as_watcher
Watcher.create(watchable: self.root, user: author)
end
def send_notification
if Setting.notified_events.include?('message_posted')
Mailer.message_posted(self).deliver
end
end
end
10. 7
class Message < ActiveRecord::Base
after_create :add_author_as_watcher
after_create :send_notification
private
def add_author_as_watcher
Watcher.create(watchable: self.root, user: author)
end
def send_notification
if Setting.notified_events.include?('message_posted')
Mailer.message_posted(self).deliver
end
end
end
11. 8
class Message < ActiveRecord::Base
after_create :add_author_as_watcher
after_create :send_notification
private
def add_author_as_watcher
Watcher.create(watchable: self.root, user: author)
end
def send_notification
if Setting.notified_events.include?('message_posted')
Mailer.message_posted(self).deliver
end
end
end
13. 10
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
14. 10
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
describe Post do
subject { Post.new(title: 'Hello RUG::B') }
describe 'create' do
it 'creates a new feed item' do
expect { subject.save }.to change { FeedItem.count }.by(1)
expect(FeedItem.last.title).to eq('Hello RUG::B')
end
end
end
17. 13
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
18. 13
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
19. describe Post do
subject { Post.new(title: 'Hello RUG::B') }
describe 'create' do
it 'creates a new feed item' do
expect { subject.save }.to change { FeedItem.count }.by(1)
expect(FeedItem.last.title).to eq('Hello RUG::B')
end
end
end 13
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
20. describe Post do
subject { Post.new(title: 'Hello RUG::B') }
describe 'create' do
it 'creates a new feed item' do
expect { subject.save }.to change { FeedItem.count }.by(1)
expect(FeedItem.last.title).to eq('Hello RUG::B')
end
end
end 13
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
22. 15
class Post < ActiveRecord::Base
after_commit :notify_users, on: :create
private
def notify_users
PostMailer.send_notifications(self).deliver_later
end
end
23. 16
class Post < ActiveRecord::Base
after_commit :notify_users, on: :create
after_commit :generate_feed_item, on: :create
after_commit :notify_editors, on: :create
private
def notify_users
PostMailer.send_notifications(self).deliver_later
end
def generate_feed_item
FeedItemGenerator.create(self)
end
def notify_editors
EditorMailer.send_notification(self).deliver_later
end
end
24. 17
class Post < ActiveRecord::Base
after_commit :notify_users, on: :create
after_commit :generate_feed_item, on: :create
after_commit :notify_editors, on: :create
after_commit :add_user_points, on: :create
private
def notify_users
PostMailer.send_notifications(self).deliver_later
end
def generate_feed_item
FeedItemGenerator.create(self)
end
def notify_editors
EditorMailer.send_notification(self).deliver_later
end
def add_user_points
UserPointsService.recalculate_points(self.user)
end
end
26. 19
class Image < ActiveRecord::Base
after_commit :generate_image_renditions, on: :create
private
def generate_image_renditions
ImageService.create_renditions(self)
end
end
34. 21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
35. 21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
36. 21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
37. 21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
38. 21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
39. 21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
40. 21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
41. Building a
Social Platform
→ User feeds
→ Project feeds
→ User Notifications
→ Project Notifications
→ User Karma
42. 23
class Project < ActiveRecord::Base
after_commit :add_user_points, on: :create
private
def add_user_points
UserPointsService.recalculate_points(self.user)
end
end
43. 23
class Project < ActiveRecord::Base
after_commit :add_user_points, on: :create
private
def add_user_points
UserPointsService.recalculate_points(self.user)
end
end
class Comment < ActiveRecord::Base
after_commit :add_user_points, on: :create
private
def add_user_points
UserPointsService.recalculate_points(self.user)
end
end
44. 24
module UpdatesUserPoints
extend ActiveSupport::Concern
included do
after_commit :add_user_points, on: :create
def add_user_points
UserPointsService.recalculate_points(self.user)
end
end
end
class Project < ActiveRecord::Base
include UpdatesUserPoints
end
class Comment < ActiveRecord::Base
include UpdatesUserPoints
end
46. Wisper
A library providing Ruby objects with Publish-Subscribe capabilities
•Decouples core business logic from external concerns
•An alternative to ActiveRecord callbacks and Observers in Rails apps
•Connect objects based on context without permanence
•React to events synchronously or asynchronously
48. 28
class Project < ActiveRecord::Base
include Wisper::Publisher
after_commit :publish_creation, on: :create
private
def publish_creation
broadcast(:project_created, self)
end
end
57. 34
class Project < ActiveRecord::Base
include Wisper::Publisher
after_commit :publish_creation, on: :create
private
def publish_creation
broadcast(:project_created, self)
end
end
59. 35
class Project < ActiveRecord::Base
include Wisper.model
end
Project.subscribe(ProjectSubscriber.new)
60. 35
class Project < ActiveRecord::Base
include Wisper.model
end
class ProjectSubscriber
def after_create(project)
UserPointsService.recalculate_points(project)
end
end
Project.subscribe(ProjectSubscriber.new)
71. 44
describe Message do
subject { Message.new(text: 'Hello RUG::B') }
describe 'create' do
it 'broadcasts message creation' do
expect { subject.save }.to broadcast(:after_create, subject)
end
end
end
72. 45
describe MessageSubscriber do
let(:message) { Message.create(text: 'Hello RUG::B') }
describe 'after_create' do
it 'adds message author as watcher' do
MessageSubscriber.after_create(message)
expect(Watcher.last.user).to eq(message.author)
end
it 'adds updates the board counter' do
expect { MessageSubscriber.after_create(message) }
.to change { message.board.count }.by(1)
end
it 'sends a notification' do
MessageSubscriber.after_create(message)
expect(UserMailer).to receive(:send_notification).with(message.board.owner)
end
end
end
74. 46
What have we achieved?
#1 - We’ve removed unrelated business logic from our classes
75. 46
What have we achieved?
#1 - We’ve removed unrelated business logic from our classes
#2 - Decoupled our callbacks, making them easier to test
76. 46
What have we achieved?
#1 - We’ve removed unrelated business logic from our classes
#2 - Decoupled our callbacks, making them easier to test
#3 -DRY’d up and slimmed down our model code
77. 46
What have we achieved?
#1 - We’ve removed unrelated business logic from our classes
#2 - Decoupled our callbacks, making them easier to test
#3 -DRY’d up and slimmed down our model code
#4 - Moved our callback logic into background jobs
80. 48
Alternatives - Observers
class CommentObserver < ActiveRecord::Observer
def after_save(comment)
EditorMailer.comment_notification(comment).deliver
end
end
82. 49
Alternatives - Decorators
class CommentDecorator < ApplicationDecorator
decorates Comment
def create
save && send_notification
end
private
def send_notification
EditorMailer.comment_notification(comment).deliver
end
end
83. 50
Alternatives - Decorators
class CommentController < ApplicationController
def create
@comment = CommentDecorator.new(Comment.new(comment_params))
if @comment.create
# handle the success
else
# handle the success
end
end
end
86. 51
Alternatives - Trailblazer
class EditorNotificationCallback
def initialize(comment)
@comment = comment
end
def call(options)
EditorMailer.comment_notification(@comment).deliver
end
end
class Comment::Create < Trailblazer::Operation
callback :after_save, EditorNotificationCallback
91. 52
Wisper
→ Lightweight and clean integration
→ Well tested and maintained
→ Plenty of integration options
→ Not just for Rails or ActiveRecord
92. 52
Wisper
→ Lightweight and clean integration
→ Well tested and maintained
→ Plenty of integration options
→ Not just for Rails or ActiveRecord
→ Great for small to medium scale, and potentially more
93. 52
Wisper
→ Lightweight and clean integration
→ Well tested and maintained
→ Plenty of integration options
→ Not just for Rails or ActiveRecord
→ Great for small to medium scale, and potentially more
→ It’s the right tool for the job for us