Um estudo sobre o pattern Observer de Ruby

Escrito em 31 de Março de 2022 por Heitor Adão Jr.

Atualizado em 24 de Agosto de 2023

Se você é um desenvolvedor Ruby iniciante ou experiente, é importante estar familiarizado com os diferentes Design Patterns desta e outras linguagens de programação.

Este artigo revisita alguns conceitos sobre design patterns — especificamente, o pattern Observer — e então apresenta um exemplo feito com a biblioteca padrão de Ruby.

O livro Design Patterns: Elements of Reusable Object-Oriented Software, da Gangue dos Quatro (Gang of Four - GoF) descreve 23 padrões de projeto. Nesta coleção, está incluso o pattern Observer, também conhecido como publish/subscribe.

 

Se você é desenvolvedor, já deve conhecer o livro — ou pelo menos já ouviu falar e sabe do que se trata.

Mesmo assim, é bom relembrar e vale a pena repetir que cada linguagem traz construções idiomáticas que facilitam ou dificultam a utilização de algum pattern.

A natureza estruturada (não orientada a objetos) da linguagem C, torna um pouco mais trabalhoso usar esses patterns, enquanto na linguagem Ruby temos construções, como os blocks, que tornam o pattern Visitor um cidadão de primeira classe, por exemplo.

Neste ensaio vamos estudar o pattern Observer — como aplicar em Ruby usando a biblioteca padrão que já vem com o interpretador MRI.

 

O que é o padrão Observer?

Imagine que você tem um objeto e este possui um estado interno. Imagine também que, quando esse estado mudar, outros objetos devem ser notificados e reagir de alguma forma a esta mudança.

Seria muito conveniente estabelecer essas relações sem criar muito acoplamento. O ideal é ter esses acontecimentos de forma automática: basta declarar que existe um relacionamento e tudo acontece num instante.

Existem muitas implementações diferentes desse pattern, mas a biblioteca padrão do Ruby tem um arquivo ‘observer.rb’ que faz tudo que precisamos. É essa implementação que estudaremos neste artigo.

Essa interação tem dois tipos de participantes. Um objeto dotado de algum atributo interessante de ser observado e um objeto observador, que reage à mudança de estado do objeto observado.

 

Mãos na massa

Primeiro, vamos criar duas classes: uma chamada Subject e outra chamada Observer.

Para que isso aconteça, é preciso incluir a biblioteca 'observer' no arquivo onde é definida a classe do objeto observado, como no snippet a seguir:

 

In [1]:
require 'observer'
Out[1]:
true

 

Vamos criar uma classe chamada Subject e, dentro da definição da classe do objeto observado, vamos incluir um módulo mixin e definir métodos acessores de uma variável state, como no exemplo a seguir:

 

In [2]:
class Subject
  include Observable
 
  attr_reader :state
 
  def state=(new_state)
    if state != new_state
      changed # mark this object as dirty 😏
      state = new_state
      notify_observers(new_state)
    end
  end
end
Out[2]:
:state=



Ao incluir o mixin Observable, a classe Subject recebe a definição de alguns métodos novos. Os três métodos mais importantes são:

 

  • add_observer
  • changed
  • notify_observers

 

Os dois últimos são mais internos; o add_observer pode ser chamado externamente. Faz sentido que o add_observer seja apenas chamado por outros objetos, pois desta forma o objeto observado sequer precisa ter conhecimento de quais são os objetos que vão reagir à mudança de estado.

Precisamos ter uma classe Observer. O objeto observador precisa implementar um método que será chamado sempre que ocorrer uma mudança de estado no objeto observado. Embora ele possa ter qualquer nome, a convenção é que esse método se chame update.

 


In [3]:
class Observer
  def update(state)
    puts "Eba! O estado do objeto mudou para #{state}!"
  end
end
Out[3]:
:update

 

Agora vamos instanciar essas classes e fazê-las conversarem entre si:

In [4]:
some_subject = Subject.new
some_observer = Observer.new

some_subject.add_observer(some_observer)

some_subject.state = 1 # o método update de some_observer será chamado automagicamente!
some_subject.state = 2 # aqui também!
Eba! O estado do objeto mudou para 1!
Eba! O estado do objeto mudou para 2!
Out[4]:
2



Entendendo o processo

A utilização do padrão Observer traz algumas consequências. Segundo os autores, ao utilizar este pattern podemos variar os objetos observado e observador de forma independente.

O relacionamento entre estes objetos é um acoplamento fraco: o objeto observado possui internamente uma lista dos observadores, mas não tem os detalhes da implementação deles. Tampouco precisa saber o que eles fazem com a informação notificada.

É possível ter muitos observers para um mesmo subject, cada um reagindo de um jeito diferente à mudança de estado.

No livro, o autor Erich Gamma alerta que, se não tomarmos cuidado e criarmos uma cadeia de objetos que se auto-notificam sobre suas mudanças, eles podem consumir recursos valiosos e até mesmo ter um loop infinito de auto-atualização. Portanto, tome cuidado para que isso não ocorra na sua implementação.

Há muitas outras coisas para falar sobre esse pattern, principalmente sobre essa implementação da biblioteca de Ruby. Ela não é a única implementação. Tem uns probleminhas, mas nada que prejudique a usabilidade ou a implementação. Ela já está na distribuição e é gratuita.

Posts relacionados