Replacing ActiveRecord callbacks
with Pub/Sub
edenspiekermann_
March 2nd, 2017
Niall Burkley (Edenspiekermann_)
@niallburkley | github.com/nburkley | niallburkley.com
Callbacks
ACTIVE RECORD
What are callbacks?
Business logic that you want
- before or after something happens
- tied to the lifecycle of the model
What are callbacks?
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?
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
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
Unrelated Business Logic in your model
- Violation of Single Responsibility Principle
#1
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
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
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
Tight Coupling & brittle tests
#2
10
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
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
11
class FeedItemGenerator
def self.create(subject)
FeedItem.create(
subject: subject,
owner: subject.user
)
end
end
12
class FeedItemGenerator
def self.create(subject, published)
FeedItem.create(
subject: subject,
owner: subject.user,
published: published
)
end
end
13
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
13
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 13
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 13
class Post < ActiveRecord::Base
after_commit :generate_feed_item, on: :create
private
def generate_feed_item
FeedItemGenerator.create(self)
end
end
Your Models are going to grow
#3
15
class Post < ActiveRecord::Base
after_commit :notify_users, on: :create
private
def notify_users
PostMailer.send_notifications(self).deliver_later
end
end
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
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
Callbacks are Synchronous
#4
19
class Image < ActiveRecord::Base
after_commit :generate_image_renditions, on: :create
private
def generate_image_renditions
ImageService.create_renditions(self)
end
end
20
Browser Controller Image ImageService
POST /images
create(params)
create_renditions(image)
image renditions
image
success
after_create callback
20
Browser Controller Image ImageService
POST /images
create(params)
create_renditions(image)
image renditions
image
success
after_create callback
20
Browser Controller Image ImageService
POST /images
create(params)
create_renditions(image)
image renditions
image
success
after_create callback
20
Browser Controller Image ImageService
POST /images
create(params)
create_renditions(image)
image renditions
image
success
after_create callback
20
Browser Controller Image ImageService
POST /images
create(params)
create_renditions(image)
image renditions
image
success
after_create callback
20
Browser Controller Image ImageService
POST /images
create(params)
create_renditions(image)
image renditions
image
success
after_create callback
20
Browser Controller Image ImageService
POST /images
create(params)
create_renditions(image)
image renditions
image
success
after_create callback
21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
21
Browser Controller Image Feed Generator
POST /images
create(params)
create_renditions(image)
image
success
ImageService
background process
image renditions
Building a
Social Platform
→ User feeds
→ Project feeds
→ User Notifications
→ Project Notifications
→ User Karma
23
class Project < ActiveRecord::Base
after_commit :add_user_points, on: :create
private
def add_user_points
UserPointsService.recalculate_points(self.user)
end
end
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
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
Wisper
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
27
Project
ProjectSubscriber
UserSubscriber
create
User
create
Events
project_created(self)
user_created(self)
user_created(user)
project_ created(post)
<broadcast>
<broadcast>
28
class Project < ActiveRecord::Base
include Wisper::Publisher
after_commit :publish_creation, on: :create
private
def publish_creation
broadcast(:project_created, self)
end
end
29
Project.subscribe(ProjectSubscriber.new)
30
class ProjectSubscriber
def project_created(project)
UserPointsService.recalculate_points(project)
end
end
30
class ProjectSubscriber
def project_created(project)
UserPointsService.recalculate_points(project)
end
end
class Project < ActiveRecord::Base
include Wisper::Publisher
after_commit :publish_creation, on: :create
private
def publish_creation
broadcast(:project_created, self)
end
end
30
class ProjectSubscriber
def project_created(project)
UserPointsService.recalculate_points(project)
end
end
class Project < ActiveRecord::Base
include Wisper::Publisher
after_commit :publish_creation, on: :create
private
def publish_creation
broadcast(:project_created, self)
end
end
30
class ProjectSubscriber
def project_created(project)
UserPointsService.recalculate_points(project)
end
end
class Project < ActiveRecord::Base
include Wisper::Publisher
after_commit :publish_creation, on: :create
private
def publish_creation
broadcast(:project_created, self)
end
end
31
Too much boilerplate
Wisper::ActiveRecord
Wisper::ActiveRecord
Broadcast Lifecycle Events
after_create
after_destroy
create_<model_name>_{successful, failed}
update_<model_name>_{successful, failed}
destroy_<model_name>_{successful, failed}
<model_name>_committed
after_commit
after_rollback
34
class Project < ActiveRecord::Base
include Wisper::Publisher
after_commit :publish_creation, on: :create
private
def publish_creation
broadcast(:project_created, self)
end
end
35
class Project < ActiveRecord::Base
include Wisper.model
end
35
class Project < ActiveRecord::Base
include Wisper.model
end
Project.subscribe(ProjectSubscriber.new)
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)
36
Make it Asynchronous
Wisper::ActiveJob
38
Post.subscribe(ProjectSubscriber, async: true)
38
Post.subscribe(ProjectSubscriber, async: true)
38
class ProjectSubscriber
def self.post_created(post)
UserPointsService.recalculate_points(post)
end
end
Post.subscribe(ProjectSubscriber, async: true)
39
What about tests?
40
class Message < ActiveRecord::Base
after_create :add_author_as_watcher
after_create :reset_counters!
after_create :send_notification
end
41
class MessageTest < ActiveSupport::TestCase
def test_create
topics_count = @board.topics_count
messages_count = @board.messages_count
message = Message.new(board: @board,
subject: 'Test message',
content: 'Test message content',
author: @user)
assert message.save
@board.reload
# topics count incremented
assert_equal topics_count + 1, @board[:topics_count]
# messages count incremented
assert_equal messages_count + 1, @board[:messages_count]
assert_equal message, @board.last_message
# author should be watching the message
assert message.watched_by?(@user)
end
end
42
class MessageTest < ActiveSupport::TestCase
def test_create
topics_count = @board.topics_count
messages_count = @board.messages_count
message = Message.new(board: @board,
subject: 'Test message',
content: 'Test message content',
author: @user)
assert message.save
@board.reload
# topics count incremented
assert_equal topics_count + 1, @board[:topics_count]
# messages count incremented
assert_equal messages_count + 1, @board[:messages_count]
assert_equal message, @board.last_message
# author should be watching the message
assert message.watched_by?(@user)
end
end
Wisper::Rspec
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
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
46
What have we achieved?
46
What have we achieved?
#1 - We’ve removed unrelated business logic from our classes
46
What have we achieved?
#1 - We’ve removed unrelated business logic from our classes
#2 - Decoupled our callbacks, making them easier to test
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
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
47
Alternatives?
48
Alternatives - Observers
48
Alternatives - Observers
class CommentObserver < ActiveRecord::Observer
def after_save(comment)
EditorMailer.comment_notification(comment).deliver
end
end
49
Alternatives - Decorators
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
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
51
Alternatives - Trailblazer
51
Alternatives - Trailblazer
class Comment::Create < Trailblazer::Operation
callback :after_save, EditorNotificationCallback
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
52
Wisper
52
Wisper
→ Lightweight and clean integration
52
Wisper
→ Lightweight and clean integration
→ Well tested and maintained
52
Wisper
→ Lightweight and clean integration
→ Well tested and maintained
→ Plenty of integration options
52
Wisper
→ Lightweight and clean integration
→ Well tested and maintained
→ Plenty of integration options
→ Not just for Rails or ActiveRecord
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
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
(👋 & Thanks) unless questions?
Niall Burkley (Edenspiekermann_)
@niallburkley | github.com/nburkley | niallburkley.com

More Related Content

PPTX
WordPress plugin #2
PDF
Javascript Application Architecture with Backbone.JS
PDF
Cakephp's Cache
 
PDF
Introduction to ReactJS and Redux
PDF
Voyager: The Widget Router
PDF
Reduxing like a pro
PDF
FIWARE Tech Summit - Empower Your CKAN
PDF
Django
WordPress plugin #2
Javascript Application Architecture with Backbone.JS
Cakephp's Cache
 
Introduction to ReactJS and Redux
Voyager: The Widget Router
Reduxing like a pro
FIWARE Tech Summit - Empower Your CKAN
Django

Viewers also liked (17)

PDF
Quick Dissolve Retailer Presentation
PDF
Action taken by Hon'ble Supreme Court of India
PPTX
¿Cómo distribuye las ganancias Uber?
PDF
Dynamo presentazione #coopupbo - 5-10-2016
PPTX
Evaluación del-desempeño-docente-20-02-17
PPTX
Nuevo planetario
PPSX
WARRASE
PPTX
Q6 evaluation
PDF
Cuadernillo de primero de secundaria español
PPTX
Antecedentes y ubicación de la Sociología como ciencia
DOCX
Si al número 7893 le agrego 27 números más
PDF
Test Driven Problem Solving - AOC Argentina 2017
PDF
La necesidad de corregir el presupuesto 2017, y prioridades para un ajuste fi...
PPTX
Battlefield 1
PDF
04. Debora Miceli: Remuneração - Conceitualização e desenho de cargos
DOCX
AMIT.R DOC
PDF
Hack&Beers Cadiz Análisis de Malware Cuckoo Sandbox
Quick Dissolve Retailer Presentation
Action taken by Hon'ble Supreme Court of India
¿Cómo distribuye las ganancias Uber?
Dynamo presentazione #coopupbo - 5-10-2016
Evaluación del-desempeño-docente-20-02-17
Nuevo planetario
WARRASE
Q6 evaluation
Cuadernillo de primero de secundaria español
Antecedentes y ubicación de la Sociología como ciencia
Si al número 7893 le agrego 27 números más
Test Driven Problem Solving - AOC Argentina 2017
La necesidad de corregir el presupuesto 2017, y prioridades para un ajuste fi...
Battlefield 1
04. Debora Miceli: Remuneração - Conceitualização e desenho de cargos
AMIT.R DOC
Hack&Beers Cadiz Análisis de Malware Cuckoo Sandbox
Ad

Similar to Replacing ActiveRecord callbacks with Pub/Sub (20)

PDF
Crud operations using aws dynamo db with flask ap is and boto3
PDF
Test-driven Development with Drupal and Codeception (DrupalCamp Brighton)
PDF
Introduction to backbone presentation
PDF
OpenWhisk Under the Hood -- London Oct 16 2016
KEY
Android workshop
PPTX
Python from zero to hero (Twitter Explorer)
PDF
TurboGears2 Pluggable Applications
ODP
CodeIgniter PHP MVC Framework
PDF
Building Universal Web Apps with React ForwardJS 2017
PDF
os-php-wiki5-a4
PDF
os-php-wiki5-a4
PDF
ReactJS for Programmers
PDF
Tips and tricks for building api heavy ruby on rails applications
PPTX
Writing automation tests with python selenium behave pageobjects
PPTX
DevOps Hackathon: Session 3 - Test Driven Infrastructure
PDF
Laravel Design Patterns
KEY
WordPress Developers Israel Meetup #1
PDF
Simple restfull app_s
PPTX
Zend framework
PPTX
Quick Fetch API Introduction
Crud operations using aws dynamo db with flask ap is and boto3
Test-driven Development with Drupal and Codeception (DrupalCamp Brighton)
Introduction to backbone presentation
OpenWhisk Under the Hood -- London Oct 16 2016
Android workshop
Python from zero to hero (Twitter Explorer)
TurboGears2 Pluggable Applications
CodeIgniter PHP MVC Framework
Building Universal Web Apps with React ForwardJS 2017
os-php-wiki5-a4
os-php-wiki5-a4
ReactJS for Programmers
Tips and tricks for building api heavy ruby on rails applications
Writing automation tests with python selenium behave pageobjects
DevOps Hackathon: Session 3 - Test Driven Infrastructure
Laravel Design Patterns
WordPress Developers Israel Meetup #1
Simple restfull app_s
Zend framework
Quick Fetch API Introduction
Ad

Recently uploaded (20)

PDF
sbt 2.0: go big (Scala Days 2025 edition)
PDF
A comparative study of natural language inference in Swahili using monolingua...
PDF
sustainability-14-14877-v2.pddhzftheheeeee
PDF
TrustArc Webinar - Click, Consent, Trust: Winning the Privacy Game
PDF
Developing a website for English-speaking practice to English as a foreign la...
PDF
Two-dimensional Klein-Gordon and Sine-Gordon numerical solutions based on dee...
PPTX
Chapter 5: Probability Theory and Statistics
PPT
Module 1.ppt Iot fundamentals and Architecture
PPTX
Final SEM Unit 1 for mit wpu at pune .pptx
PPT
What is a Computer? Input Devices /output devices
PPTX
The various Industrial Revolutions .pptx
PDF
Flame analysis and combustion estimation using large language and vision assi...
PDF
Taming the Chaos: How to Turn Unstructured Data into Decisions
PDF
A proposed approach for plagiarism detection in Myanmar Unicode text
PDF
A contest of sentiment analysis: k-nearest neighbor versus neural network
PDF
Getting started with AI Agents and Multi-Agent Systems
PDF
The influence of sentiment analysis in enhancing early warning system model f...
PDF
Hybrid horned lizard optimization algorithm-aquila optimizer for DC motor
PPTX
Microsoft Excel 365/2024 Beginner's training
PPTX
Configure Apache Mutual Authentication
sbt 2.0: go big (Scala Days 2025 edition)
A comparative study of natural language inference in Swahili using monolingua...
sustainability-14-14877-v2.pddhzftheheeeee
TrustArc Webinar - Click, Consent, Trust: Winning the Privacy Game
Developing a website for English-speaking practice to English as a foreign la...
Two-dimensional Klein-Gordon and Sine-Gordon numerical solutions based on dee...
Chapter 5: Probability Theory and Statistics
Module 1.ppt Iot fundamentals and Architecture
Final SEM Unit 1 for mit wpu at pune .pptx
What is a Computer? Input Devices /output devices
The various Industrial Revolutions .pptx
Flame analysis and combustion estimation using large language and vision assi...
Taming the Chaos: How to Turn Unstructured Data into Decisions
A proposed approach for plagiarism detection in Myanmar Unicode text
A contest of sentiment analysis: k-nearest neighbor versus neural network
Getting started with AI Agents and Multi-Agent Systems
The influence of sentiment analysis in enhancing early warning system model f...
Hybrid horned lizard optimization algorithm-aquila optimizer for DC motor
Microsoft Excel 365/2024 Beginner's training
Configure Apache Mutual Authentication

Replacing ActiveRecord callbacks with Pub/Sub

  • 1. Replacing ActiveRecord callbacks with Pub/Sub edenspiekermann_ March 2nd, 2017 Niall Burkley (Edenspiekermann_) @niallburkley | github.com/nburkley | niallburkley.com
  • 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
  • 8. Unrelated Business Logic in your model - Violation of Single Responsibility Principle #1
  • 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
  • 12. Tight Coupling & brittle tests #2
  • 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
  • 16. 12 class FeedItemGenerator def self.create(subject, published) FeedItem.create( subject: subject, owner: subject.user, published: published ) 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
  • 21. Your Models are going to grow #3
  • 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
  • 27. 20 Browser Controller Image ImageService POST /images create(params) create_renditions(image) image renditions image success after_create callback
  • 28. 20 Browser Controller Image ImageService POST /images create(params) create_renditions(image) image renditions image success after_create callback
  • 29. 20 Browser Controller Image ImageService POST /images create(params) create_renditions(image) image renditions image success after_create callback
  • 30. 20 Browser Controller Image ImageService POST /images create(params) create_renditions(image) image renditions image success after_create callback
  • 31. 20 Browser Controller Image ImageService POST /images create(params) create_renditions(image) image renditions image success after_create callback
  • 32. 20 Browser Controller Image ImageService POST /images create(params) create_renditions(image) image renditions image success after_create callback
  • 33. 20 Browser Controller Image ImageService POST /images create(params) create_renditions(image) image renditions image success after_create callback
  • 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
  • 51. 30 class ProjectSubscriber def project_created(project) UserPointsService.recalculate_points(project) end end class Project < ActiveRecord::Base include Wisper::Publisher after_commit :publish_creation, on: :create private def publish_creation broadcast(:project_created, self) end end
  • 52. 30 class ProjectSubscriber def project_created(project) UserPointsService.recalculate_points(project) end end class Project < ActiveRecord::Base include Wisper::Publisher after_commit :publish_creation, on: :create private def publish_creation broadcast(:project_created, self) end end
  • 53. 30 class ProjectSubscriber def project_created(project) UserPointsService.recalculate_points(project) end end class Project < ActiveRecord::Base include Wisper::Publisher after_commit :publish_creation, on: :create private def publish_creation broadcast(:project_created, self) end end
  • 56. Wisper::ActiveRecord Broadcast Lifecycle Events after_create after_destroy create_<model_name>_{successful, failed} update_<model_name>_{successful, failed} destroy_<model_name>_{successful, failed} <model_name>_committed after_commit after_rollback
  • 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
  • 58. 35 class Project < ActiveRecord::Base include Wisper.model 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)
  • 67. 40 class Message < ActiveRecord::Base after_create :add_author_as_watcher after_create :reset_counters! after_create :send_notification end
  • 68. 41 class MessageTest < ActiveSupport::TestCase def test_create topics_count = @board.topics_count messages_count = @board.messages_count message = Message.new(board: @board, subject: 'Test message', content: 'Test message content', author: @user) assert message.save @board.reload # topics count incremented assert_equal topics_count + 1, @board[:topics_count] # messages count incremented assert_equal messages_count + 1, @board[:messages_count] assert_equal message, @board.last_message # author should be watching the message assert message.watched_by?(@user) end end
  • 69. 42 class MessageTest < ActiveSupport::TestCase def test_create topics_count = @board.topics_count messages_count = @board.messages_count message = Message.new(board: @board, subject: 'Test message', content: 'Test message content', author: @user) assert message.save @board.reload # topics count incremented assert_equal topics_count + 1, @board[:topics_count] # messages count incremented assert_equal messages_count + 1, @board[:messages_count] assert_equal message, @board.last_message # author should be watching the message assert message.watched_by?(@user) end end
  • 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
  • 73. 46 What have we achieved?
  • 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
  • 85. 51 Alternatives - Trailblazer class Comment::Create < Trailblazer::Operation callback :after_save, EditorNotificationCallback
  • 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
  • 88. 52 Wisper → Lightweight and clean integration
  • 89. 52 Wisper → Lightweight and clean integration → Well tested and maintained
  • 90. 52 Wisper → Lightweight and clean integration → Well tested and maintained → Plenty of integration options
  • 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
  • 94. (👋 & Thanks) unless questions? Niall Burkley (Edenspiekermann_) @niallburkley | github.com/nburkley | niallburkley.com