Skip to content

Instantly share code, notes, and snippets.

@serradura
Last active April 21, 2022 19:33
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save serradura/48c0fa03e3a4d1db302e7e0b883c47ca to your computer and use it in GitHub Desktop.
Save serradura/48c0fa03e3a4d1db302e7e0b883c47ca to your computer and use it in GitHub Desktop.
Simple observer (pub/sub) in Ruby
require 'bundler/inline'
gemfile do
source 'https://rubygems.org'
gem 'u-test'
gem 'activerecord', require: 'active_record'
gem 'sqlite3'
end
module Observers
class Manager
EMPTY_HASH = {}.freeze
def initialize(list = nil)
@list = (list.is_a?(Array) ? list : []).flatten.compact
end
def attach(observer, options = EMPTY_HASH)
return self if !options[:allow_duplication] && @list.any? { |ob, _data| ob == observer }
@list << [observer, options[:data]]
self
end
def detach(observer)
@list.delete_if { |ob, _data| ob == observer }
self
end
def call(subject, action: :call)
@list.each do |observer, data|
next unless observer.respond_to?(action)
handler = observer.method(action)
handler.arity == 2 ? handler.call(subject, data) : handler.call(subject)
end
self
end
alias notify call
private_constant :EMPTY_HASH
end
module ClassMethods
def call_observers(with: :call)
proc do |object|
Array(with).each { |action| object.observers.call(object, action: action) }
end
end
def notify_observers(with: :call)
call_observers(with: with)
end
end
def self.included(base)
base.extend(ClassMethods)
end
def observers
@observers ||= Observers::Manager.new
end
end
# == Test ==
require 'singleton'
class Printer
include Singleton
def self.history; instance.history; end
def self.puts(value); instance.puts(value); end
attr_reader :history
def initialize
@history = []
end
def puts(value)
@history << value
Kernel.puts(value)
end
end
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:host => "localhost",
:database => ':memory:'
)
ActiveRecord::Schema.define do
create_table :posts do |t|
t.column :title, :string
end
create_table :books do |t|
t.column :title, :string
end
end
class Post < ActiveRecord::Base
include Observers
after_commit \
&call_observers(with: [:print_title, :print_title_with_data])
end
class Book < ActiveRecord::Base
include Observers
after_commit(&notify_observers(with: [:print_title, :print_title_with_data]))
end
module TitlePrinter
def self.print_title(post)
Printer.puts("Title: #{post.title}")
end
def self.print_title_with_data(post, data)
Printer.puts("Title: #{post.title}, from: #{data[:from]}")
end
end
class ObserversTest < Microtest::Test
def setup
Printer.history.clear
end
def test_observer_execution_using_call
Post.transaction do
post = Post.new(title: 'Hello world')
post.observers.attach(TitlePrinter, data: { from: 'Test 1' })
post.save
end
assert 'Title: Hello world' == Printer.history[0]
assert 'Title: Hello world, from: Test 1' == Printer.history[1]
end
def test_observer_execution_using_notify
Book.transaction do
book = Book.new(title: 'Observers')
book.observers.attach(TitlePrinter, data: { from: 'Test 2' })
book.save
end
assert 'Title: Observers' == Printer.history[0]
assert 'Title: Observers, from: Test 2' == Printer.history[1]
end
def test_observer_deletion
Book.transaction do
book = Book.new(title: 'Observers')
book.observers.attach(TitlePrinter, data: { from: 'Test 2' })
book.observers.detach(TitlePrinter)
book.save
end
assert Printer.history.empty?
end
end
Microtest.call
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment