brainguy 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.yardopts +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.erb +345 -0
- data/README.markdown +579 -0
- data/Rakefile +21 -0
- data/brainguy.gemspec +28 -0
- data/examples/include_manifestly_observable.rb +22 -0
- data/examples/include_observable.rb +18 -0
- data/examples/include_observer.rb +36 -0
- data/examples/manual_observable.rb +22 -0
- data/examples/open_observer.rb +31 -0
- data/examples/proc_observer.rb +10 -0
- data/examples/scoped_subscription.rb +39 -0
- data/examples/synopsis.rb +56 -0
- data/lib/brainguy.rb +34 -0
- data/lib/brainguy/basic_notifier.rb +19 -0
- data/lib/brainguy/emitter.rb +110 -0
- data/lib/brainguy/error_collecting_notifier.rb +26 -0
- data/lib/brainguy/error_handling_notifier.rb +63 -0
- data/lib/brainguy/event.rb +13 -0
- data/lib/brainguy/fluent_emitter.rb +30 -0
- data/lib/brainguy/full_subscription.rb +8 -0
- data/lib/brainguy/idempotent_emitter.rb +40 -0
- data/lib/brainguy/manifest_emitter.rb +78 -0
- data/lib/brainguy/manifestly_observable.rb +62 -0
- data/lib/brainguy/observable.rb +33 -0
- data/lib/brainguy/observer.rb +71 -0
- data/lib/brainguy/open_observer.rb +65 -0
- data/lib/brainguy/single_event_subscription.rb +31 -0
- data/lib/brainguy/subscription.rb +59 -0
- data/lib/brainguy/subscription_scope.rb +62 -0
- data/lib/brainguy/version.rb +4 -0
- data/scripts/benchmark_listener_dispatch.rb +222 -0
- data/spec/brainguy/emitter_spec.rb +25 -0
- data/spec/brainguy/error_collecting_notifier_spec.rb +19 -0
- data/spec/brainguy/error_handling_notifier_spec.rb +63 -0
- data/spec/brainguy/manifest_emitter_spec.rb +68 -0
- data/spec/brainguy/manifestly_observable_spec.rb +43 -0
- data/spec/brainguy/observable_spec.rb +9 -0
- data/spec/brainguy/observer_spec.rb +72 -0
- data/spec/brainguy/open_observer_spec.rb +57 -0
- data/spec/brainguy/single_event_subscription_spec.rb +16 -0
- data/spec/brainguy/subscription_scope_spec.rb +72 -0
- data/spec/brainguy/subscription_spec.rb +46 -0
- data/spec/features/basics_spec.rb +153 -0
- data/spec/features/idempotent_events_spec.rb +69 -0
- data/spec/features/method_scoped_events_spec.rb +90 -0
- data/spec/support/shared_examples_for_eventful_modules.rb +36 -0
- metadata +196 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a5ee4639149ae25d01b264ed4b9c95195362945f
|
4
|
+
data.tar.gz: e01b8fa15ad59f03df5a6e5844e21eead9cd1b81
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c0ccb42305475236a4291b1b3d32236b84ff3297628f0817271ca68c99bc789501b1b7c5d658dd38cea86fe03c2933fd6eff67921d7acaefe740da55405efdd4
|
7
|
+
data.tar.gz: f92036dd02022d50319c71a5f65ffdb71387636bcde316b47217595d0165cd4a076808d52d66c8d482a883d45bc2225baf2633c62a9200c86eacaaad5fcf611d
|
data/.gitignore
ADDED
data/.yardopts
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Avdi Grimm
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.erb
ADDED
@@ -0,0 +1,345 @@
|
|
1
|
+
# Brainguy
|
2
|
+
|
3
|
+
![Observer, AKA "Brain Guy"](http://static.tvtropes.org/pmwiki/pub/images/MST3K_Brain_Guy_7093.jpg)
|
4
|
+
|
5
|
+
Brainguy is an Observer library for Ruby.
|
6
|
+
|
7
|
+
## Synopsis
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
<%= File.read("examples/synopsis.rb") %>
|
11
|
+
```
|
12
|
+
|
13
|
+
## Introduction
|
14
|
+
|
15
|
+
*Well, here we are again.*
|
16
|
+
|
17
|
+
Back with another of those block-rockin' READMEs!
|
18
|
+
|
19
|
+
*You know, I can just leave now.*
|
20
|
+
|
21
|
+
Sorry. It won't happen again.
|
22
|
+
|
23
|
+
*So, "Brainguy", huh. What's the deal this time?*
|
24
|
+
|
25
|
+
This is an Observer pattern library for Ruby. The name is a play on the
|
26
|
+
character from Mystery Sci---
|
27
|
+
|
28
|
+
*Yeah yeah blah blah nerd nerd very clever. What's it do?*
|
29
|
+
|
30
|
+
In a nutshell, it's a decoupling mechanism. It lets "observer" objects
|
31
|
+
subscribe to events generated by other objects.
|
32
|
+
|
33
|
+
*Kind of like the `observer` Ruby standard library?"*
|
34
|
+
|
35
|
+
Yeah, exactly. But this library is a little bit fancier. It adds a
|
36
|
+
number of conveniences that you otherwise might have to build yourself on top of `observer`.
|
37
|
+
|
38
|
+
*Such as?*
|
39
|
+
|
40
|
+
Well, the most important feature it has is *named event types*. Instead of a single "update" event, events have symbolic names. Observers can choose which events they care about, and ignore the rest.
|
41
|
+
|
42
|
+
### Defining some terms
|
43
|
+
|
44
|
+
*What exactly is a "observer"? Is it a special kind of object?*
|
45
|
+
|
46
|
+
Not really, no. Fundamentally a observer is any object which responds to `#call`. The most obvious example of such an object is a `Proc`. Here's an example of using a proc as a simple observer:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
<%= File.read("examples/proc_observer.rb") %>
|
50
|
+
```
|
51
|
+
|
52
|
+
Every time the emitter emits an event, the observer proc will receive `#call` with an `Event` object as an argument.
|
53
|
+
|
54
|
+
*What's an "emitter"?*
|
55
|
+
|
56
|
+
An Emitter serves dual roles: first, it manages subscriptions to a particular event source. And second, it can "emit" events to all of the observers currently subscribed to it.
|
57
|
+
|
58
|
+
*What exactly is an "event", anyway?*
|
59
|
+
|
60
|
+
Notionally an event is some occurrence in an object, which other objects might want to know about. What sort of occurrences might be depends on your problem domain. A `User` might have a `:modified` event. An `WebServiceRequest` might have a `:success` event. A `Toaster` might have a `:pop` event. And so on.
|
61
|
+
|
62
|
+
*So an event is just a symbol?*
|
63
|
+
|
64
|
+
An event is *named* with a symbol. But there is some other information that normally travels along with an event:
|
65
|
+
|
66
|
+
- An event *source*, which is the observer object that generated the event.
|
67
|
+
- An arbitrary list of *arguments*.
|
68
|
+
|
69
|
+
Extra arguments can be added to an event by passing extra arguments to the `#emit`, like this:
|
70
|
+
|
71
|
+
```ruby
|
72
|
+
events.emit(:movie_sign, movie_title: "Giant Spider Invasion")
|
73
|
+
```
|
74
|
+
|
75
|
+
For convenience, the event name, source, and arguments are all bundled into an `Event` object before being disseminated to observers.
|
76
|
+
|
77
|
+
### Making an object observable
|
78
|
+
|
79
|
+
*OK, say I have an object that I want to make observable. How would I go about that?*
|
80
|
+
|
81
|
+
Well, the no-magic way might go something like this:
|
82
|
+
|
83
|
+
```ruby
|
84
|
+
<%= File.read("examples/manual_observable.rb") %>
|
85
|
+
```
|
86
|
+
|
87
|
+
Notice that we pass `self` to the new `Emitter`, so that it will know what object to set as the event source for emitted events.
|
88
|
+
|
89
|
+
*That's pretty straightforward. Is there a more-magic way?*
|
90
|
+
|
91
|
+
Of course! But it's not much more magic. There's an `Observable` module that just packages up the convention we used above into a reusable mixin you can use in any of your classes. Here's what that code would look like using the mixin:
|
92
|
+
|
93
|
+
```ruby
|
94
|
+
<%= File.read("examples/include_observable.rb") %>
|
95
|
+
```
|
96
|
+
|
97
|
+
*I see that instead of `events.emit(...)`, now the class just uses `emit(...)`. And the same with `#on`.*
|
98
|
+
|
99
|
+
Very observant! `Observable` adds four methods to classes which mix it in:
|
100
|
+
|
101
|
+
- `#on`, to quickly attach single-event handlers on the object.
|
102
|
+
- `#emit`, a private method for conveniently emitting events inside the class.
|
103
|
+
- `#events`, to access the `Emitter` object.
|
104
|
+
- `#with_subscription_scope`, which we'll talk about later.
|
105
|
+
|
106
|
+
*That's not a lot of methods added.*
|
107
|
+
|
108
|
+
Nope! That's intentional. These are your classes, and I don't want to clutter up your API unnecessarily. `#on` and `#emit` are provided as conveniences for common actions. Anything else you need, you can get to via the `Emitter` returned from `#events`.
|
109
|
+
|
110
|
+
### Constraining event types
|
111
|
+
|
112
|
+
*I see that un-handled events are just ignored. Doesn't that make it easy to miss events because of a typo in the name?*
|
113
|
+
|
114
|
+
Yeah, it kinda does. In order to help with that, there's an alternative kind of emitter: a `ManifestEmitter`. And to go along with it, there's a `ManifestlyObservable` mixin module. We customize the module with a list of known event names. Then if anything tries to either emit or subscribe to an unknown event name, the emitter outputs a warning.
|
115
|
+
|
116
|
+
Well, that's what it does by default. We can also customize the policy for how to handle unknown events, as this example demonstrates:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
<%= File.read("examples/include_manifestly_observable.rb") %>
|
120
|
+
```
|
121
|
+
|
122
|
+
### All about observers
|
123
|
+
|
124
|
+
*I'm still a little confused about `#on`. Is that just another way to add an observer?*
|
125
|
+
|
126
|
+
`#on` is really just a shortcut. Often we don't want to attach a whole observer to an observable object. We just want to trigger a particular block of code to be run when a specific event is detected. So `#on` makes it easy to hook up a block of code to a single event.
|
127
|
+
|
128
|
+
*So it's a special case.*
|
129
|
+
|
130
|
+
Yep!
|
131
|
+
|
132
|
+
*Let's talk about the general case a bit more. You said an observer is just a callable object?*
|
133
|
+
|
134
|
+
Yeah. Anything which will respond to `#call` and accept a single `Event` as an argument.
|
135
|
+
|
136
|
+
*But what if I want my observer to do different things depending on what kind of event it receives? Do I have to write a case statement inside my `#call` method?*
|
137
|
+
|
138
|
+
You could if you wanted to. But that's a common desire, so there are some conveniences for it.
|
139
|
+
|
140
|
+
*Such as...?*
|
141
|
+
|
142
|
+
Well, first off, there's `OpenObserver`. It's kinda like Ruby's `OpenObject`, but for observer objects. You can use it to quickly put together a reusable observer object. For instance, here's an example where we have two different observable objects, observed by a single `OpenObserver`.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
<%= File.read("examples/open_observer.rb") %>
|
146
|
+
```
|
147
|
+
|
148
|
+
There are a few other ways to instantiate an `OpenObserver`; check out the source code and tests for more information.
|
149
|
+
|
150
|
+
*What if my observer needs are more elaborate? What if I want a dedicated class for observing an event stream?*
|
151
|
+
|
152
|
+
There's a helper for that as well. Here's an example where we have a `Poem` class that can recite a poem, generating events along the way. And then we have an `HtmlFormatter` which observes those events and incrementally constructs some HTML text as it does so.
|
153
|
+
|
154
|
+
```ruby
|
155
|
+
<%= File.read("examples/include_observer.rb") %>
|
156
|
+
```
|
157
|
+
|
158
|
+
*So including `Observer` automatically handles the dispatching of events from `#call` to the various `#on_` methods?*
|
159
|
+
|
160
|
+
Yes, exactly. And through some metaprogramming, it is able to do this in a way that is just as performant as a hand-written case statement.
|
161
|
+
|
162
|
+
*How do you know it's that fast?*
|
163
|
+
|
164
|
+
You can run the proof-of-concept benchmark for yourself! It's in the `scripts` directory.
|
165
|
+
|
166
|
+
### Managing subscription lifetime
|
167
|
+
|
168
|
+
*You know, it occurs to me that in the `Poem` example, it really doesn't make sense to have an `HtmlFormatter` plugged into a `Poem` forever. Is there a way to attach it before the call to `#recite`, and then detach it immediately after?*
|
169
|
+
|
170
|
+
Of course. All listener registration methods return a `Subscription` object which can be used to manage the subscription of an observer to emitter. If we wanted to observe the `Poem` for just a single recital, we could do it like this:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
p = Poem.new
|
174
|
+
f = HtmlFormatter.new
|
175
|
+
subscription = p.events.attach(f)
|
176
|
+
p.recite
|
177
|
+
subscription.cancel
|
178
|
+
```
|
179
|
+
|
180
|
+
*OK, so I just need to remember to `#cancel` the subscriptions that I don't want sticking around.*
|
181
|
+
|
182
|
+
That's one way to do it. But this turns out to be a common use case. It's often desirable to have observers that are in effect just for the length of a single method call.
|
183
|
+
|
184
|
+
Here's how we might re-write the "poem" example with event subscriptions scoped to just the `#recite` call:
|
185
|
+
|
186
|
+
```ruby
|
187
|
+
<%= File.read("examples/scoped_subscription.rb") %>
|
188
|
+
```
|
189
|
+
|
190
|
+
In this example, the `HtmlFormatter` is only subscribed to poem events for the duration of the call to `#recite`. After that it is automatically detached.
|
191
|
+
|
192
|
+
### Replacing return values with events
|
193
|
+
|
194
|
+
*Interesting. I can see this being useful for more than just traditionally event-generating objects.*
|
195
|
+
|
196
|
+
Indeed it is! This turns out to be a useful pattern for any kind of method which acts as a "command".
|
197
|
+
|
198
|
+
For instance, let's imagine a fictional HTTP request method. Different things happen over the course of a request:
|
199
|
+
|
200
|
+
- headers come back
|
201
|
+
- data comes back (possibly more than once, if it is a streaming-style connection)
|
202
|
+
- an error may occur
|
203
|
+
- otherwise, at some point it will reach a successful finish
|
204
|
+
|
205
|
+
Let's look at how that could be modeled using an "event-ful" method:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
connection.request(:get, "/") do |events|
|
209
|
+
events.on(:header){ ... } # handle a header
|
210
|
+
events.on(:data){ ... } # handle data
|
211
|
+
events.on(:error){ ... } # handle errors
|
212
|
+
events.on(:success){ ... } # finish up
|
213
|
+
end
|
214
|
+
```
|
215
|
+
|
216
|
+
This API has some interesting properties:
|
217
|
+
|
218
|
+
- Notice how some of the events that are handled will only occur once (`error`, `success`), whereas others (`data`, `header`) may be called multiple times. The event-handler API style means that both singular and repeatable events can be handled in a consistent way.
|
219
|
+
- A common headache in designing APIs is deciding how to handle errors. Should an exception be raised? Should there be an exceptional return value? Using events, the client code can set the error policy.
|
220
|
+
|
221
|
+
*But couldn't you accomplish the same thing by returning different values for success, failure, etc?*
|
222
|
+
|
223
|
+
Not easily. Sure, you could define a method that returned `[:success, 200]` on success, and `[:error, 500]` on failure. But what about the `data` events that may be emitted multiple times as data comes in? Libraries typically handle this limitation by providing separate APIs and/or objects for "streaming" responses. Using events handlers makes it possible to handle both single-return and streaming-style requests in a consistent way.
|
224
|
+
|
225
|
+
*I don't like that blocks-in-a-block syntax*
|
226
|
+
|
227
|
+
If you're willing to wait until a method call is complete before handling events, there's an alternative to that syntax. Let's say our `#request` method is implemented something like this:
|
228
|
+
|
229
|
+
```ruby
|
230
|
+
class Connection
|
231
|
+
include Brainguy::Observable
|
232
|
+
|
233
|
+
def request(method, path, &block)
|
234
|
+
with_subscription_scope(block) do
|
235
|
+
# ...
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
# ...
|
240
|
+
end
|
241
|
+
```
|
242
|
+
|
243
|
+
In that case, instead of sending it with a block, we can do this:
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
connection.request(:get, "/")
|
247
|
+
.on(:header){ ... } # handle a header
|
248
|
+
.on(:data){ ... } # handle data
|
249
|
+
.on(:error){ ... } # handle errors
|
250
|
+
.on(:success){ ... } # finish up
|
251
|
+
```
|
252
|
+
|
253
|
+
*How the heck does that work?*
|
254
|
+
|
255
|
+
If the method is called without a block, events are queued up in an {Brainguy::IdempotentEmitter}. This is a special kind of emitter that "plays back" any events that an observer missed, as soon as it is attached.
|
256
|
+
|
257
|
+
Then it's wrapped in a special {Brainguy::FluentEmitter} before being returned. This enables the "chained" calling style you can see in the example above. Normally, sending `#on` would return a {Brainguy::Subscription} object, so that wouldn't work.
|
258
|
+
|
259
|
+
The upshot is that all the events are collected over the course of the method's execution. Then they are played back on each handler as it is added.
|
260
|
+
|
261
|
+
*What if I **only** want eventful methods? I don't want my object to carry a long-lived list of observers around?*
|
262
|
+
|
263
|
+
Gotcha covered. You can use {Brainguy.with_subscription_scope} to add a temporary subscription scope to any method without first including {Brainguy::Observable}.
|
264
|
+
|
265
|
+
```ruby
|
266
|
+
class Connection
|
267
|
+
def request(method, path, &block)
|
268
|
+
Brainguy.with_subscription_scope(self) do
|
269
|
+
# ...
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# ...
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
*This is a lot to take in. Anything else you want to tell me about?*
|
278
|
+
|
279
|
+
We've covered most of the major features. One thing we haven't talked about is error suppression.
|
280
|
+
|
281
|
+
### Suppressing errors
|
282
|
+
|
283
|
+
*Why would you want to suppress errors?*
|
284
|
+
|
285
|
+
Well, we all know that observers effect the thing being observed. But it can be nice to minimize that effect as much as possible. For instance, if you have a critical process that's being observed, you may want to ensure that spurious errors inside of observers don't cause it to crash.
|
286
|
+
|
287
|
+
*Yeah, I could see where that could be a problem.*
|
288
|
+
|
289
|
+
So there are some tools for setting a policy in place for what to do with errors in event handlers, including turning them into warnings, suppressing them entirely, or building a list of errors.
|
290
|
+
|
291
|
+
I'm not going to go over them in detail here in the README, but you should check out {Brainguy::ErrorHandlingNotifier} and {Brainguy::ErrorCollectingNotifier}, along with their spec files, for more information. They are pretty easy to use.
|
292
|
+
|
293
|
+
## FAQ
|
294
|
+
|
295
|
+
*Is this library like ActiveRecord callbacks? Or like Rails observers?*
|
296
|
+
|
297
|
+
No. ActiveRecord enables callbacks to be enabled at a class level, so that every instance is implicitly being subscribed to. Rails "observers" enable observers to be added with no knowledge whatsoever on the part of the objects being observed.
|
298
|
+
|
299
|
+
Brainguy explicitly eschews this kind of "spooky action at a distance". If you want to be notified of what goes on inside an object, you have to subscribe to that object.
|
300
|
+
|
301
|
+
*Is this an aspect-oriented programming library? Or a lisp-style "method advice" system?*
|
302
|
+
|
303
|
+
No. Aspect-oriented programming and lisp-style method advice are more ways of adding "spooky action at a distance", where the code being advised may have no idea that it is having foreign logic attached to it. As well as no control over *where* that foreign logic is applied.
|
304
|
+
|
305
|
+
In Brainguy, by contrast, objects are explicitly subscribed-to, and events are explicitly emitted.
|
306
|
+
|
307
|
+
*Is this a library for "Reactive Programming"?*
|
308
|
+
|
309
|
+
Not in and of itself. It could potentially serve as the foundation for such a library though.
|
310
|
+
|
311
|
+
*Is this a library for creating "hooks"?*
|
312
|
+
|
313
|
+
Sort of. Observers do let you "hook" arbitrary handlers to events in other objects. However, this library is not aimed at enabling you to create hooks that *modify* the behavior of other methods. It's primarily intended to allow objects to be *notified* of significant events, without interfering in the processing of the object sending out the notifications.
|
314
|
+
|
315
|
+
*Is this an asynchronous messaging or reactor system?*
|
316
|
+
|
317
|
+
No. Brainguy events are processed synchronously have no awareness of concurrency.
|
318
|
+
|
319
|
+
## Installation
|
320
|
+
|
321
|
+
Add this line to your application's Gemfile:
|
322
|
+
|
323
|
+
```ruby
|
324
|
+
gem 'brainguy'
|
325
|
+
```
|
326
|
+
|
327
|
+
And then execute:
|
328
|
+
|
329
|
+
$ bundle
|
330
|
+
|
331
|
+
Or install it yourself as:
|
332
|
+
|
333
|
+
$ gem install brainguy
|
334
|
+
|
335
|
+
## Usage
|
336
|
+
|
337
|
+
Coming soon!
|
338
|
+
|
339
|
+
## Contributing
|
340
|
+
|
341
|
+
1. Fork it ( https://github.com/avdi/brainguy/fork )
|
342
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
343
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
344
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
345
|
+
5. Create a new Pull Request
|
data/README.markdown
ADDED
@@ -0,0 +1,579 @@
|
|
1
|
+
# Brainguy
|
2
|
+
|
3
|
+
![Observer, AKA "Brain Guy"](http://static.tvtropes.org/pmwiki/pub/images/MST3K_Brain_Guy_7093.jpg)
|
4
|
+
|
5
|
+
Brainguy is an Observer library for Ruby.
|
6
|
+
|
7
|
+
## Synopsis
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require "brainguy"
|
11
|
+
|
12
|
+
class SatelliteOfLove
|
13
|
+
include Brainguy::Observable
|
14
|
+
|
15
|
+
def intro_song
|
16
|
+
emit(:robot_roll_call)
|
17
|
+
end
|
18
|
+
|
19
|
+
def send_the_movie
|
20
|
+
emit(:movie_sign)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Crew
|
25
|
+
include Brainguy::Observer
|
26
|
+
end
|
27
|
+
|
28
|
+
class TomServo < Crew
|
29
|
+
def on_robot_roll_call(event)
|
30
|
+
puts "Tom: Check me out!"
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class CrowTRobot < Crew
|
35
|
+
def on_robot_roll_call(event)
|
36
|
+
puts "Crow: I'm different!"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class MikeNelson < Crew
|
41
|
+
def on_movie_sign(event)
|
42
|
+
puts "Mike: Oh no we've got movie sign!"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
sol = SatelliteOfLove.new
|
47
|
+
# Attach specific event handlers without a listener object
|
48
|
+
sol.on(:robot_roll_call) do
|
49
|
+
puts "[Robot roll call!]"
|
50
|
+
end
|
51
|
+
sol.on(:movie_sign) do
|
52
|
+
puts "[Movie sign flashes]"
|
53
|
+
end
|
54
|
+
sol.events.attach TomServo.new
|
55
|
+
sol.events.attach CrowTRobot.new
|
56
|
+
sol.events.attach MikeNelson.new
|
57
|
+
|
58
|
+
sol.intro_song
|
59
|
+
sol.send_the_movie
|
60
|
+
|
61
|
+
# >> [Robot roll call!]
|
62
|
+
# >> Tom: Check me out!
|
63
|
+
# >> Crow: I'm different!
|
64
|
+
# >> [Movie sign flashes]
|
65
|
+
# >> Mike: Oh no we've got movie sign!
|
66
|
+
|
67
|
+
```
|
68
|
+
|
69
|
+
## Introduction
|
70
|
+
|
71
|
+
*Well, here we are again.*
|
72
|
+
|
73
|
+
Back with another of those block-rockin' READMEs!
|
74
|
+
|
75
|
+
*You know, I can just leave now.*
|
76
|
+
|
77
|
+
Sorry. It won't happen again.
|
78
|
+
|
79
|
+
*So, "Brainguy", huh. What's the deal this time?*
|
80
|
+
|
81
|
+
This is an Observer pattern library for Ruby. The name is a play on the
|
82
|
+
character from Mystery Sci---
|
83
|
+
|
84
|
+
*Yeah yeah blah blah nerd nerd very clever. What's it do?*
|
85
|
+
|
86
|
+
In a nutshell, it's a decoupling mechanism. It lets "observer" objects
|
87
|
+
subscribe to events generated by other objects.
|
88
|
+
|
89
|
+
*Kind of like the `observer` Ruby standard library?"*
|
90
|
+
|
91
|
+
Yeah, exactly. But this library is a little bit fancier. It adds a
|
92
|
+
number of conveniences that you otherwise might have to build yourself on top of `observer`.
|
93
|
+
|
94
|
+
*Such as?*
|
95
|
+
|
96
|
+
Well, the most important feature it has is *named event types*. Instead of a single "update" event, events have symbolic names. Observers can choose which events they care about, and ignore the rest.
|
97
|
+
|
98
|
+
### Defining some terms
|
99
|
+
|
100
|
+
*What exactly is a "observer"? Is it a special kind of object?*
|
101
|
+
|
102
|
+
Not really, no. Fundamentally a observer is any object which responds to `#call`. The most obvious example of such an object is a `Proc`. Here's an example of using a proc as a simple observer:
|
103
|
+
|
104
|
+
```ruby
|
105
|
+
require "brainguy"
|
106
|
+
|
107
|
+
events = Brainguy::Emitter.new
|
108
|
+
observer = proc do |event|
|
109
|
+
puts "Got event: #{event.name}"
|
110
|
+
end
|
111
|
+
events.attach(observer)
|
112
|
+
events.emit(:ding)
|
113
|
+
|
114
|
+
# >> Got event: ding
|
115
|
+
|
116
|
+
```
|
117
|
+
|
118
|
+
Every time the emitter emits an event, the observer proc will receive `#call` with an `Event` object as an argument.
|
119
|
+
|
120
|
+
*What's an "emitter"?*
|
121
|
+
|
122
|
+
An Emitter serves dual roles: first, it manages subscriptions to a particular event source. And second, it can "emit" events to all of the observers currently subscribed to it.
|
123
|
+
|
124
|
+
*What exactly is an "event", anyway?*
|
125
|
+
|
126
|
+
Notionally an event is some occurrence in an object, which other objects might want to know about. What sort of occurrences might be depends on your problem domain. A `User` might have a `:modified` event. An `WebServiceRequest` might have a `:success` event. A `Toaster` might have a `:pop` event. And so on.
|
127
|
+
|
128
|
+
*So an event is just a symbol?*
|
129
|
+
|
130
|
+
An event is *named* with a symbol. But there is some other information that normally travels along with an event:
|
131
|
+
|
132
|
+
- An event *source*, which is the observer object that generated the event.
|
133
|
+
- An arbitrary list of *arguments*.
|
134
|
+
|
135
|
+
Extra arguments can be added to an event by passing extra arguments to the `#emit`, like this:
|
136
|
+
|
137
|
+
```ruby
|
138
|
+
events.emit(:movie_sign, movie_title: "Giant Spider Invasion")
|
139
|
+
```
|
140
|
+
|
141
|
+
For convenience, the event name, source, and arguments are all bundled into an `Event` object before being disseminated to observers.
|
142
|
+
|
143
|
+
### Making an object observable
|
144
|
+
|
145
|
+
*OK, say I have an object that I want to make observable. How would I go about that?*
|
146
|
+
|
147
|
+
Well, the no-magic way might go something like this:
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
require "brainguy"
|
151
|
+
|
152
|
+
class Toaster
|
153
|
+
attr_reader :events
|
154
|
+
|
155
|
+
def initialize
|
156
|
+
@events = Brainguy::Emitter.new(self)
|
157
|
+
end
|
158
|
+
|
159
|
+
def make_toast
|
160
|
+
events.emit(:start)
|
161
|
+
events.emit(:pop)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
toaster = Toaster.new
|
166
|
+
toaster.events.on(:pop) do
|
167
|
+
puts "Toanst is done!"
|
168
|
+
end
|
169
|
+
toaster.make_toast
|
170
|
+
|
171
|
+
# >> Toast is done!
|
172
|
+
|
173
|
+
```
|
174
|
+
|
175
|
+
Notice that we pass `self` to the new `Emitter`, so that it will know what object to set as the event source for emitted events.
|
176
|
+
|
177
|
+
*That's pretty straightforward. Is there a more-magic way?*
|
178
|
+
|
179
|
+
Of course! But it's not much more magic. There's an `Observable` module that just packages up the convention we used above into a reusable mixin you can use in any of your classes. Here's what that code would look like using the mixin:
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
require "brainguy"
|
183
|
+
|
184
|
+
class Toaster
|
185
|
+
include Brainguy::Observable
|
186
|
+
|
187
|
+
def make_toast
|
188
|
+
emit(:start)
|
189
|
+
emit(:pop)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
toaster = Toaster.new
|
194
|
+
toaster.on(:pop) do
|
195
|
+
puts "Toast is done!"
|
196
|
+
end
|
197
|
+
toaster.make_toast
|
198
|
+
|
199
|
+
# >> Toast is done!
|
200
|
+
|
201
|
+
```
|
202
|
+
|
203
|
+
*I see that instead of `events.emit(...)`, now the class just uses `emit(...)`. And the same with `#on`.*
|
204
|
+
|
205
|
+
Very observant! `Observable` adds four methods to classes which mix it in:
|
206
|
+
|
207
|
+
- `#on`, to quickly attach single-event handlers on the object.
|
208
|
+
- `#emit`, a private method for conveniently emitting events inside the class.
|
209
|
+
- `#events`, to access the `Emitter` object.
|
210
|
+
- `#with_subscription_scope`, which we'll talk about later.
|
211
|
+
|
212
|
+
*That's not a lot of methods added.*
|
213
|
+
|
214
|
+
Nope! That's intentional. These are your classes, and I don't want to clutter up your API unnecessarily. `#on` and `#emit` are provided as conveniences for common actions. Anything else you need, you can get to via the `Emitter` returned from `#events`.
|
215
|
+
|
216
|
+
### Constraining event types
|
217
|
+
|
218
|
+
*I see that un-handled events are just ignored. Doesn't that make it easy to miss events because of a typo in the name?*
|
219
|
+
|
220
|
+
Yeah, it kinda does. In order to help with that, there's an alternative kind of emitter: a `ManifestEmitter`. And to go along with it, there's a `ManifestlyObservable` mixin module. We customize the module with a list of known event names. Then if anything tries to either emit or subscribe to an unknown event name, the emitter outputs a warning.
|
221
|
+
|
222
|
+
Well, that's what it does by default. We can also customize the policy for how to handle unknown events, as this example demonstrates:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
require "brainguy"
|
226
|
+
|
227
|
+
class Toaster
|
228
|
+
include Brainguy::ManifestlyObservable.new(:start, :pop)
|
229
|
+
|
230
|
+
def make_toast
|
231
|
+
emit(:start)
|
232
|
+
emit(:lop)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
toaster = Toaster.new
|
237
|
+
toaster.events.unknown_event_policy = :raise_error
|
238
|
+
toaster.on(:plop) do
|
239
|
+
puts "Toast is done!"
|
240
|
+
end
|
241
|
+
toaster.make_toast
|
242
|
+
|
243
|
+
# ~> Brainguy::UnknownEvent
|
244
|
+
# ~> #on received for unknown event type 'plop'
|
245
|
+
# ~>
|
246
|
+
# ~> xmptmp-in27856uxq.rb:14:in `<main>'
|
247
|
+
|
248
|
+
```
|
249
|
+
|
250
|
+
### All about observers
|
251
|
+
|
252
|
+
*I'm still a little confused about `#on`. Is that just another way to add an observer?*
|
253
|
+
|
254
|
+
`#on` is really just a shortcut. Often we don't want to attach a whole observer to an observable object. We just want to trigger a particular block of code to be run when a specific event is detected. So `#on` makes it easy to hook up a block of code to a single event.
|
255
|
+
|
256
|
+
*So it's a special case.*
|
257
|
+
|
258
|
+
Yep!
|
259
|
+
|
260
|
+
*Let's talk about the general case a bit more. You said an observer is just a callable object?*
|
261
|
+
|
262
|
+
Yeah. Anything which will respond to `#call` and accept a single `Event` as an argument.
|
263
|
+
|
264
|
+
*But what if I want my observer to do different things depending on what kind of event it receives? Do I have to write a case statement inside my `#call` method?*
|
265
|
+
|
266
|
+
You could if you wanted to. But that's a common desire, so there are some conveniences for it.
|
267
|
+
|
268
|
+
*Such as...?*
|
269
|
+
|
270
|
+
Well, first off, there's `OpenObserver`. It's kinda like Ruby's `OpenObject`, but for observer objects. You can use it to quickly put together a reusable observer object. For instance, here's an example where we have two different observable objects, observed by a single `OpenObserver`.
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
require "brainguy"
|
274
|
+
|
275
|
+
class VideoRender
|
276
|
+
include Brainguy::Observable
|
277
|
+
attr_reader :name
|
278
|
+
def initialize(name)
|
279
|
+
@name = name
|
280
|
+
end
|
281
|
+
|
282
|
+
def do_render
|
283
|
+
emit(:complete)
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
v1 = VideoRender.new("foo.mp4")
|
288
|
+
v2 = VideoRender.new("bar.mp4")
|
289
|
+
|
290
|
+
observer = Brainguy::OpenObserver.new do |o|
|
291
|
+
o.on_complete do |event|
|
292
|
+
puts "Video #{event.source.name} is done rendering!"
|
293
|
+
end
|
294
|
+
end
|
295
|
+
|
296
|
+
v1.events.attach(observer)
|
297
|
+
v2.events.attach(observer)
|
298
|
+
|
299
|
+
v1.do_render
|
300
|
+
v2.do_render
|
301
|
+
|
302
|
+
# >> Video foo.mp4 is done rendering!
|
303
|
+
# >> Video bar.mp4 is done rendering!
|
304
|
+
|
305
|
+
```
|
306
|
+
|
307
|
+
There are a few other ways to instantiate an `OpenObserver`; check out the source code and tests for more information.
|
308
|
+
|
309
|
+
*What if my observer needs are more elaborate? What if I want a dedicated class for observing an event stream?*
|
310
|
+
|
311
|
+
There's a helper for that as well. Here's an example where we have a `Poem` class that can recite a poem, generating events along the way. And then we have an `HtmlFormatter` which observes those events and incrementally constructs some HTML text as it does so.
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
require "brainguy"
|
315
|
+
|
316
|
+
class Poem
|
317
|
+
include Brainguy::Observable
|
318
|
+
def recite
|
319
|
+
emit(:title, "Jabberwocky")
|
320
|
+
emit(:line, "'twas brillig, and the slithy toves")
|
321
|
+
emit(:line, "Did gyre and gimbal in the wabe")
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
class HtmlFormatter
|
326
|
+
include Brainguy::Observer
|
327
|
+
|
328
|
+
attr_reader :result
|
329
|
+
|
330
|
+
def initialize
|
331
|
+
@result = ""
|
332
|
+
end
|
333
|
+
|
334
|
+
def on_title(event)
|
335
|
+
@result << "<h1>#{event.args.first}</h1>"
|
336
|
+
end
|
337
|
+
|
338
|
+
def on_line(event)
|
339
|
+
@result << "#{event.args.first}</br>"
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
p = Poem.new
|
344
|
+
f = HtmlFormatter.new
|
345
|
+
p.events.attach(f)
|
346
|
+
p.recite
|
347
|
+
|
348
|
+
f.result
|
349
|
+
# => "<h1>Jabberwocky</h1>'twas brillig, and the slithy toves</br>Did gyre an...
|
350
|
+
|
351
|
+
```
|
352
|
+
|
353
|
+
*So including `Observer` automatically handles the dispatching of events from `#call` to the various `#on_` methods?*
|
354
|
+
|
355
|
+
Yes, exactly. And through some metaprogramming, it is able to do this in a way that is just as performant as a hand-written case statement.
|
356
|
+
|
357
|
+
*How do you know it's that fast?*
|
358
|
+
|
359
|
+
You can run the proof-of-concept benchmark for yourself! It's in the `scripts` directory.
|
360
|
+
|
361
|
+
### Managing subscription lifetime
|
362
|
+
|
363
|
+
*You know, it occurs to me that in the `Poem` example, it really doesn't make sense to have an `HtmlFormatter` plugged into a `Poem` forever. Is there a way to attach it before the call to `#recite`, and then detach it immediately after?*
|
364
|
+
|
365
|
+
Of course. All listener registration methods return a `Subscription` object which can be used to manage the subscription of an observer to emitter. If we wanted to observe the `Poem` for just a single recital, we could do it like this:
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
p = Poem.new
|
369
|
+
f = HtmlFormatter.new
|
370
|
+
subscription = p.events.attach(f)
|
371
|
+
p.recite
|
372
|
+
subscription.cancel
|
373
|
+
```
|
374
|
+
|
375
|
+
*OK, so I just need to remember to `#cancel` the subscriptions that I don't want sticking around.*
|
376
|
+
|
377
|
+
That's one way to do it. But this turns out to be a common use case. It's often desirable to have observers that are in effect just for the length of a single method call.
|
378
|
+
|
379
|
+
Here's how we might re-write the "poem" example with event subscriptions scoped to just the `#recite` call:
|
380
|
+
|
381
|
+
```ruby
|
382
|
+
require "brainguy"
|
383
|
+
|
384
|
+
class Poem
|
385
|
+
include Brainguy::Observable
|
386
|
+
def recite(&block)
|
387
|
+
with_subscription_scope(block) do
|
388
|
+
emit(:title, "Jabberwocky")
|
389
|
+
emit(:line, "'twas brillig, and the slithy toves")
|
390
|
+
emit(:line, "Did gyre and gimbal in the wabe")
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
class HtmlFormatter
|
396
|
+
include Brainguy::Observer
|
397
|
+
|
398
|
+
attr_reader :result
|
399
|
+
|
400
|
+
def initialize
|
401
|
+
@result = ""
|
402
|
+
end
|
403
|
+
|
404
|
+
def on_title(event)
|
405
|
+
@result << "<h1>#{event.args.first}</h1>"
|
406
|
+
end
|
407
|
+
|
408
|
+
def on_line(event)
|
409
|
+
@result << "#{event.args.first}</br>"
|
410
|
+
end
|
411
|
+
end
|
412
|
+
|
413
|
+
p = Poem.new
|
414
|
+
f = HtmlFormatter.new
|
415
|
+
p.recite do |events|
|
416
|
+
events.attach(f)
|
417
|
+
end
|
418
|
+
|
419
|
+
f.result
|
420
|
+
# => "<h1>Jabberwocky</h1>'twas brillig, and the slithy toves</br>Did gyre an...
|
421
|
+
|
422
|
+
```
|
423
|
+
|
424
|
+
In this example, the `HtmlFormatter` is only subscribed to poem events for the duration of the call to `#recite`. After that it is automatically detached.
|
425
|
+
|
426
|
+
### Replacing return values with events
|
427
|
+
|
428
|
+
*Interesting. I can see this being useful for more than just traditionally event-generating objects.*
|
429
|
+
|
430
|
+
Indeed it is! This turns out to be a useful pattern for any kind of method which acts as a "command".
|
431
|
+
|
432
|
+
For instance, let's imagine a fictional HTTP request method. Different things happen over the course of a request:
|
433
|
+
|
434
|
+
- headers come back
|
435
|
+
- data comes back (possibly more than once, if it is a streaming-style connection)
|
436
|
+
- an error may occur
|
437
|
+
- otherwise, at some point it will reach a successful finish
|
438
|
+
|
439
|
+
Let's look at how that could be modeled using an "event-ful" method:
|
440
|
+
|
441
|
+
```ruby
|
442
|
+
connection.request(:get, "/") do |events|
|
443
|
+
events.on(:header){ ... } # handle a header
|
444
|
+
events.on(:data){ ... } # handle data
|
445
|
+
events.on(:error){ ... } # handle errors
|
446
|
+
events.on(:success){ ... } # finish up
|
447
|
+
end
|
448
|
+
```
|
449
|
+
|
450
|
+
This API has some interesting properties:
|
451
|
+
|
452
|
+
- Notice how some of the events that are handled will only occur once (`error`, `success`), whereas others (`data`, `header`) may be called multiple times. The event-handler API style means that both singular and repeatable events can be handled in a consistent way.
|
453
|
+
- A common headache in designing APIs is deciding how to handle errors. Should an exception be raised? Should there be an exceptional return value? Using events, the client code can set the error policy.
|
454
|
+
|
455
|
+
*But couldn't you accomplish the same thing by returning different values for success, failure, etc?*
|
456
|
+
|
457
|
+
Not easily. Sure, you could define a method that returned `[:success, 200]` on success, and `[:error, 500]` on failure. But what about the `data` events that may be emitted multiple times as data comes in? Libraries typically handle this limitation by providing separate APIs and/or objects for "streaming" responses. Using events handlers makes it possible to handle both single-return and streaming-style requests in a consistent way.
|
458
|
+
|
459
|
+
*I don't like that blocks-in-a-block syntax*
|
460
|
+
|
461
|
+
If you're willing to wait until a method call is complete before handling events, there's an alternative to that syntax. Let's say our `#request` method is implemented something like this:
|
462
|
+
|
463
|
+
```ruby
|
464
|
+
class Connection
|
465
|
+
include Brainguy::Observable
|
466
|
+
|
467
|
+
def request(method, path, &block)
|
468
|
+
with_subscription_scope(block) do
|
469
|
+
# ...
|
470
|
+
end
|
471
|
+
end
|
472
|
+
|
473
|
+
# ...
|
474
|
+
end
|
475
|
+
```
|
476
|
+
|
477
|
+
In that case, instead of sending it with a block, we can do this:
|
478
|
+
|
479
|
+
```ruby
|
480
|
+
connection.request(:get, "/")
|
481
|
+
.on(:header){ ... } # handle a header
|
482
|
+
.on(:data){ ... } # handle data
|
483
|
+
.on(:error){ ... } # handle errors
|
484
|
+
.on(:success){ ... } # finish up
|
485
|
+
```
|
486
|
+
|
487
|
+
*How the heck does that work?*
|
488
|
+
|
489
|
+
If the method is called without a block, events are queued up in an {Brainguy::IdempotentEmitter}. This is a special kind of emitter that "plays back" any events that an observer missed, as soon as it is attached.
|
490
|
+
|
491
|
+
Then it's wrapped in a special {Brainguy::FluentEmitter} before being returned. This enables the "chained" calling style you can see in the example above. Normally, sending `#on` would return a {Brainguy::Subscription} object, so that wouldn't work.
|
492
|
+
|
493
|
+
The upshot is that all the events are collected over the course of the method's execution. Then they are played back on each handler as it is added.
|
494
|
+
|
495
|
+
*What if I **only** want eventful methods? I don't want my object to carry a long-lived list of observers around?*
|
496
|
+
|
497
|
+
Gotcha covered. You can use {Brainguy.with_subscription_scope} to add a temporary subscription scope to any method without first including {Brainguy::Observable}.
|
498
|
+
|
499
|
+
```ruby
|
500
|
+
class Connection
|
501
|
+
def request(method, path, &block)
|
502
|
+
Brainguy.with_subscription_scope(self) do
|
503
|
+
# ...
|
504
|
+
end
|
505
|
+
end
|
506
|
+
|
507
|
+
# ...
|
508
|
+
end
|
509
|
+
```
|
510
|
+
|
511
|
+
*This is a lot to take in. Anything else you want to tell me about?*
|
512
|
+
|
513
|
+
We've covered most of the major features. One thing we haven't talked about is error suppression.
|
514
|
+
|
515
|
+
### Suppressing errors
|
516
|
+
|
517
|
+
*Why would you want to suppress errors?*
|
518
|
+
|
519
|
+
Well, we all know that observers effect the thing being observed. But it can be nice to minimize that effect as much as possible. For instance, if you have a critical process that's being observed, you may want to ensure that spurious errors inside of observers don't cause it to crash.
|
520
|
+
|
521
|
+
*Yeah, I could see where that could be a problem.*
|
522
|
+
|
523
|
+
So there are some tools for setting a policy in place for what to do with errors in event handlers, including turning them into warnings, suppressing them entirely, or building a list of errors.
|
524
|
+
|
525
|
+
I'm not going to go over them in detail here in the README, but you should check out {Brainguy::ErrorHandlingNotifier} and {Brainguy::ErrorCollectingNotifier}, along with their spec files, for more information. They are pretty easy to use.
|
526
|
+
|
527
|
+
## FAQ
|
528
|
+
|
529
|
+
*Is this library like ActiveRecord callbacks? Or like Rails observers?*
|
530
|
+
|
531
|
+
No. ActiveRecord enables callbacks to be enabled at a class level, so that every instance is implicitly being subscribed to. Rails "observers" enable observers to be added with no knowledge whatsoever on the part of the objects being observed.
|
532
|
+
|
533
|
+
Brainguy explicitly eschews this kind of "spooky action at a distance". If you want to be notified of what goes on inside an object, you have to subscribe to that object.
|
534
|
+
|
535
|
+
*Is this an aspect-oriented programming library? Or a lisp-style "method advice" system?*
|
536
|
+
|
537
|
+
No. Aspect-oriented programming and lisp-style method advice are more ways of adding "spooky action at a distance", where the code being advised may have no idea that it is having foreign logic attached to it. As well as no control over *where* that foreign logic is applied.
|
538
|
+
|
539
|
+
In Brainguy, by contrast, objects are explicitly subscribed-to, and events are explicitly emitted.
|
540
|
+
|
541
|
+
*Is this a library for "Reactive Programming"?*
|
542
|
+
|
543
|
+
Not in and of itself. It could potentially serve as the foundation for such a library though.
|
544
|
+
|
545
|
+
*Is this a library for creating "hooks"?*
|
546
|
+
|
547
|
+
Sort of. Observers do let you "hook" arbitrary handlers to events in other objects. However, this library is not aimed at enabling you to create hooks that *modify* the behavior of other methods. It's primarily intended to allow objects to be *notified* of significant events, without interfering in the processing of the object sending out the notifications.
|
548
|
+
|
549
|
+
*Is this an asynchronous messaging or reactor system?*
|
550
|
+
|
551
|
+
No. Brainguy events are processed synchronously have no awareness of concurrency.
|
552
|
+
|
553
|
+
## Installation
|
554
|
+
|
555
|
+
Add this line to your application's Gemfile:
|
556
|
+
|
557
|
+
```ruby
|
558
|
+
gem 'brainguy'
|
559
|
+
```
|
560
|
+
|
561
|
+
And then execute:
|
562
|
+
|
563
|
+
$ bundle
|
564
|
+
|
565
|
+
Or install it yourself as:
|
566
|
+
|
567
|
+
$ gem install brainguy
|
568
|
+
|
569
|
+
## Usage
|
570
|
+
|
571
|
+
Coming soon!
|
572
|
+
|
573
|
+
## Contributing
|
574
|
+
|
575
|
+
1. Fork it ( https://github.com/avdi/brainguy/fork )
|
576
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
577
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
578
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
579
|
+
5. Create a new Pull Request
|