u-observers 0.8.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,529 @@
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 callable como um observador](#usando-um-callable-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
+ - [Definindo observers que executam apenas uma vez](#definindo-observers-que-executam-apenas-uma-vez)
45
+ - [`observers.attach(*args, perform_once: true)`](#observersattachargs-perform_once-true)
46
+ - [`observers.once(event:, call:, ...)`](#observersonceevent-call-)
47
+ - [Desanexando observers](#desanexando-observers)
48
+ - [Integrações ActiveRecord e ActiveModel](#integrações-activerecord-e-activemodel)
49
+ - [notify_observers_on()](#notify_observers_on)
50
+ - [notify_observers()](#notify_observers)
51
+ - [Desenvolvimento](#desenvolvimento)
52
+ - [Contribuindo](#contribuindo)
53
+ - [License](#license)
54
+ - [Código de conduta](#código-de-conduta)
55
+
56
+ # Instalação
57
+
58
+ Adicione esta linha ao Gemfile da sua aplicação e execute `bundle install`:
59
+
60
+ ```ruby
61
+ gem 'u-observers'
62
+ ```
63
+
64
+ # Compatibilidade
65
+
66
+ | u-observers | branch | ruby | activerecord |
67
+ | ----------- | ------- | -------- | ------------- |
68
+ | unreleased | main | >= 2.2.0 | >= 3.2, < 6.1 |
69
+ | 2.2.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
70
+ | 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
71
+
72
+ > **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).
73
+
74
+ [⬆️ Voltar para o índice](#índice-)
75
+
76
+ ## Uso
77
+
78
+ Qualquer classe com o `Micro::Observers` incluído pode notificar eventos para observadores anexados.
79
+
80
+ ```ruby
81
+ require 'securerandom'
82
+
83
+ class Order
84
+ include Micro::Observers
85
+
86
+ attr_reader :code
87
+
88
+ def initialize
89
+ @code, @status = SecureRandom.alphanumeric, :draft
90
+ end
91
+
92
+ def canceled?
93
+ @status == :canceled
94
+ end
95
+
96
+ def cancel!
97
+ return self if canceled?
98
+
99
+ @status = :canceled
100
+
101
+ observers.subject_changed!
102
+ observers.notify(:canceled) and return self
103
+ end
104
+ end
105
+
106
+ module OrderEvents
107
+ def self.canceled(order)
108
+ puts "The order #(#{order.code}) has been canceled."
109
+ end
110
+ end
111
+
112
+ order = Order.new
113
+ #<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
114
+
115
+ order.observers.attach(OrderEvents) # anexando vários observadores. Exemplo: observers.attach(A, B, C)
116
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>
117
+
118
+ order.canceled?
119
+ # false
120
+
121
+ order.cancel!
122
+ # A mensagem abaixo será impressa pelo observador (OrderEvents):
123
+ # The order #(X0o9yf1GsdQFvLR4) has been canceled
124
+
125
+ order.canceled?
126
+ # true
127
+
128
+ order.observers.detach(OrderEvents) # desanexando vários observadores. Exemplo: observers.detach(A, B, C)
129
+ # <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[]>
130
+
131
+ order.canceled?
132
+ # true
133
+
134
+ order.observers.subject_changed!
135
+ order.observers.notify(:canceled) # nada acontecerá, pois não há observadores vinculados (observers.attach)
136
+ ```
137
+
138
+ **Destaques do exemplo anterior:**
139
+
140
+ Para evitar um comportamento indesejado, você precisa marcar o "subject" (sujeito) como alterado antes de notificar seus observadores sobre algum evento.
141
+
142
+ Você pode fazer isso ao usar o método `#subject_changed!`. Ele marcará automaticamente o sujeito como alterado.
143
+
144
+ 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)`
145
+
146
+ 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.
147
+
148
+ ```ruby
149
+ order.observers.notify
150
+ # ArgumentError (no events (expected at least 1))
151
+ ```
152
+
153
+ [⬆️ Voltar para o índice](#índice-)
154
+
155
+ ### Compartilhando um contexto com seus observadores
156
+
157
+ 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*.
158
+
159
+ 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.
160
+
161
+ ```ruby
162
+ class Order
163
+ include Micro::Observers
164
+
165
+ def cancel!
166
+ observers.subject_changed!
167
+ observers.notify(:canceled)
168
+ self
169
+ end
170
+ end
171
+
172
+ module OrderEvents
173
+ def self.canceled(order, event)
174
+ puts "The order #(#{order.object_id}) has been canceled. (from: #{event.context[:from]})" # event.ctx é um alias para event.context
175
+ end
176
+ end
177
+
178
+ order = Order.new
179
+ order.observers.attach(OrderEvents, context: { from: 'example #2' }) # anexando vários observadores. Exemplo: observers.attach(A, B, context: {hello:: world})
180
+ order.cancel!
181
+ # A mensagem abaixo será impressa pelo observador (OrderEvents):
182
+ # The order #(70196221441820) has been canceled. (from: example #2)
183
+ ```
184
+
185
+ [⬆️ Voltar para o índice](#índice-)
186
+
187
+ ### Compartilhando dados ao notificar os observadores
188
+
189
+ 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.
190
+
191
+ ```ruby
192
+ class Order
193
+ include Micro::Observers
194
+ end
195
+
196
+ module OrderHandler
197
+ def self.changed(order, event)
198
+ puts "The order #(#{order.object_id}) received the number #{event.data} from #{event.ctx[:from]}."
199
+ end
200
+ end
201
+
202
+ order = Order.new
203
+ order.observers.attach(OrderHandler, context: { from: 'example #3' })
204
+ order.observers.subject_changed!
205
+ order.observers.notify(:changed, data: 1)
206
+
207
+ # A mensagem abaixo será impressa pelo observador (OrderHandler):
208
+ # The order #(70196221441820) received the number 1 from example #3.
209
+ ```
210
+
211
+ [⬆️ Voltar para o índice](#índice-)
212
+
213
+ ### O que é `Micro::Observers::Event`?
214
+
215
+ O `Micro::Observers::Event` é o payload do evento. Veja abaixo todas as suas propriedades:
216
+ - `#name` será o evento transmitido.
217
+ - `#subject` será o sujeito observado.
218
+ - `#context` serão [os dados de contexto](#compartilhando-um-contexto-com-seus-observadores) que foram definidos no momento em que você anexa o *observer*.
219
+ - `#data` será [o valor compartilhado na notificação dos observadores](#compartilhando-dados-ao-notificar-os-observadores).
220
+ - `#ctx` é um apelido para o método `#context`.
221
+ - `#subj` é um *alias* para o método `#subject`.
222
+
223
+ [⬆️ Voltar para o índice](#índice-)
224
+
225
+ ### Usando um callable como um observador
226
+
227
+ O método `observers.on()` permite que você anexe um callable (objeto que responda ao método `call`) como um observador.
228
+
229
+ 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).
230
+
231
+ Este método recebe as opções abaixo:
232
+ 1. `:event` o nome do evento esperado.
233
+ 2. `:call` o próprio callable.
234
+ 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.
235
+ 4. `:context` serão os dados de contexto que foram definidos no momento em que você anexa o *observer*.
236
+
237
+ ```ruby
238
+ class Person
239
+ include Micro::Observers
240
+
241
+ attr_reader :name
242
+
243
+ def initialize(name)
244
+ @name = name
245
+ end
246
+
247
+ def name=(new_name)
248
+ return unless observers.subject_changed(new_name != @name)
249
+
250
+ @name = new_name
251
+
252
+ observers.notify(:name_has_been_changed)
253
+ end
254
+ end
255
+
256
+ PrintPersonName = -> (data) do
257
+ puts("Person name: #{data.fetch(:person).name}, number: #{data.fetch(:number)}")
258
+ end
259
+
260
+ person = Person.new('Aristóteles')
261
+
262
+ person.observers.on(
263
+ event: :name_has_been_changed,
264
+ call: PrintPersonName,
265
+ with: -> event { {person: event.subject, number: event.context} },
266
+ context: rand
267
+ )
268
+
269
+ person.name = 'Coutinho'
270
+
271
+ # A mensagem abaixo será impressa pelo observador (PrintPersonName):
272
+ # Person name: Coutinho, number: 0.5018509191706862
273
+ ```
274
+
275
+ [⬆️ Voltar para o índice](#índice-)
276
+
277
+ ### Chamando os observadores
278
+
279
+ 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`.
280
+
281
+ ```ruby
282
+ class Order
283
+ include Micro::Observers
284
+
285
+ def cancel!
286
+ observers.subject_changed!
287
+ observers.call # na prática, este é um alias para observers.notify(:call)
288
+ self
289
+ end
290
+ end
291
+
292
+ OrderCancellation = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
293
+
294
+ order = Order.new
295
+ order.observers.attach(OrderCancellation)
296
+ order.cancel!
297
+
298
+ # A mensagem abaixo será impressa pelo observador (OrderCancellation):
299
+ # The order #(70196221441820) has been canceled.
300
+ ```
301
+
302
+ > **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.
303
+
304
+ [⬆️ Voltar para o índice](#índice-)
305
+
306
+ ### Notificar observadores sem marcá-los como alterados
307
+
308
+ Este recurso deve ser usado com cuidado!
309
+
310
+ Se você usar os métodos `#notify!` ou `#call!` você não precisará marcar observers com `#subject_changed`.
311
+
312
+ [⬆️ Voltar para o índice](#índice-)
313
+
314
+ ### Definindo observers que executam apenas uma vez
315
+
316
+ Existem duas formas de anexar um observer e definir que ele executará apenas uma vez.
317
+
318
+ A primeira forma de fazer isso é passando a opção `perform_once: true` para o método `observers.attach()`. Exemplo:
319
+
320
+ #### `observers.attach(*args, perform_once: true)`
321
+
322
+ ```ruby
323
+ class Order
324
+ include Micro::Observers
325
+
326
+ def cancel!
327
+ observers.notify!(:canceled)
328
+ end
329
+ end
330
+
331
+ module OrderNotifications
332
+ def self.canceled(order)
333
+ puts "The order #(#{order.object_id}) has been canceled."
334
+ end
335
+ end
336
+
337
+ order = Order.new
338
+ order.observers.attach(OrderNotifications, perform_once: true) # you can also pass an array of observers with this option
339
+
340
+ order.observers.some? # true
341
+ order.cancel! # The order #(70291642071660) has been canceled.
342
+
343
+ order.observers.some? # false
344
+ order.cancel! # Nothing will happen because there aren't observers.
345
+ ```
346
+
347
+ #### `observers.once(event:, call:, ...)`
348
+
349
+ A segunda forma de conseguir isso é usando o método `observers.once()` que tem a mesma API do [`observers.on()`](#usando-um-callable-como-um-observador). Mas a diferença é que o método `#once()` removerá o observer após a sua execução.
350
+
351
+ ```ruby
352
+ class Order
353
+ include Micro::Observers
354
+
355
+ def cancel!
356
+ observers.notify!(:canceled)
357
+ end
358
+ end
359
+
360
+ module NotifyAfterCancel
361
+ def self.call(event)
362
+ puts "The order #(#{event.subject.object_id}) has been canceled."
363
+ end
364
+ end
365
+
366
+ order = Order.new
367
+ order.observers.once(event: :canceled, call: NotifyAfterCancel)
368
+
369
+ order.observers.some? # true
370
+ order.cancel! # The order #(70301497466060) has been canceled.
371
+
372
+ order.observers.some? # false
373
+ order.cancel! # Nothing will happen because there aren't observers.
374
+ ```
375
+
376
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
377
+
378
+ ### Desanexando observers
379
+
380
+ Como mostrado no primeiro exemplo, você pode usar o `observers.detach()` para remove observers.
381
+
382
+ 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()`.
383
+
384
+ ```ruby
385
+ class Order
386
+ include Micro::Observers
387
+ end
388
+
389
+ NotifyAfterCancel = -> {}
390
+
391
+ module OrderNotifications
392
+ def self.canceled(_order)
393
+ end
394
+ end
395
+
396
+ order = Order.new
397
+ order.observers.on(event: :canceled, call: NotifyAfterCancel)
398
+ order.observers.attach(OrderNotifications)
399
+
400
+ order.observers.some? # true
401
+ order.observers.count # 2
402
+
403
+ order.observers.off(:canceled) # removing the callable (NotifyAfterCancel).
404
+ order.observers.some? # true
405
+ order.observers.count # 1
406
+
407
+ order.observers.off(OrderNotifications)
408
+ order.observers.some? # false
409
+ order.observers.count # 0
410
+ ```
411
+
412
+ [⬆️ &nbsp; Back to Top](#table-of-contents-)
413
+
414
+ ### Integrações ActiveRecord e ActiveModel
415
+
416
+ Para fazer uso deste recurso, você precisa de um módulo adicional.
417
+
418
+ Exemplo de Gemfile:
419
+ ```ruby
420
+ gem 'u-observers', require: 'u-observers/for/active_record'
421
+ ```
422
+
423
+ 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:
424
+
425
+ #### notify_observers_on()
426
+
427
+ O `notify_observers_on` permite que você defina um ou mais callbacks do `ActiveModel`/`ActiveRecord`, que serão usados ​​para notificar seus *observers*.
428
+
429
+ ```ruby
430
+ class Post < ActiveRecord::Base
431
+ include ::Micro::Observers::For::ActiveRecord
432
+
433
+ notify_observers_on(:after_commit) # usando vários callbacks. Exemplo: notificar_observadores_on(:before_save, :after_commit)
434
+
435
+ # O método acima faz o mesmo que o exemplo comentado abaixo.
436
+ #
437
+ # after_commit do | record |
438
+ # record.subject_changed!
439
+ # record.notify (:after_commit)
440
+ # end
441
+ end
442
+
443
+ module TitlePrinter
444
+ def self.after_commit(post)
445
+ puts "Title: #{post.title}"
446
+ end
447
+ end
448
+
449
+ module TitlePrinterWithContext
450
+ def self.after_commit(post, event)
451
+ puts "Title: #{post.title} (from: #{event.context[:from]})"
452
+ end
453
+ end
454
+
455
+ Post.transaction do
456
+ post = Post.new(title: 'Hello world')
457
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #6' })
458
+ post.save
459
+ end
460
+
461
+ # A mensagem abaixo será impressa pelos observadores (TitlePrinter, TitlePrinterWithContext):
462
+ # Title: Hello world
463
+ # Title: Hello world (de: exemplo # 6)
464
+ ```
465
+
466
+ [⬆️ Voltar para o índice](#índice-)
467
+
468
+ #### notify_observers()
469
+
470
+ 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`.
471
+
472
+ ```ruby
473
+ class Post < ActiveRecord::Base
474
+ include ::Micro::Observers::For::ActiveRecord
475
+
476
+ after_commit(&notify_observers(:transaction_completed))
477
+
478
+ # O método acima faz o mesmo que o exemplo comentado abaixo.
479
+ #
480
+ # after_commit do | record |
481
+ # record.subject_changed!
482
+ # record.notify (:transaction_completed)
483
+ # end
484
+ end
485
+
486
+ module TitlePrinter
487
+ def self.transaction_completed(post)
488
+ puts("Title: #{post.title}")
489
+ end
490
+ end
491
+
492
+ module TitlePrinterWithContext
493
+ def self.transaction_completed(post, event)
494
+ puts("Title: #{post.title} (from: #{event.ctx[:from]})")
495
+ end
496
+ end
497
+
498
+ Post.transaction do
499
+ post = Post.new(title: 'Olá mundo')
500
+ post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
501
+ post.save
502
+ end
503
+
504
+ # A mensagem abaixo será impressa pelos observadores (TitlePrinter, TitlePrinterWithContext):
505
+ # Title: Olá mundo
506
+ # Title: Olá mundo (from: example # 5)
507
+ ```
508
+
509
+ > **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.
510
+
511
+ [⬆️ Voltar para o índice](#índice-)
512
+
513
+ ## Desenvolvimento
514
+
515
+ 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.
516
+
517
+ 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).
518
+
519
+ ## Contribuindo
520
+
521
+ 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).
522
+
523
+ ## License
524
+
525
+ A gem está disponível como código aberto sob os termos da [Licença MIT](https://opensource.org/licenses/MIT).
526
+
527
+ ## Código de conduta
528
+
529
+ 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).