Observer pattern uma abordagem prática

Lidando com eventos sem transformar sua aplicação em um inferno

Executar ações condicionais no desenvolvimento de software faz parte da rotina de qualquer desenvolvedor. É bastante comum ter ações relacionadas à mudança de estado de um objeto. Hoje eu vou abordar um pouco de como aplicar observer-patter usando o Ruby como linguagem de programação.

Segundo o Wikipedia Observe o padrão Observer define uma dependência um-para-muitos entre objetos para que, quando um objeto mude de estado, todos os seus dependentes sejam notificados e atualizados automaticamente.

pub-and-sub

Para exemplificar criei uma implementação baseada no sistema de notificação do youtube, onde um usuário que se inscreveu em um canal(e ativou o sininho 😅) é notificado toda vez que que um novo vídeo novo publicado.

class YoutubeNotifiable
 attr_reader :episodes, :subscribers

 def initialize
   @episodes = []
   @subscribers = []
 end

 def add_subscriber(new_subscriber)
   subscribers << new_subscriber
 end

 def remove_subscriber(subscriber)
   subscribers.delete(subscriber)
 end

 def add_episode(new_episode)
   episodes << new_episode

   subscribers.each do |subscriber|
     puts "Heey #{subscriber} has new video on the channel! 👉🏻 #{new_episode}"
   end
 end
end

channel = YoutubeNotifiable.new

channel.add_subscriber('Aristóteles')
channel.add_subscriber('Joe')

channel.add_episode('how to use observer pattern!')

# Heey Aristóteles has new video on the channel! 👉🏻 how to use observer pattern!
# Heey Joe has new video on the channel! 👉🏻 how to use observer pattern!

Eu particularmente, acredito que o cenário de subscriber do Youtube é uma ideia muito próxima do conceito de pub/sub. Dentro de aplicações Ruby on Rails é bem comum vermos esses conceitos aplicados a eventos ligados a camada de persistência de dados. Vamos ver como é isso na prática.

Rails Observer 🔮

Se não me engano, até a versão 3, o Rails já vinha com uma biblioteca que possibilitava o uso de observers ligados aos eventos do Active Record a rails-observers. Já na versão 4 em diante do Framework ela foi removida do core, com tudo, para usá-la basta adicioná-la como dependência da sua aplicação. Comumente, você tem um diretório chamado de observers dentro do diretório app algo como:

$ tree

my_app
|-app
| |-models
| |-viwes
| |-observers
| ...

E suas classes devem herdar de ActiveRecord::Observer. Isso é o bastante para que você consiga isolar a lógica que está fora da camada de domínio.

class CommentObserver < ActiveRecord::Observer
 def after_save(comment)
   Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
 end
end

Neste exemplo, uma notificação deve ser enviada toda vez que um novo comentário for salvo em banco, bem prático.

A biblioteca padrão do Ruby já fornece uma API para trabalhar com manipulação de eventos de forma nativa, porém, se você estiver usando o Rails eu desaconselho o uso dessa biblioteca pois Active Model/Active Record sobrescreve o método change, o que acaba comprometendo o comportamento por conta da reescrita.

Trade-off

É evidente a simplicidade da implementação que a rails-observer nos entrega. Não tivemos que fazer nenhum tipo de configuração ou mesmo fazer grandes alterações e tudo já estava pronto para funcionar. Com tudo, essa abordagem traz consigo algumas coisas que, pessoalmente, não considero muito interessantes hoje em dia.

O primeiro e principal ponto é o fato de que o uso da gem é completamente acoplada ao Active Record. Isso limita as possibilidades dentro do que diz respeito à extensão dos casos de uso para o observer pattern e naturalmente se você precisa ir além, ou melhor, certamente irá precisar criar novas abstrações para casos de uso mais genéricos. Como implementações para casos de uso sem Active Record, por exemplo.

Outro ponto a se perceber é: O que NÃO acontece!

Isso mesmo! Não fica muito claro o ponto em que esses eventos irão acontecer. Isso te força, de certa forma, a ter que saber da existência desses eventos ou vai te fazer perder um bom tempo tendo que debugar literalmente todos os logs da execução contextual.

{}.call "<-" Hell

Quem lida, ou principalmente já fez besteira, com uso excessivo de calbacks, entende que eles abrem muitas brechas para transformar sua aplicação em um verdadeiro inferno daí o termo "{}.call '<-' Hell" ou "callback hell" (Eu sei, é uma piada horrível 😅).

Nesse ponto eu prefiro ser mais explícito, e é aí que entra uma sacada que tem me ajudado bastante como desenvolver .

"Entendimento > Padrão A, B"

Se tem uma coisa que me deixa "muito com o pé atrás" dentro do ecossistema Ruby on Rails hoje em dia, são as coisas que acontecem de forma um pouco "mágica" digamos assim. Não me entenda mal, eu amo esse ecossistema, foi onde dei meus primeiros passos e aprendi a caminhar.

Sempre me pego escrevendo um pouco mais de código ou quebrando um pouco mais a cabeça pra tentar manter a implementação mais sucinta possível. Partindo desse princípio, eu prefiro manter as chamadas aos meus observers de forma explícitas. Vou tentar explicar melhor usando um exemplo.

u-observers

Ultimamente eu tenho adotado de forma maciça aqui dentro do JUS o uso de algumas gems(libs) feitas pelo Rodrigo Serradura, uma delas é u-observe. A idéia por trás das libs ou falando especificamente da u-observe, é que ela casa muito bem com a proposta que mencionei anteriormente, isso, sem contar com o fato de ter uma API extremamente enxuta e objetiva.

Então pra demonstrar o ponto mencionado anteriormente irei usar com um exemplo.

Show Me The Code

Imagine, que dentro de ecommerce, por algum motivo, que você irá precisar notificar a equipe de logística caso algum pedido seja cancelado, para que daí então eles possam tomar medidas internas inerentes a essa situação. O mesmo poderia ser implementado da seguinte forma:

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)
   Notifications.canceled_order("The order #(#{order.code}) has been canceled.")
 end
end

# Instanciando um objeto da class Order chamado "order"
order = Order.new

# anexa observadores
order.observers.attach(OrderEvents)
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>

order.cancel!
# A mensagem abaixo será impressa pelo observador (OrderEvents):
# The order #(X0o9yf1GsdQFvLR4) has been canceled

order.canceled?
# true

Vamos entender com mais detalhes o que acontece no exemplo acima. 🧐

A primeira coisa a se notar é que eu precisei incluir o módulo Micro::Observers na minha classe Order. Isso irá permitir que eu possa notificar os eventos ao observadores que forem anexados.

#...
class Order
 include Micro::Observers
 # ...
 # ...
end

Como explicado anteriormente, eu preciso enviar uma notificação caso um pedido seja cancelado. Indo direto ao ponto, isso irá acontecer dentro do método cancel! onde eu altero explicitamente o subject e notifico o meu observer através de um "callable" chamado notify.

def cancel!
 return self if canceled?

 @status = :canceled

 observers.subject_changed!
 # De @subject_changed=false para @subject_changed=true
 observers.notify(:canceled) and return self
 #@subscribers=[OrderEvents]
end

Para anexar um observer a um objeto order é muito simples:

order = Order.new
order.observers.attach(OrderEvents)
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>

Agora perceba que fica explícito, no método cancel!, o que irá acontecer depois da mudança no status:

def cancel!

 return self if canceled? # Retornar a instância caso o status do objeto seja igual à :canceled*

 @status = :canceled # Muda o status atual para :canceled.*

 observers.subject_changed! # Marca o sujeito como alterado.*

 observers.notify(:canceled) and return self # Notifica o observador e retorna o objeto da instância.*
end
  1. O método subject_changed! marcará automaticamente o sujeito como alterado
  2. O observador, que está "ouvindo", será notificado através do método notify

obs: O símbolo que é passado como argumento para o método notify, ":canceled", corresponde ao nome do método público do módulo OrderEvents, que será responsavel por executar as ações subsequentes. 👇🏻

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

Sem "mágia", sem letras miúdas no contrato, uma API simples e elegante. 😉

Conclusão

A ideia por trás dessa abordagem é que você possa manter a aplicabilidade flexível. No final, eu efetivamente não vou te dizer como trabalhar o observer-patern. A ideia por trás de tudo é apenas proporcionar mecanismos que promovam o uso da técnica em si. 😉

Se você entende muito ou "nem tanto assim" o que você pode resolver com ou causar com isso:

  • Possibilitar baixo acoplamento entre os objetos dependentes e o assunto.
  • Acoplamento abstrato.
  • Suporte para broadcast.
  • Dificuldade em saber o que foi mudado.

É importante entender e fazer uso consciente de eventos para evitar ficar pensando em tudo como um grande IF. 😄

Comentários