u-observers 0.7.0 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,426 @@
1
+ <p align="center">
2
+ <h1 align="center">👀 μ-observers</h1>
3
+ <p align="center"><i>Implementação simples e poderosa do padrão observer.</i></p>
4
+ <br>
5
+ </p>
6
+
7
+ <p align="center">
8
+ <img src="https://img.shields.io/badge/ruby->%3D%202.2.0-ruby.svg?colorA=99004d&colorB=cc0066" alt="Ruby">
9
+
10
+ <a href="https://rubygems.org/gems/u-observers">
11
+ <img alt="Gem" src="https://img.shields.io/gem/v/u-observers.svg?style=flat-square">
12
+ </a>
13
+
14
+ <a href="https://travis-ci.com/serradura/u-observers">
15
+ <img alt="Build Status" src="https://travis-ci.com/serradura/u-observers.svg?branch=main">
16
+ </a>
17
+
18
+ <a href="https://codeclimate.com/github/serradura/u-observers/maintainability">
19
+ <img alt="Maintainability" src="https://api.codeclimate.com/v1/badges/e72ffa84bc95c59823f2/maintainability">
20
+ </a>
21
+
22
+ <a href="https://codeclimate.com/github/serradura/u-observers/test_coverage">
23
+ <img alt="Test Coverage" src="https://api.codeclimate.com/v1/badges/e72ffa84bc95c59823f2/test_coverage">
24
+ </a>
25
+ </p>
26
+
27
+ Esta gem implementa o padrão observer[[1]](https://en.wikipedia.org/wiki/Observer_pattern)[[2]](https://refactoring.guru/design-patterns/observer) (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.
28
+
29
+ A biblioteca padrão do Ruby [tem uma abstração](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html) que permite usar esse padrão, mas seu design pode entrar em conflito com outras bibliotecas convencionais, como [`ActiveModel`/`ActiveRecord`](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changed), que também tem o método [`changed`](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html#method-i-changed). Nesse caso, o comportamento ficaria comprometido por conta dessa sobrescrita de métodos.
30
+
31
+ 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).
32
+
33
+ # Índice <!-- omit in toc -->
34
+
35
+ - [Instalação](#instalação)
36
+ - [Compatibilidade](#compatibilidade)
37
+ - [Uso](#uso)
38
+ - [Compartilhando um contexto com seus observadores](#compartilhando-um-contexto-com-seus-observadores)
39
+ - [Compartilhando dados ao notificar os observadores](#compartilhando-dados-ao-notificar-os-observadores)
40
+ - [O que é `Micro::Observers::Event`?](#o-que-é-microobserversevent)
41
+ - [Usando um calleable como um observador](#usando-um-calleable-como-um-observador)
42
+ - [Chamando os observadores](#chamando-os-observadores)
43
+ - [Notificar observadores sem marcá-los como alterados](#notificar-observadores-sem-marcá-los-como-alterados)
44
+ - [Integrações ActiveRecord e ActiveModel](#integrações-activerecord-e-activemodel)
45
+ - [notify_observers_on()](#notify_observers_on)
46
+ - [notify_observers()](#notify_observers)
47
+ - [Desenvolvimento](#desenvolvimento)
48
+ - [Contribuindo](#contribuindo)
49
+ - [License](#license)
50
+ - [Código de conduta](#código-de-conduta)
51
+
52
+ # Instalação
53
+
54
+ Adicione esta linha ao Gemfile da sua aplicação e execute `bundle install`:
55
+
56
+ ```ruby
57
+ gem 'u-observers'
58
+ ```
59
+
60
+ # Compatibilidade
61
+
62
+ | u-observers | branch | ruby | activerecord |
63
+ | ----------- | ------- | -------- | ------------- |
64
+ | unreleased | main | >= 2.2.0 | >= 3.2, < 6.1 |
65
+ | 2.1.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
66
+ | 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
67
+
68
+ > **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](https://guides.rubyonrails.org/active_record_callbacks.html).
69
+
70
+ [⬆️ Voltar para o índice](#índice-)
71
+
72
+ ## Uso
73
+
74
+ Qualquer classe com o `Micro::Observers` incluído pode notificar eventos para observadores anexados.
75
+
76
+ ```ruby
77
+ require 'securerandom'
78
+
79
+ class Order
80
+ include Micro::Observers
81
+
82
+ attr_reader :code
83
+
84
+ def initialize
85
+ @code, @status = SecureRandom.alphanumeric, :draft
86
+ end
87
+
88
+ def canceled?
89
+ @status == :canceled
90
+ end
91
+
92
+ def cancel!
93
+ return self if canceled?
94
+
95
+ @status = :canceled
96
+
97
+ observers.subject_changed!
98
+ observers.notify(:canceled) and return self
99
+ end
100
+ end
101
+
102
+ module OrderEvents
103
+ def self.canceled(order)
104
+ puts "The order #(#{order.code}) has been canceled."
105
+ end
106
+ end
107
+
108
+ order = Order.new
109
+ #<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
110
+
111
+ order.observers.attach(OrderEvents) # anexando vários observadores. Exemplo: observers.attach(A, B, C)
112
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>
113
+
114
+ order.canceled?
115
+ # false
116
+
117
+ order.cancel!
118
+ # A mensagem abaixo será impressa pelo observador (OrderEvents):
119
+ # The order #(X0o9yf1GsdQFvLR4) has been canceled
120
+
121
+ order.canceled?
122
+ # true
123
+
124
+ order.observers.detach(OrderEvents) # desanexando vários observadores. Exemplo: observers.detach(A, B, C)
125
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[]>
126
+
127
+ order.canceled?
128
+ # true
129
+
130
+ order.observers.subject_changed!
131
+ order.observers.notify(:canceled) # nada acontecerá, pois não há observadores vinculados (observers.attach)
132
+ ```
133
+
134
+ **Destaques do exemplo anterior:**
135
+
136
+ Para evitar um comportamento indesejado, você precisa marcar o "subject" (sujeito) como alterado antes de notificar seus observadores sobre algum evento.
137
+
138
+ Você pode fazer isso ao usar o método `#subject_changed!`. Ele marcará automaticamente o sujeito como alterado.
139
+
140
+ 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)`
141
+
142
+ 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.
143
+
144
+ ```ruby
145
+ order.observers.notify
146
+ # ArgumentError (no events (expected at least 1))
147
+ ```
148
+
149
+ [⬆️ Voltar para o índice](#índice-)
150
+
151
+ ### Compartilhando um contexto com seus observadores
152
+
153
+ 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*.
154
+
155
+ 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.
156
+
157
+ ```ruby
158
+ class Order
159
+ include Micro::Observers
160
+
161
+ def cancel!
162
+ observers.subject_changed!
163
+ observers.notify(:canceled)
164
+ self
165
+ end
166
+ end
167
+
168
+ module OrderEvents
169
+ def self.canceled(order, event)
170
+ puts "The order #(#{order.object_id}) has been canceled. (from: #{event.context[:from]})" # event.ctx é um alias para event.context
171
+ end
172
+ end
173
+
174
+ order = Order.new
175
+ order.observers.attach(OrderEvents, context: { from: 'example #2' }) # anexando vários observadores. Exemplo: observers.attach(A, B, context: {hello:: world})
176
+ order.cancel!
177
+ # A mensagem abaixo será impressa pelo observador (OrderEvents):
178
+ # The order #(70196221441820) has been canceled. (from: example #2)
179
+ ```
180
+
181
+ [⬆️ Voltar para o índice](#índice-)
182
+
183
+ ### Compartilhando dados ao notificar os observadores
184
+
185
+ Como mencionado anteriormente, o [`event context`](#compartilhando-um-contexto-com-seus-observadores) é 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.
186
+
187
+ ```ruby
188
+ class Order
189
+ include Micro::Observers
190
+ end
191
+
192
+ module OrderHandler
193
+ def self.changed(order, event)
194
+ puts "The order #(#{order.object_id}) received the number #{event.data} from #{event.ctx[:from]}."
195
+ end
196
+ end
197
+
198
+ order = Order.new
199
+ order.observers.attach(OrderHandler, context: { from: 'example #3' })
200
+ order.observers.subject_changed!
201
+ order.observers.notify(:changed, data: 1)
202
+
203
+ # A mensagem abaixo será impressa pelo observador (OrderHandler):
204
+ # The order #(70196221441820) received the number 1 from example #3.
205
+ ```
206
+
207
+ [⬆️ Voltar para o índice](#índice-)
208
+
209
+ ### O que é `Micro::Observers::Event`?
210
+
211
+ O `Micro::Observers::Event` é o payload do evento. Veja abaixo todas as suas propriedades:
212
+ - `#name` será o evento transmitido.
213
+ - `#subject` será o sujeito observado.
214
+ - `#context` serão [os dados de contexto](#compartilhando-um-contexto-com-seus-observadores) que foram definidos no momento em que você anexa o *observer*.
215
+ - `#data` será [o valor compartilhado na notificação dos observadores](#compartilhando-dados-ao-notificar-os-observadores).
216
+ - `#ctx` é um apelido para o método `#context`.
217
+ - `#subj` é um *alias* para o método `#subject`.
218
+
219
+ [⬆️ Voltar para o índice](#índice-)
220
+
221
+ ### Usando um calleable como um observador
222
+
223
+ O método `observers.on()` permite que você anexe um callable (objeto que responda ao método `call`) como um observador.
224
+
225
+ Um callable tende a ter uma responsabilidade bem definida, promovendo assim o uso de [SRP (Single-responsibility principle).
226
+
227
+ 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)](https://en.wikipedia.org/wiki/Single-responsibility_principle) do que um observador convencional (que poderia ter N métodos para responder a diferentes tipos de notificação).
228
+
229
+ Este método recebe as opções abaixo:
230
+ 1. `:event` o nome do evento esperado.
231
+ 2. `:call` o próprio callable.
232
+ 3. `:with` (opcional) pode definir o valor que será usado como argumento do objeto callable. Portanto, se for um `Proc`, uma instância de `Micro::Observers::Event` será recebida como o argumento `Proc` e sua saída será o argumento que pode ser chamado. Mas se essa opção não for definida, a instância `Micro::Observers::Event` será o argumento do callable.
233
+ 4. `#context` serão os dados de contexto que foram definidos no momento em que você anexa o *observer*.
234
+
235
+ ```ruby
236
+ class Person
237
+ include Micro::Observers
238
+
239
+ attr_reader :name
240
+
241
+ def initialize(name)
242
+ @name = name
243
+ end
244
+
245
+ def name=(new_name)
246
+ return unless observers.subject_changed(new_name != @name)
247
+
248
+ @name = new_name
249
+
250
+ observers.notify(:name_has_been_changed)
251
+ end
252
+ end
253
+
254
+ PrintPersonName = -> (data) do
255
+ puts("Person name: #{data.fetch(:person).name}, number: #{data.fetch(:number)}")
256
+ end
257
+
258
+ person = Person.new('Aristóteles')
259
+
260
+ person.observers.on(
261
+ event: :name_has_been_changed,
262
+ call: PrintPersonName,
263
+ with: -> event { {person: event.subject, number: rand} }
264
+ )
265
+
266
+ person.name = 'Coutinho'
267
+
268
+ # A mensagem abaixo será impressa pelo observador (PrintPersonName):
269
+ # Person name: Coutinho, number: 0.5018509191706862
270
+ ```
271
+
272
+ [⬆️ Voltar para o índice](#índice-)
273
+
274
+ ### Chamando os observadores
275
+
276
+ 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`.
277
+
278
+ ```ruby
279
+ class Order
280
+ include Micro::Observers
281
+
282
+ def cancel!
283
+ observers.subject_changed!
284
+ observers.call # na prática, este é um alias para observers.notify(:call)
285
+ self
286
+ end
287
+ end
288
+
289
+ OrderCancellation = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
290
+
291
+ order = Order.new
292
+ order.observers.attach(OrderCancellation)
293
+ order.cancel!
294
+
295
+ # A mensagem abaixo será impressa pelo observador (OrderCancellation):
296
+ # The order #(70196221441820) has been canceled.
297
+ ```
298
+
299
+ > **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.
300
+
301
+ [⬆️ Voltar para o índice](#índice-)
302
+
303
+ ### Notificar observadores sem marcá-los como alterados
304
+
305
+ Este recurso deve ser usado com cuidado!
306
+
307
+ Se você usar os métodos `#notify!` ou `#call!` você não precisará marcar observers com `#subject_changed`.
308
+
309
+ [⬆️ Voltar para o índice](#índice-)
310
+
311
+ ### Integrações ActiveRecord e ActiveModel
312
+
313
+ Para fazer uso deste recurso, você precisa de um módulo adicional.
314
+
315
+ Exemplo de Gemfile:
316
+ ```ruby
317
+ gem 'u-observers', require: 'u-observers/for/active_record'
318
+ ```
319
+
320
+ 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:
321
+
322
+ #### notify_observers_on()
323
+
324
+ O `notify_observers_on` permite que você defina um ou mais callbacks do `ActiveModel`/`ActiveRecord`, que serão usados ​​para notificar seus *observers*.
325
+
326
+ ```ruby
327
+ class Post < ActiveRecord::Base
328
+ include ::Micro::Observers::For::ActiveRecord
329
+
330
+ notify_observers_on(:after_commit) # usando vários callbacks. Exemplo: notificar_observadores_on(:before_save, :after_commit)
331
+
332
+ # O método acima faz o mesmo que o exemplo comentado abaixo.
333
+ #
334
+ # after_commit do | record |
335
+ # record.subject_changed!
336
+ # record.notify (:after_commit)
337
+ # end
338
+ end
339
+
340
+ module TitlePrinter
341
+ def self.after_commit(post)
342
+ puts "Title: #{post.title}"
343
+ end
344
+ end
345
+
346
+ module TitlePrinterWithContext
347
+ def self.after_commit(post, event)
348
+ puts "Title: #{post.title} (from: #{event.context[:from]})"
349
+ end
350
+ end
351
+
352
+ Post.transaction do
353
+ post = Post.new(title: 'Hello world')
354
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #6' })
355
+ post.save
356
+ end
357
+
358
+ # A mensagem abaixo será impressa pelos observadores (TitlePrinter, TitlePrinterWithContext):
359
+ # Title: Hello world
360
+ # Title: Hello world (de: exemplo # 6)
361
+ ```
362
+
363
+ [⬆️ Voltar para o índice](#índice-)
364
+
365
+ #### notify_observers()
366
+
367
+ 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`.
368
+
369
+ ```ruby
370
+ class Post < ActiveRecord::Base
371
+ include ::Micro::Observers::For::ActiveRecord
372
+
373
+ after_commit(&notify_observers(:transaction_completed))
374
+
375
+ # O método acima faz o mesmo que o exemplo comentado abaixo.
376
+ #
377
+ # after_commit do | record |
378
+ # record.subject_changed!
379
+ # record.notify (:transaction_completed)
380
+ # end
381
+ end
382
+
383
+ module TitlePrinter
384
+ def self.transaction_completed(post)
385
+ puts("Title: #{post.title}")
386
+ end
387
+ end
388
+
389
+ module TitlePrinterWithContext
390
+ def self.transaction_completed(post, event)
391
+ puts("Title: #{post.title} (from: #{event.ctx[:from]})")
392
+ end
393
+ end
394
+
395
+ Post.transaction do
396
+ post = Post.new(title: 'Olá mundo')
397
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
398
+ post.save
399
+ end
400
+
401
+ # A mensagem abaixo será impressa pelos observadores (TitlePrinter, TitlePrinterWithContext):
402
+ # Title: Olá mundo
403
+ # Title: Olá mundo (from: example # 5)
404
+ ```
405
+
406
+ > **Observação**: você pode usar `include ::Micro::Observers::For::ActiveModel` se sua classe apenas fizer uso do `ActiveModel` e todos os exemplos anteriores funcionarão.
407
+
408
+ [⬆️ Voltar para o índice](#índice-)
409
+
410
+ ## Desenvolvimento
411
+
412
+ 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.
413
+
414
+ 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](https://rubygems.org).
415
+
416
+ ## Contribuindo
417
+
418
+ 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](https://github.com/serradura/u-observers/blob/master/CODE_OF_CONDUCT.md).
419
+
420
+ ## License
421
+
422
+ A gem está disponível como código aberto sob os termos da [Licença MIT](https://opensource.org/licenses/MIT).
423
+
424
+ ## Código de conduta
425
+
426
+ 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](https://github.com/serradura/u-observers/blob/master/CODE_OF_CONDUCT.md).
@@ -3,31 +3,11 @@ require 'micro/observers/version'
3
3
  module Micro
4
4
  module Observers
5
5
  require 'micro/observers/utils'
6
- require 'micro/observers/events_or_actions'
7
- require 'micro/observers/manager'
8
-
9
- module ClassMethods
10
- def notify_observers!(with:)
11
- proc { |object| with.each { |evt_or_act| object.observers.notify(evt_or_act) } }
12
- end
13
-
14
- def notify_observers(*events)
15
- notify_observers!(with: EventsOrActions[events])
16
- end
17
-
18
- def call_observers(options = Utils::EMPTY_HASH)
19
- notify_observers!(with: EventsOrActions.fetch_actions(options))
20
- end
21
- end
22
-
23
- def self.included(base)
24
- base.extend(ClassMethods)
25
- base.send(:private_class_method, :notify_observers!)
26
- end
6
+ require 'micro/observers/event'
7
+ require 'micro/observers/set'
27
8
 
28
9
  def observers
29
- @observers ||= Observers::Manager.for(self)
10
+ @__observers ||= Observers::Set.for(self)
30
11
  end
31
-
32
12
  end
33
13
  end