Implementação simples e poderosa do padrão observer.
Esta gem implementa o padrão observer[1][2] (também conhecido como publicar/assinar). Ela fornece um mecanismo simples para um objeto informar um conjunto de objetos de terceiros interessados quando seu estado muda.
A biblioteca padrão do Ruby tem uma abstração que permite usar esse padrão, mas seu design pode entrar em conflito com outras bibliotecas convencionais, como ActiveModel
/ActiveRecord
, que também tem o método changed
. Nesse caso, o comportamento ficaria comprometido por conta dessa sobrescrita de métodos.
Por causa desse problema, decidi criar uma gem que encapsula o padrão sem alterar tanto a implementação do objeto. O Micro::Observers
inclui apenas um método de instância na classe de destino (sua instância será o sujeito/objeto observado).
- Instalação
- Compatibilidade
- Uso
- Compartilhando um contexto com seus observadores
- Compartilhando dados ao notificar os observadores
- O que é
Micro::Observers::Event
? - Usando um callable como um observador
- Chamando os observadores
- Notificar observadores sem marcá-los como alterados
- Definindo observers que executam apenas uma vez
- Definindo observers com blocos
- Desanexando observers
- Integrações ActiveRecord e ActiveModel
- Desenvolvimento
- Contribuindo
- License
- Código de conduta
- Uso
Adicione esta linha ao Gemfile da sua aplicação e execute bundle install
:
gem 'u-observers'
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 |
Nota: O ActiveRecord não é uma dependência, mas você pode adicionar um módulo para habilitar alguns métodos estáticos que foram projetados para serem usados com seus callbacks.
Qualquer classe com o Micro::Observers
incluído pode notificar eventos para observadores anexados.
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) # anexando vários observadores. Exemplo: observers.attach(A, B, C)
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>
order.canceled?
# false
order.cancel!
# A mensagem abaixo será impressa pelo observador (OrderEvents):
# The order #(X0o9yf1GsdQFvLR4) has been canceled
order.canceled?
# true
order.observers.detach(OrderEvents) # desanexando vários observadores. Exemplo: 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) # nada acontecerá, pois não há observadores vinculados (observers.attach)
Destaques do exemplo anterior:
Para evitar um comportamento indesejado, você precisa marcar o "subject" (sujeito) como alterado antes de notificar seus observadores sobre algum evento.
Você pode fazer isso ao usar o método #subject_changed!
. Ele marcará automaticamente o sujeito como alterado.
Mas se você precisar aplicar alguma condicional para marcar uma mudança, você pode usar o método #subject_changed
. Exemplo: observers.subject_changed(name != new_name)
O método #notify
sempre requer um evento para fazer uma transmissão. Portanto, se você tentar usá-lo sem nenhum evento, você obterá uma exceção.
order.observers.notify
# ArgumentError (no events (expected at least 1))
Para compartilhar um valor de contexto (qualquer tipo de objeto Ruby) com um ou mais observadores, você precisará usar a palavra-chave :context
como o último argumento do método #attach
. Este recurso oferece a você uma oportunidade única de compartilhar um valor no momento de anexar um observer.
Quando o método do observer receber dois argumentos, o primeiro será o sujeito e o segundo uma instância Micro::Observers::Event
que terá o valor do contexto.
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 é um alias para event.context
end
end
order = Order.new
order.observers.attach(OrderEvents, context: { from: 'example #2' }) # anexando vários observadores. Exemplo: observers.attach(A, B, context: {hello:: world})
order.cancel!
# A mensagem abaixo será impressa pelo observador (OrderEvents):
# The order #(70196221441820) has been canceled. (from: example #2)
Como mencionado anteriormente, o event context
é um valor armazenado quando você anexa seu observer. Mas, às vezes, será útil enviar alguns dados adicionais ao transmitir um evento aos seus observers. O event data
dá a você esta oportunidade única de compartilhar algum valor no momento da notificação.
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)
# A mensagem abaixo será impressa pelo observador (OrderHandler):
# The order #(70196221441820) received the number 1 from example #3.
O Micro::Observers::Event
é o payload do evento. Veja abaixo todas as suas propriedades:
#name
será o evento transmitido.#subject
será o sujeito observado.#context
serão os dados de contexto que foram definidos no momento em que você anexa o observer.#data
será o valor compartilhado na notificação dos observadores.#ctx
é um apelido para o método#context
.#subj
é um alias para o método#subject
.
O método observers.on()
permite que você anexe um callable (objeto que responda ao método call
) como um observador.
Normalmente, um callable tem uma responsabilidade bem definida (faz apenas uma coisa), por isso, tende a ser mais amigável com o SRP (princípio de responsabilidade única) do que um observador convencional (que poderia ter N métodos para responder a diferentes tipos de notificação).
Este método recebe as opções abaixo:
:event
o nome do evento esperado.:call
o próprio callable.:with
(opcional) pode definir o valor que será usado como argumento do objeto callable. Portanto, se for umProc
, uma instância deMicro::Observers::Event
será recebida como o argumentoProc
e sua saída será o argumento que pode ser chamado. Mas se essa opção não for definida, a instânciaMicro::Observers::Event
será o argumento do callable.:context
serão os dados de contexto que foram definidos no momento em que você anexa o 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('Aristóteles')
person.observers.on(
event: :name_has_been_changed,
call: PrintPersonName,
with: -> event { {person: event.subject, number: event.context} },
context: rand
)
person.name = 'Coutinho'
# A mensagem abaixo será impressa pelo observador (PrintPersonName):
# Person name: Coutinho, number: 0.5018509191706862
Você pode usar um callable (uma classe, módulo ou objeto que responda ao método call
) para ser seu observer. Para fazer isso, você só precisa usar o método #call
em vez de #notify
.
class Order
include Micro::Observers
def cancel!
observers.subject_changed!
observers.call # na prática, este é um alias para observers.notify(:call)
self
end
end
OrderCancellation = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
order = Order.new
order.observers.attach(OrderCancellation)
order.cancel!
# A mensagem abaixo será impressa pelo observador (OrderCancellation):
# The order #(70196221441820) has been canceled.
Nota: O
observers.call
pode receber um ou mais eventos, mas no caso de receber eventos/argumentos, o evento padrão (call
) não será transmitido.
Este recurso deve ser usado com cuidado!
Se você usar os métodos #notify!
ou #call!
você não precisará marcar observers com #subject_changed
.
Existem duas formas de anexar um observer e definir que ele executará apenas uma vez.
A primeira forma de fazer isso é passando a opção perform_once: true
para o método observers.attach()
. Exemplo:
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.
A segunda forma de conseguir isso é usando o método observers.once()
que tem a mesma API do observers.on()
. Mas a diferença é que o método #once()
removerá o observer após a sua execução.
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.
Os métodos #on()
e #once()
podem receber um evento (a symbol
) e um bloco para definir observers.
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
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
Ruby permite que você substitua qualquer bloco com um lambda
/proc
. Portanto, será possível usar este tipo de recurso para definir seus observers. Exemplo:
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.
Como mostrado no primeiro exemplo, você pode usar o observers.detach()
para remove observers.
Mas, existe uma alternativa a esse método que permite remover objetos observers ou remover callables pelo nome de seus eventos. O método para fazer isso é: 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
Para fazer uso deste recurso, você precisa de um módulo adicional.
Exemplo de Gemfile:
gem 'u-observers', require: 'u-observers/for/active_record'
Este recurso irá expor módulos que podem ser usados para adicionar macros (métodos estáticos) que foram projetados para funcionar com os callbacks do ActiveModel
/ActiveRecord
. Exemplo:
O notify_observers_on
permite que você defina um ou mais callbacks do ActiveModel
/ActiveRecord
, que serão usados para notificar seus observers.
class Post < ActiveRecord::Base
include ::Micro::Observers::For::ActiveRecord
notify_observers_on(:after_commit) # usando vários callbacks. Exemplo: notificar_observadores_on(:before_save, :after_commit)
# O método acima faz o mesmo que o exemplo comentado abaixo.
#
# 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
# A mensagem abaixo será impressa pelos observadores (TitlePrinter, TitlePrinterWithContext):
# Title: Hello world
# Title: Hello world (de: exemplo # 6)
O notify_observers
permite definir um ou mais eventos, que serão utilizados para notificar após a execução de algum callback do ActiveModel
/ActiveRecord
.
class Post < ActiveRecord::Base
include ::Micro::Observers::For::ActiveRecord
after_commit(¬ify_observers(:transaction_completed))
# O método acima faz o mesmo que o exemplo comentado abaixo.
#
# 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
# A mensagem abaixo será impressa pelos observadores (TitlePrinter, TitlePrinterWithContext):
# Title: Olá mundo
# Title: Olá mundo (from: example # 5)
Observação: você pode usar
include ::Micro::Observers::For::ActiveModel
se sua classe apenas fizer uso doActiveModel
e todos os exemplos anteriores funcionarão.
Depois de verificar o repositório, execute bin/setup
para instalar as dependências. Em seguida, execute rake test
para executar os testes. Você também pode executar bin/console
um prompt interativo que permitirá que você experimente.
Para instalar esta gem em sua máquina local, execute bundle exec rake install
. Para lançar uma nova versão, atualize o número da versão em version.rb
e execute bundle exec rake release
, que criará uma tag git para a versão, envie os commits ao git e envie e envie o arquivo .gem
para rubygems.org.
Reportar bugs e solicitações de pull-requests são bem-vindos no GitHub em https://github.com/serradura/u-observers. Este projeto pretende ser um espaço seguro e acolhedor para colaboração, e espera-se que os colaboradores sigam o código de conduta.
A gem está disponível como código aberto sob os termos da Licença MIT.
Espera-se que todos que interagem nas bases de código do projeto Micro::Observers
, rastreadores de problemas, salas de bate-papo e listas de discussão sigam o código de conduta.