Last active
April 21, 2022 19:33
-
-
Save serradura/48c0fa03e3a4d1db302e7e0b883c47ca to your computer and use it in GitHub Desktop.
Simple observer (pub/sub) in Ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(¬ify_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