All Projects → serradura → u-observers

serradura / u-observers

Licence: MIT license
Simple and powerful implementation of the observer pattern.

Programming Languages

ruby
36898 projects - #4 most used programming language
shell
77523 projects

Projects that are alternatives of or similar to u-observers

Elasticsearch Rails
Elasticsearch integrations for ActiveModel/Record and Ruby on Rails
Stars: ✭ 2,896 (+9241.94%)
Mutual labels:  activerecord, activemodel
active model serializers validator
🃏 An extension to ActiveModel::Serializer that validates serializers output against a JSON schema
Stars: ✭ 18 (-41.94%)
Mutual labels:  activerecord, activemodel
activeentity
Active Record without Database
Stars: ✭ 46 (+48.39%)
Mutual labels:  activerecord, activemodel
duck record
Used for creating virtual models like ActiveType or ModelAttribute does.
Stars: ✭ 23 (-25.81%)
Mutual labels:  activerecord, activemodel
active record distinct on
Support for `DISTINCT ON` statements when querying with ActiveRecord
Stars: ✭ 23 (-25.81%)
Mutual labels:  activerecord
talek
a Private Publish Subscribe System
Stars: ✭ 39 (+25.81%)
Mutual labels:  pubsub
activemodel-datastore
Ruby on Rails with Active Model and Google Cloud Datastore. Extracted from Agrimatics Aero.
Stars: ✭ 47 (+51.61%)
Mutual labels:  activemodel
iris3
An upgraded and improved version of the Iris automatic GCP-labeling project
Stars: ✭ 38 (+22.58%)
Mutual labels:  pubsub
ar-role
ActiveRecord behavior, which provides relation roles (table inheritance)
Stars: ✭ 34 (+9.68%)
Mutual labels:  activerecord
hanbo-db
hanboDB is a high available,low latency memory database system
Stars: ✭ 29 (-6.45%)
Mutual labels:  pubsub
simple redis
Simple and resilient redis client for rust.
Stars: ✭ 21 (-32.26%)
Mutual labels:  pubsub
MusicPlayer
Android music player example.
Stars: ✭ 20 (-35.48%)
Mutual labels:  observer-pattern
terraform-splunk-log-export
Deploy Google Cloud log export to Splunk using Terraform
Stars: ✭ 26 (-16.13%)
Mutual labels:  pubsub
metamorphosis
Easy and flexible Kafka Library for Laravel and PHP 7
Stars: ✭ 39 (+25.81%)
Mutual labels:  pubsub
activerecord-shard for
Database Sharding Library for ActiveRecord
Stars: ✭ 16 (-48.39%)
Mutual labels:  activerecord
kane
Google Pub/Sub client for Elixir
Stars: ✭ 92 (+196.77%)
Mutual labels:  pubsub
WebApiWithBackgroundWorker
Small demo showing how to implement Pub/Sub with a BackgroundWorker in .NET Core
Stars: ✭ 55 (+77.42%)
Mutual labels:  pubsub
Pubbie
A high performance pubsub client/server implementation for .NET Core
Stars: ✭ 122 (+293.55%)
Mutual labels:  pubsub
SwiftObserver
Elegant Reactive Primitives for Clean Swift Architecture #NoRx
Stars: ✭ 14 (-54.84%)
Mutual labels:  observer-pattern
souls
SOULs 🔥 Build Serverless Apps faster like Rails. Powered by Ruby GraphQL, RBS/Steep, Active Record, RSpec, RuboCop, and Google Cloud.
Stars: ✭ 327 (+954.84%)
Mutual labels:  pubsub

👀 μ-observers

Simple and powerful implementation of the observer pattern.


Ruby Gem Build Status Maintainability Test Coverage

This gem implements the observer pattern [1][2] (also known as publish/subscribe). It provides a simple mechanism for one object to inform a set of interested third-party objects when its state changes.

Ruby's standard library has an abstraction that enables you to use this pattern. But its design can conflict with other mainstream libraries, like the ActiveModel/ActiveRecord, which also has the changed method. In this case, the behavior of the Stdlib will be compromised.

Because of this issue, I decided to create a gem that encapsulates the pattern without changing the object's implementation so much. The Micro::Observers includes just one instance method in the target class (its instance will be the observed subject/object).

Note: Você entende português? 🇧🇷 🇵🇹 Verifique o README traduzido em pt-BR.

Table of contents

Installation

Add this line to your application's Gemfile and bundle install:

gem 'u-observers'

Compatibility

u-observers branch ruby activerecord
unreleased main >= 2.2.0 >= 3.2, < 6.1
2.3.0 v2.x >= 2.2.0 >= 3.2, < 6.1
1.0.0 v1.x >= 2.2.0 >= 3.2, < 6.1

Note: The ActiveRecord isn't a dependency, but you could add a module to enable some static methods that were designed to be used with its callbacks.

⬆️   Back to Top

Usage

Any class with Micro::Observers module included can notify events to attached observers.

require 'securerandom'

class Order
  include Micro::Observers

  attr_reader :code

  def initialize
    @code, @status = SecureRandom.alphanumeric, :draft
  end

  def canceled?
    @status == :canceled
  end

  def cancel!
    return self if canceled?

    @status = :canceled

    observers.subject_changed!
    observers.notify(:canceled) and return self
  end
end

module OrderEvents
  def self.canceled(order)
    puts "The order #(#{order.code}) has been canceled."
  end
end

order = Order.new
#<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>

order.observers.attach(OrderEvents)  # attaching multiple observers. e.g. observers.attach(A, B, C)
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>

order.canceled?
# false

order.cancel!
# The message below will be printed by the observer (OrderEvents):
# The order #(X0o9yf1GsdQFvLR4) has been canceled

order.canceled?
# true

order.observers.detach(OrderEvents)  # detaching multiple observers. e.g. observers.detach(A, B, C)
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[]>

order.canceled?
# true

order.observers.subject_changed!
order.observers.notify(:canceled) # nothing will happen, because there are no observers attached.

Highlights of the previous example:

To avoid an undesired behavior, you need to mark the subject as changed before notifying your observers about some event.

You can do this when using the #subject_changed! method. It will automatically mark the subject as changed.

But if you need to apply some conditional to mark a change, you can use the #subject_changed method. e.g. observers.subject_changed(name != new_name)

The #notify method always requires an event to make a broadcast. So, if you try to use it without one or more events (symbol values) you will get an exception.

order.observers.notify
# ArgumentError (no events (expected at least 1))

⬆️   Back to Top

Sharing a context with your observers

To share a context value (any kind of Ruby object) with one or more observers, you will need to use the :context keyword as the last argument of the #attach method. This feature gives you a unique opportunity to share a value in the attaching moment.

When the observer method receives two arguments, the first one will be the subject, and the second one an instance of Micro::Observers::Event that will have the given context value.

class Order
  include Micro::Observers

  def cancel!
    observers.subject_changed!
    observers.notify(:canceled)
    self
  end
end

module OrderEvents
  def self.canceled(order, event)
    puts "The order #(#{order.object_id}) has been canceled. (from: #{event.context[:from]})" # event.ctx is an alias for event.context
  end
end

order = Order.new
order.observers.attach(OrderEvents, context: { from: 'example #2' }) # attaching multiple observers. e.g. observers.attach(A, B, context: {hello: :world})
order.cancel!
# The message below will be printed by the observer (OrderEvents):
# The order #(70196221441820) has been canceled. (from: example #2)

⬆️   Back to Top

Sharing data when notifying the observers

As previously mentioned, the event context is a value that is stored when you attach your observer. But sometimes, it will be useful to send some additional data when broadcasting an event to the observers. The event data gives you this unique opportunity to share some value at the the notification moment.

class Order
  include Micro::Observers
end

module OrderHandler
  def self.changed(order, event)
    puts "The order #(#{order.object_id}) received the number #{event.data} from #{event.ctx[:from]}."
  end
end

order = Order.new
order.observers.attach(OrderHandler, context: { from: 'example #3' })
order.observers.subject_changed!
order.observers.notify(:changed, data: 1)
# The message below will be printed by the observer (OrderHandler):
# The order #(70196221441820) received the number 1 from example #3.

⬆️   Back to Top

What is a Micro::Observers::Event?

The Micro::Observers::Event is the event payload. Follow below all of its properties:

⬆️   Back to Top

Using a callable as an observer

The observers.on() method enables you to attach a callable as an observer.

Usually, a callable has a well-defined responsibility (do only one thing), because of this, it tends to be more SRP (Single-responsibility principle) friendly than a conventional observer (that could have N methods to respond to different kinds of notification).

This method receives the below options:

  1. :event the expected event name.
  2. :call the callable object itself.
  3. :with (optional) it can define the value which will be used as the callable object's argument. So, if it is a Proc, a Micro::Observers::Event instance will be received as the Proc argument, and its output will be the callable argument. But if this option wasn't defined, the Micro::Observers::Event instance will be the callable argument.
  4. :context will be the context data that was defined in the moment that you attach the observer.
class Person
  include Micro::Observers

  attr_reader :name

  def initialize(name)
    @name = name
  end

  def name=(new_name)
    return unless observers.subject_changed(new_name != @name)

    @name = new_name

    observers.notify(:name_has_been_changed)
  end
end

PrintPersonName = -> (data) do
  puts("Person name: #{data.fetch(:person).name}, number: #{data.fetch(:number)}")
end

person = Person.new('Rodrigo')

person.observers.on(
  event: :name_has_been_changed,
  call: PrintPersonName,
  with: -> event { {person: event.subject, number: event.context} },
  context: rand
)

person.name = 'Serradura'
# The message below will be printed by the observer (PrintPersonName):
# Person name: Serradura, number: 0.5018509191706862

⬆️   Back to Top

Calling the observers

You can use a callable (a class, module, or object that responds to the call method) to be your observers. To do this, you only need to make use of the method #call instead of #notify.

class Order
  include Micro::Observers

  def cancel!
    observers.subject_changed!
    observers.call # in practice, this is a shortcut to observers.notify(:call)
    self
  end
end

NotifyAfterCancel = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }

order = Order.new
order.observers.attach(NotifyAfterCancel)
order.cancel!
# The message below will be printed by the observer (NotifyAfterCancel):
# The order #(70196221441820) has been canceled.

Note: The observers.call can receive one or more events, but in this case, the default event (call) won't be transmitted.

⬆️   Back to Top

Notifying observers without marking them as changed

This feature needs to be used with caution!

If you use the methods #notify! or #call! you won't need to mark observers with #subject_changed.

⬆️   Back to Top

Defining observers that execute only once

There are two ways to attach an observer and define it to be performed only once.

The first way to do this is passing the perform_once: true option to the observers.attach() method. e.g.

observers.attach(*args, perform_once: true)

class Order
  include Micro::Observers

  def cancel!
    observers.notify!(:canceled)
  end
end

module OrderNotifications
  def self.canceled(order)
    puts "The order #(#{order.object_id}) has been canceled."
  end
end

order = Order.new
order.observers.attach(OrderNotifications, perform_once: true) # you can also pass an array of observers with this option

order.observers.some? # true
order.cancel!         # The order #(70291642071660) has been canceled.

order.observers.some? # false
order.cancel!         # Nothing will happen because there aren't observers.

observers.once(event:, call:, ...)

The second way to achieve this is using observers.once() that has the same API of observers.on(). But the difference of the #once() method is that it will remove the observer after its execution.

class Order
  include Micro::Observers

  def cancel!
    observers.notify!(:canceled)
  end
end

module NotifyAfterCancel
  def self.call(event)
    puts "The order #(#{event.subject.object_id}) has been canceled."
  end
end

order = Order.new
order.observers.once(event: :canceled, call: NotifyAfterCancel)

order.observers.some? # true
order.cancel!         # The order #(70301497466060) has been canceled.

order.observers.some? # false
order.cancel!         # Nothing will happen because there aren't observers.

⬆️   Back to Top

Defining observers using blocks

The methods #on() and #once() can receive an event (symbol) and a block to define observers.

observers.on()

class Order
  include Micro::Observers

  def cancel!
    observers.notify!(:canceled)
  end
end

order = Order.new
order.observers.on(:canceled) do |event|
  puts "The order #(#{event.subject.object_id}) has been canceled."
end

order.observers.some? # true

order.cancel!         # The order #(70301497466060) has been canceled.

order.observers.some? # true

observers.once()

class Order
  include Micro::Observers

  def cancel!
    observers.notify!(:canceled)
  end
end

order = Order.new
order.observers.once(:canceled) do |event|
  puts "The order #(#{event.subject.object_id}) has been canceled."
end

order.observers.some? # true

order.cancel!         # The order #(70301497466060) has been canceled.

order.observers.some? # false

Replacing a block by a lambda/proc

Ruby allows you to replace any block with a lambda/proc. So, it will be possible to use this kind of feature to define your observers. e.g.

class Order
  include Micro::Observers

  def cancel!
    observers.notify!(:canceled)
  end
end

NotifyAfterCancel = -> event { puts "The order #(#{event.subject.object_id}) has been canceled." }

order = Order.new
order.observers.once(:canceled, &NotifyAfterCancel)

order.observers.some? # true
order.cancel!         # The order #(70301497466060) has been canceled.

order.observers.some? # false
order.cancel!         # Nothing will happen because there aren't observers.

⬆️   Back to Top

Detaching observers

As shown in the first example, you can use the observers.detach() to remove observers.

But, there is an alternative method to remove observer objects or remove callables by their event names. The method to do this is: observers.off().

class Order
  include Micro::Observers
end

NotifyAfterCancel = -> {}

module OrderNotifications
  def self.canceled(_order)
  end
end

order = Order.new
order.observers.on(:canceled) { |_event| }
order.observers.on(event: :canceled, call: NotifyAfterCancel)
order.observers.attach(OrderNotifications)

order.observers.some? # true
order.observers.count # 3

order.observers.off(:canceled) # removing the callable (NotifyAfterCancel).
order.observers.some? # true
order.observers.count # 1

order.observers.off(OrderNotifications)
order.observers.some? # false
order.observers.count # 0

⬆️   Back to Top

ActiveRecord and ActiveModel integrations

To make use of this feature you need to require an additional module.

Gemfile example:

gem 'u-observers', require: 'u-observers/for/active_record'

This feature will expose modules that could be used to add macros (static methods) that were designed to work with ActiveModel/ActiveRecord callbacks. e.g:

.notify_observers_on()

The notify_observers_on allows you to define one or more ActiveModel/ActiveRecord callbacks, that will be used to notify your observers.

class Post < ActiveRecord::Base
  include ::Micro::Observers::For::ActiveRecord

  notify_observers_on(:after_commit) # using multiple callbacks. e.g. notify_observers_on(:before_save, :after_commit)

  # The method above does the same as the commented example below.
  #
  # after_commit do |record|
  #  record.subject_changed!
  #  record.notify(:after_commit)
  # end
end

module TitlePrinter
  def self.after_commit(post)
    puts "Title: #{post.title}"
  end
end

module TitlePrinterWithContext
  def self.after_commit(post, event)
    puts "Title: #{post.title} (from: #{event.context[:from]})"
  end
end

Post.transaction do
  post = Post.new(title: 'Hello world')
  post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #6' })
  post.save
end
# The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
# Title: Hello world
# Title: Hello world (from: example #6)

⬆️   Back to Top

.notify_observers()

The notify_observers allows you to define one or more events, that will be used to notify after the execution of some ActiveModel/ActiveRecord callback.

class Post < ActiveRecord::Base
  include ::Micro::Observers::For::ActiveRecord

  after_commit(&notify_observers(:transaction_completed))

  # The method above does the same as the commented example below.
  #
  # after_commit do |record|
  #  record.subject_changed!
  #  record.notify(:transaction_completed)
  # end
end

module TitlePrinterWithContext
  def self.transaction_completed(post, event)
    puts("Title: #{post.title} (from: #{event.ctx[:from]})")
  end
end

Post.transaction do
  post = Post.new(title: 'Olá mundo')

  post.observers.on(:transaction_completed) { |event| puts("Title: #{event.subject.title}") }

  post.observers.attach(TitlePrinterWithContext, context: { from: 'example #7' })

  post.save
end
# The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
# Title: Olá mundo
# Title: Olá mundo (from: example #5)

Note: You can use include ::Micro::Observers::For::ActiveModel if your class only makes use of the ActiveModel and all the previous examples will work.

⬆️   Back to Top

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake test to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/serradura/u-observers. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the Micro::Observers project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.

Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].