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.
- checksums.yaml +4 -4
- data/.tool-versions +1 -0
- data/.travis.sh +34 -0
- data/.travis.yml +27 -3
- data/Gemfile +31 -5
- data/README.md +495 -11
- data/README.pt-BR.md +529 -0
- data/lib/micro/observers.rb +5 -23
- data/lib/micro/observers/broadcast.rb +73 -0
- data/lib/micro/observers/event.rb +21 -0
- data/lib/micro/observers/event/names.rb +29 -0
- data/lib/micro/observers/for/active_model.rb +36 -0
- data/lib/micro/observers/for/active_record.rb +16 -0
- data/lib/micro/observers/set.rb +100 -0
- data/lib/micro/observers/subscribers.rb +110 -0
- data/lib/micro/observers/utils.rb +12 -2
- data/lib/micro/observers/version.rb +1 -1
- data/lib/u-observers/for/active_model.rb +2 -0
- data/lib/u-observers/for/active_record.rb +2 -0
- data/test.sh +11 -0
- data/u-observers.gemspec +5 -1
- metadata +44 -5
- data/lib/micro/observers/events_or_actions.rb +0 -25
- data/lib/micro/observers/manager.rb +0 -101
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7da28034b714bb64baf033cf43705ed585fa007659964dc36bec440436292f0
|
4
|
+
data.tar.gz: cd2b40e92cdfd12708ab36f1ed8a7726ecfbc2c6e96223ca090aedc7758f44f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 51be920a8f6f0123e0a94fbf180fb85517ddccb107f729f6eaa38ac840e14a9d207cb82e8f3f76e018e3836f4a7cc0b47b7ea6320ffd0615d22df49c6b693d73
|
7
|
+
data.tar.gz: 66c73487b724149f640e096b24a1e9ecf5e5d8780e9679f1d182823eda377e8b289ed88c98d73f6a81147e945eba7a8ce24278244ab50c1a191ebba1b912e265
|
data/.tool-versions
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby 2.6.5
|
data/.travis.sh
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/bin/bash
|
2
|
+
|
3
|
+
ruby_v=$(ruby -v)
|
4
|
+
|
5
|
+
bundle update
|
6
|
+
bundle exec rake test
|
7
|
+
|
8
|
+
ACTIVERECORD_VERSION='3.2' bundle update
|
9
|
+
ACTIVERECORD_VERSION='3.2' bundle exec rake test
|
10
|
+
|
11
|
+
ACTIVERECORD_VERSION='4.0' bundle update
|
12
|
+
ACTIVERECORD_VERSION='4.0' bundle exec rake test
|
13
|
+
|
14
|
+
ACTIVERECORD_VERSION='4.1' bundle update
|
15
|
+
ACTIVERECORD_VERSION='4.1' bundle exec rake test
|
16
|
+
|
17
|
+
ACTIVERECORD_VERSION='4.2' bundle update
|
18
|
+
ACTIVERECORD_VERSION='4.2' bundle exec rake test
|
19
|
+
|
20
|
+
ACTIVERECORD_VERSION='5.0' bundle update
|
21
|
+
ACTIVERECORD_VERSION='5.0' bundle exec rake test
|
22
|
+
|
23
|
+
ACTIVERECORD_VERSION='5.1' bundle update
|
24
|
+
ACTIVERECORD_VERSION='5.1' bundle exec rake test
|
25
|
+
|
26
|
+
if [[ ! $ruby_v =~ '2.2.0' ]]; then
|
27
|
+
ACTIVERECORD_VERSION='5.2' bundle update
|
28
|
+
ACTIVERECORD_VERSION='5.2' bundle exec rake test
|
29
|
+
fi
|
30
|
+
|
31
|
+
if [[ $ruby_v =~ '2.5.' ]] || [[ $ruby_v =~ '2.6.' ]] || [[ $ruby_v =~ '2.7.' ]]; then
|
32
|
+
ACTIVERECORD_VERSION='6.0' bundle update
|
33
|
+
ACTIVERECORD_VERSION='6.0' bundle exec rake test
|
34
|
+
fi
|
data/.travis.yml
CHANGED
@@ -1,6 +1,30 @@
|
|
1
1
|
---
|
2
2
|
language: ruby
|
3
|
-
|
3
|
+
|
4
|
+
sudo: false
|
5
|
+
|
4
6
|
rvm:
|
5
|
-
|
6
|
-
|
7
|
+
- 2.2.0
|
8
|
+
- 2.3.0
|
9
|
+
- 2.4.0
|
10
|
+
- 2.5.0
|
11
|
+
- 2.6.0
|
12
|
+
- 2.7.0
|
13
|
+
|
14
|
+
cache: bundler
|
15
|
+
|
16
|
+
before_install:
|
17
|
+
- gem uninstall -v '>= 2' -i $(rvm gemdir)@global -ax bundler || true
|
18
|
+
- gem install bundler -v '< 2'
|
19
|
+
|
20
|
+
install: bundle install --jobs=3 --retry=3
|
21
|
+
|
22
|
+
before_script:
|
23
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
24
|
+
- chmod +x ./cc-test-reporter
|
25
|
+
- "./cc-test-reporter before-build"
|
26
|
+
|
27
|
+
script: "./.travis.sh"
|
28
|
+
|
29
|
+
after_success:
|
30
|
+
- "./cc-test-reporter after-build -t simplecov"
|
data/Gemfile
CHANGED
@@ -3,12 +3,38 @@ source 'https://rubygems.org'
|
|
3
3
|
# Specify your gem's dependencies in u-observers.gemspec
|
4
4
|
gemspec
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
activerecord_version = ENV.fetch('ACTIVERECORD_VERSION', '6.1')
|
7
|
+
|
8
|
+
activerecord = case activerecord_version
|
9
|
+
when '3.2' then '3.2.22'
|
10
|
+
when '4.0' then '4.0.13'
|
11
|
+
when '4.1' then '4.1.16'
|
12
|
+
when '4.2' then '4.2.11'
|
13
|
+
when '5.0' then '5.0.7'
|
14
|
+
when '5.1' then '5.1.7'
|
15
|
+
when '5.2' then '5.2.3'
|
16
|
+
when '6.0' then '6.0.3'
|
17
|
+
end
|
18
|
+
|
19
|
+
simplecov_version =
|
20
|
+
case RUBY_VERSION
|
21
|
+
when /\A2.[23]/ then '~> 0.17.1'
|
22
|
+
when /\A2.4/ then '~> 0.18.5'
|
23
|
+
else '~> 0.19'
|
24
|
+
end
|
8
25
|
|
9
26
|
group :test do
|
10
|
-
gem '
|
11
|
-
gem '
|
27
|
+
gem 'minitest', activerecord_version < '4.1' ? '~> 4.2' : '~> 5.0'
|
28
|
+
gem 'simplecov', simplecov_version, require: false
|
29
|
+
|
30
|
+
if activerecord
|
31
|
+
sqlite3 =
|
32
|
+
case activerecord
|
33
|
+
when /\A6\.0/, nil then '~> 1.4.0'
|
34
|
+
else '~> 1.3.0'
|
35
|
+
end
|
12
36
|
|
13
|
-
|
37
|
+
gem 'sqlite3', sqlite3
|
38
|
+
gem 'activerecord', activerecord, require: 'active_record'
|
39
|
+
end
|
14
40
|
end
|
data/README.md
CHANGED
@@ -1,28 +1,512 @@
|
|
1
|
-
|
1
|
+
<p align="center">
|
2
|
+
<h1 align="center">👀 μ-observers</h1>
|
3
|
+
<p align="center"><i>Simple and powerful implementation of the observer pattern.</i></p>
|
4
|
+
<br>
|
5
|
+
</p>
|
2
6
|
|
3
|
-
|
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">
|
4
9
|
|
5
|
-
|
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>
|
6
13
|
|
7
|
-
|
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>
|
8
17
|
|
9
|
-
|
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
|
+
This gem implements the observer pattern [[1]](https://en.wikipedia.org/wiki/Observer_pattern)[[2]](https://refactoring.guru/design-patterns/observer) (also known as publish/subscribe). It provides a simple mechanism for one object to inform a set of interested third-party objects when its state changes.
|
28
|
+
|
29
|
+
Ruby's standard library [has an abstraction](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html) that enables you to use this pattern. But its design can conflict with other mainstream libraries, like the [`ActiveModel`/`ActiveRecord`](https://api.rubyonrails.org/classes/ActiveModel/Dirty.html#method-i-changed), which also has the [`changed`](https://ruby-doc.org/stdlib-2.7.1/libdoc/observer/rdoc/Observable.html#method-i-changed) method. In this case, the behavior of the Stdlib will be compromised.
|
30
|
+
|
31
|
+
Because of this issue, I decided to create a gem that encapsulates the pattern without changing the object's implementation so much. The `Micro::Observers` includes just one instance method in the target class (its instance will be the observed subject/object).
|
32
|
+
|
33
|
+
> **Note:** Você entende português? 🇧🇷 🇵🇹 Verifique o [README traduzido em pt-BR](https://github.com/serradura/u-observers/blob/main/README.pt-BR.md).
|
34
|
+
|
35
|
+
# Table of contents <!-- omit in toc -->
|
36
|
+
- [Installation](#installation)
|
37
|
+
- [Compatibility](#compatibility)
|
38
|
+
- [Usage](#usage)
|
39
|
+
- [Sharing a context with your observers](#sharing-a-context-with-your-observers)
|
40
|
+
- [Sharing data when notifying the observers](#sharing-data-when-notifying-the-observers)
|
41
|
+
- [What is a `Micro::Observers::Event`?](#what-is-a-microobserversevent)
|
42
|
+
- [Using a callable as an observer](#using-a-callable-as-an-observer)
|
43
|
+
- [Calling the observers](#calling-the-observers)
|
44
|
+
- [Notifying observers without marking them as changed](#notifying-observers-without-marking-them-as-changed)
|
45
|
+
- [Defining observers that execute only once](#defining-observers-that-execute-only-once)
|
46
|
+
- [`observers.attach(*args, perform_once: true)`](#observersattachargs-perform_once-true)
|
47
|
+
- [`observers.once(event:, call:, ...)`](#observersonceevent-call-)
|
48
|
+
- [Detaching observers](#detaching-observers)
|
49
|
+
- [ActiveRecord and ActiveModel integrations](#activerecord-and-activemodel-integrations)
|
50
|
+
- [notify_observers_on()](#notify_observers_on)
|
51
|
+
- [notify_observers()](#notify_observers)
|
52
|
+
- [Development](#development)
|
53
|
+
- [Contributing](#contributing)
|
54
|
+
- [License](#license)
|
55
|
+
- [Code of Conduct](#code-of-conduct)
|
56
|
+
|
57
|
+
# Installation
|
58
|
+
|
59
|
+
Add this line to your application's Gemfile and `bundle install`:
|
10
60
|
|
11
61
|
```ruby
|
12
|
-
gem '
|
62
|
+
gem 'u-observers'
|
13
63
|
```
|
14
64
|
|
15
|
-
|
65
|
+
# Compatibility
|
16
66
|
|
17
|
-
|
67
|
+
| u-observers | branch | ruby | activerecord |
|
68
|
+
| ----------- | ------- | -------- | ------------- |
|
69
|
+
| unreleased | main | >= 2.2.0 | >= 3.2, < 6.1 |
|
70
|
+
| 2.2.0 | v2.x | >= 2.2.0 | >= 3.2, < 6.1 |
|
71
|
+
| 1.0.0 | v1.x | >= 2.2.0 | >= 3.2, < 6.1 |
|
18
72
|
|
19
|
-
|
73
|
+
> **Note**: The ActiveRecord isn't a dependency, but you could add a module to enable some static methods that were designed to be used with its [callbacks](https://guides.rubyonrails.org/active_record_callbacks.html).
|
20
74
|
|
21
|
-
|
75
|
+
[⬆️ Back to Top](#table-of-contents-)
|
22
76
|
|
23
77
|
## Usage
|
24
78
|
|
25
|
-
|
79
|
+
Any class with `Micro::Observers` module included can notify events to attached observers.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
require 'securerandom'
|
83
|
+
|
84
|
+
class Order
|
85
|
+
include Micro::Observers
|
86
|
+
|
87
|
+
attr_reader :code
|
88
|
+
|
89
|
+
def initialize
|
90
|
+
@code, @status = SecureRandom.alphanumeric, :draft
|
91
|
+
end
|
92
|
+
|
93
|
+
def canceled?
|
94
|
+
@status == :canceled
|
95
|
+
end
|
96
|
+
|
97
|
+
def cancel!
|
98
|
+
return self if canceled?
|
99
|
+
|
100
|
+
@status = :canceled
|
101
|
+
|
102
|
+
observers.subject_changed!
|
103
|
+
observers.notify(:canceled) and return self
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
module OrderEvents
|
108
|
+
def self.canceled(order)
|
109
|
+
puts "The order #(#{order.code}) has been canceled."
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
order = Order.new
|
114
|
+
#<Order:0x00007fb5dd8fce70 @code="X0o9yf1GsdQFvLR4", @status=:draft>
|
115
|
+
|
116
|
+
order.observers.attach(OrderEvents) # attaching multiple observers. e.g. observers.attach(A, B, C)
|
117
|
+
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[OrderEvents]>
|
118
|
+
|
119
|
+
order.canceled?
|
120
|
+
# false
|
121
|
+
|
122
|
+
order.cancel!
|
123
|
+
# The message below will be printed by the observer (OrderEvents):
|
124
|
+
# The order #(X0o9yf1GsdQFvLR4) has been canceled
|
125
|
+
|
126
|
+
order.canceled?
|
127
|
+
# true
|
128
|
+
|
129
|
+
order.observers.detach(OrderEvents) # detaching multiple observers. e.g. observers.detach(A, B, C)
|
130
|
+
# <#Micro::Observers::Set @subject=#<Order:0x00007fb5dd8fce70> @subject_changed=false @subscribers=[]>
|
131
|
+
|
132
|
+
order.canceled?
|
133
|
+
# true
|
134
|
+
|
135
|
+
order.observers.subject_changed!
|
136
|
+
order.observers.notify(:canceled) # nothing will happen, because there are no observers attached.
|
137
|
+
```
|
138
|
+
|
139
|
+
**Highlights of the previous example:**
|
140
|
+
|
141
|
+
To avoid an undesired behavior, you need to mark the subject as changed before notifying your observers about some event.
|
142
|
+
|
143
|
+
You can do this when using the `#subject_changed!` method. It will automatically mark the subject as changed.
|
144
|
+
|
145
|
+
But if you need to apply some conditional to mark a change, you can use the `#subject_changed` method. e.g. `observers.subject_changed(name != new_name)`
|
146
|
+
|
147
|
+
The `#notify` method always requires an event to make a broadcast. So, if you try to use it without one or more events (symbol values) you will get an exception.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
order.observers.notify
|
151
|
+
# ArgumentError (no events (expected at least 1))
|
152
|
+
```
|
153
|
+
|
154
|
+
[⬆️ Back to Top](#table-of-contents-)
|
155
|
+
|
156
|
+
### Sharing a context with your observers
|
157
|
+
|
158
|
+
To share a context value (any kind of Ruby object) with one or more observers, you will need to use the `:context` keyword as the last argument of the `#attach` method. This feature gives you a unique opportunity to share a value in the attaching moment.
|
159
|
+
|
160
|
+
When the observer method receives two arguments, the first one will be the subject, and the second one an instance of `Micro::Observers::Event` that will have the given context value.
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class Order
|
164
|
+
include Micro::Observers
|
165
|
+
|
166
|
+
def cancel!
|
167
|
+
observers.subject_changed!
|
168
|
+
observers.notify(:canceled)
|
169
|
+
self
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
module OrderEvents
|
174
|
+
def self.canceled(order, event)
|
175
|
+
puts "The order #(#{order.object_id}) has been canceled. (from: #{event.context[:from]})" # event.ctx is an alias for event.context
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
order = Order.new
|
180
|
+
order.observers.attach(OrderEvents, context: { from: 'example #2' }) # attaching multiple observers. e.g. observers.attach(A, B, context: {hello: :world})
|
181
|
+
order.cancel!
|
182
|
+
# The message below will be printed by the observer (OrderEvents):
|
183
|
+
# The order #(70196221441820) has been canceled. (from: example #2)
|
184
|
+
```
|
185
|
+
|
186
|
+
[⬆️ Back to Top](#table-of-contents-)
|
187
|
+
|
188
|
+
### Sharing data when notifying the observers
|
189
|
+
|
190
|
+
As previously mentioned, the [`event context`](#sharing-a-context-with-your-observers) is a value that is stored when you attach your observer. But sometimes, it will be useful to send some additional data when broadcasting an event to the observers. The `event data` gives you this unique opportunity to share some value at the the notification moment.
|
191
|
+
|
192
|
+
```ruby
|
193
|
+
class Order
|
194
|
+
include Micro::Observers
|
195
|
+
end
|
196
|
+
|
197
|
+
module OrderHandler
|
198
|
+
def self.changed(order, event)
|
199
|
+
puts "The order #(#{order.object_id}) received the number #{event.data} from #{event.ctx[:from]}."
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
order = Order.new
|
204
|
+
order.observers.attach(OrderHandler, context: { from: 'example #3' })
|
205
|
+
order.observers.subject_changed!
|
206
|
+
order.observers.notify(:changed, data: 1)
|
207
|
+
# The message below will be printed by the observer (OrderHandler):
|
208
|
+
# The order #(70196221441820) received the number 1 from example #3.
|
209
|
+
```
|
210
|
+
|
211
|
+
[⬆️ Back to Top](#table-of-contents-)
|
212
|
+
|
213
|
+
### What is a `Micro::Observers::Event`?
|
214
|
+
|
215
|
+
The `Micro::Observers::Event` is the event payload. Follow below all of its properties:
|
216
|
+
|
217
|
+
- `#name` will be the broadcasted event.
|
218
|
+
- `#subject` will be the observed subject.
|
219
|
+
- `#context` will be [the context data](#sharing-a-context-with-your-observers) that was defined in the moment that you attach the observer.
|
220
|
+
- `#data` will be [the value that was shared in the observers' notification](#sharing-data-when-notifying-the-observers).
|
221
|
+
- `#ctx` is an alias for the `#context` method.
|
222
|
+
- `#subj` is an alias for the `#subject` method.
|
223
|
+
|
224
|
+
[⬆️ Back to Top](#table-of-contents-)
|
225
|
+
|
226
|
+
### Using a callable as an observer
|
227
|
+
|
228
|
+
The `observers.on()` method enables you to attach a callable as an observer.
|
229
|
+
|
230
|
+
Usually, a callable has a well-defined responsibility (do only one thing), because of this, it tends to be more [SRP (Single-responsibility principle)](https://en.wikipedia.org/wiki/Single-responsibility_principle). friendly than a conventional observer (that could have N methods to respond to different kinds of notification).
|
231
|
+
|
232
|
+
This method receives the below options:
|
233
|
+
1. `:event` the expected event name.
|
234
|
+
2. `:call` the callable object itself.
|
235
|
+
3. `:with` (optional) it can define the value which will be used as the callable object's argument. So, if it is a `Proc`, a `Micro::Observers::Event` instance will be received as the `Proc` argument, and its output will be the callable argument. But if this option wasn't defined, the `Micro::Observers::Event` instance will be the callable argument.
|
236
|
+
4. `:context` will be the context data that was defined in the moment that you attach the observer.
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
class Person
|
240
|
+
include Micro::Observers
|
241
|
+
|
242
|
+
attr_reader :name
|
243
|
+
|
244
|
+
def initialize(name)
|
245
|
+
@name = name
|
246
|
+
end
|
247
|
+
|
248
|
+
def name=(new_name)
|
249
|
+
return unless observers.subject_changed(new_name != @name)
|
250
|
+
|
251
|
+
@name = new_name
|
252
|
+
|
253
|
+
observers.notify(:name_has_been_changed)
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
PrintPersonName = -> (data) do
|
258
|
+
puts("Person name: #{data.fetch(:person).name}, number: #{data.fetch(:number)}")
|
259
|
+
end
|
260
|
+
|
261
|
+
person = Person.new('Rodrigo')
|
262
|
+
|
263
|
+
person.observers.on(
|
264
|
+
event: :name_has_been_changed,
|
265
|
+
call: PrintPersonName,
|
266
|
+
with: -> event { {person: event.subject, number: event.context} },
|
267
|
+
context: rand
|
268
|
+
)
|
269
|
+
|
270
|
+
person.name = 'Serradura'
|
271
|
+
# The message below will be printed by the observer (PrintPersonName):
|
272
|
+
# Person name: Serradura, number: 0.5018509191706862
|
273
|
+
```
|
274
|
+
|
275
|
+
[⬆️ Back to Top](#table-of-contents-)
|
276
|
+
|
277
|
+
### Calling the observers
|
278
|
+
|
279
|
+
You can use a callable (a class, module, or object that responds to the call method) to be your observers.
|
280
|
+
To do this, you only need to make use of the method `#call` instead of `#notify`.
|
281
|
+
|
282
|
+
```ruby
|
283
|
+
class Order
|
284
|
+
include Micro::Observers
|
285
|
+
|
286
|
+
def cancel!
|
287
|
+
observers.subject_changed!
|
288
|
+
observers.call # in practice, this is a shortcut to observers.notify(:call)
|
289
|
+
self
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
NotifyAfterCancel = -> (order) { puts "The order #(#{order.object_id}) has been canceled." }
|
294
|
+
|
295
|
+
order = Order.new
|
296
|
+
order.observers.attach(NotifyAfterCancel)
|
297
|
+
order.cancel!
|
298
|
+
# The message below will be printed by the observer (NotifyAfterCancel):
|
299
|
+
# The order #(70196221441820) has been canceled.
|
300
|
+
```
|
301
|
+
|
302
|
+
> **Note**: The `observers.call` can receive one or more events, but in this case, the default event (`call`) won't be transmitted.
|
303
|
+
|
304
|
+
[⬆️ Back to Top](#table-of-contents-)
|
305
|
+
|
306
|
+
### Notifying observers without marking them as changed
|
307
|
+
|
308
|
+
This feature needs to be used with caution!
|
309
|
+
|
310
|
+
If you use the methods `#notify!` or `#call!` you won't need to mark observers with `#subject_changed`.
|
311
|
+
|
312
|
+
[⬆️ Back to Top](#table-of-contents-)
|
313
|
+
|
314
|
+
### Defining observers that execute only once
|
315
|
+
|
316
|
+
There are two ways to attach an observer and define it to be performed only once.
|
317
|
+
|
318
|
+
The first way to do this is passing the `perform_once: true` option to the `observers.attach()` method. e.g.
|
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
|
+
The second way to achieve this is using `observers.once()` that has the same API of [`observers.on()`](#using-a-callable-as-an-observer). But the difference of the `#once()` method is that it will remove the observer after its execution.
|
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
|
+
[⬆️ Back to Top](#table-of-contents-)
|
377
|
+
|
378
|
+
### Detaching observers
|
379
|
+
|
380
|
+
As shown in the first example, you can use the `observers.detach()` to remove observers.
|
381
|
+
|
382
|
+
But, there is an alternative method to remove observer objects or remove callables by their event names. The method to do this is: `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
|
+
[⬆️ Back to Top](#table-of-contents-)
|
413
|
+
|
414
|
+
### ActiveRecord and ActiveModel integrations
|
415
|
+
|
416
|
+
To make use of this feature you need to require an additional module.
|
417
|
+
|
418
|
+
Gemfile example:
|
419
|
+
```ruby
|
420
|
+
gem 'u-observers', require: 'u-observers/for/active_record'
|
421
|
+
```
|
422
|
+
|
423
|
+
This feature will expose modules that could be used to add macros (static methods) that were designed to work with `ActiveModel`/`ActiveRecord` callbacks. e.g:
|
424
|
+
|
425
|
+
#### notify_observers_on()
|
426
|
+
|
427
|
+
The `notify_observers_on` allows you to define one or more `ActiveModel`/`ActiveRecord` callbacks, that will be used to notify your object observers.
|
428
|
+
|
429
|
+
```ruby
|
430
|
+
class Post < ActiveRecord::Base
|
431
|
+
include ::Micro::Observers::For::ActiveRecord
|
432
|
+
|
433
|
+
notify_observers_on(:after_commit) # using multiple callbacks. e.g. notify_observers_on(:before_save, :after_commit)
|
434
|
+
|
435
|
+
# The method above does the same as the commented example below.
|
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
|
+
# The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
|
461
|
+
# Title: Hello world
|
462
|
+
# Title: Hello world (from: example #6)
|
463
|
+
```
|
464
|
+
|
465
|
+
[⬆️ Back to Top](#table-of-contents-)
|
466
|
+
|
467
|
+
#### notify_observers()
|
468
|
+
|
469
|
+
The `notify_observers` allows you to define one or more *events*, that will be used to notify after the execution of some `ActiveModel`/`ActiveRecord` callback.
|
470
|
+
|
471
|
+
```ruby
|
472
|
+
class Post < ActiveRecord::Base
|
473
|
+
include ::Micro::Observers::For::ActiveRecord
|
474
|
+
|
475
|
+
after_commit(¬ify_observers(:transaction_completed))
|
476
|
+
|
477
|
+
# The method above does the same as the commented example below.
|
478
|
+
#
|
479
|
+
# after_commit do |record|
|
480
|
+
# record.subject_changed!
|
481
|
+
# record.notify(:transaction_completed)
|
482
|
+
# end
|
483
|
+
end
|
484
|
+
|
485
|
+
module TitlePrinter
|
486
|
+
def self.transaction_completed(post)
|
487
|
+
puts("Title: #{post.title}")
|
488
|
+
end
|
489
|
+
end
|
490
|
+
|
491
|
+
module TitlePrinterWithContext
|
492
|
+
def self.transaction_completed(post, event)
|
493
|
+
puts("Title: #{post.title} (from: #{event.ctx[:from]})")
|
494
|
+
end
|
495
|
+
end
|
496
|
+
|
497
|
+
Post.transaction do
|
498
|
+
post = Post.new(title: 'Olá mundo')
|
499
|
+
post.observers.attach(TitlePrinter, TitlePrinterWithContext, context: { from: 'example #7' })
|
500
|
+
post.save
|
501
|
+
end
|
502
|
+
# The message below will be printed by the observers (TitlePrinter, TitlePrinterWithContext):
|
503
|
+
# Title: Olá mundo
|
504
|
+
# Title: Olá mundo (from: example #5)
|
505
|
+
```
|
506
|
+
|
507
|
+
> **Note**: You can use `include ::Micro::Observers::For::ActiveModel` if your class only makes use of the `ActiveModel` and all the previous examples will work.
|
508
|
+
|
509
|
+
[⬆️ Back to Top](#table-of-contents-)
|
26
510
|
|
27
511
|
## Development
|
28
512
|
|