README.md in wisper-1.5.0 vs README.md in wisper-1.6.0
- old
+ new
@@ -1,19 +1,20 @@
# Wisper
-Wisper is a Ruby library for decoupling and managing the dependencies of your
-Ruby objects using Pub/Sub.
+*A micro library providing Ruby objects with Publish-Subscribe capabilities*
[![Gem Version](https://badge.fury.io/rb/wisper.png)](http://badge.fury.io/rb/wisper)
[![Code Climate](https://codeclimate.com/github/krisleech/wisper.png)](https://codeclimate.com/github/krisleech/wisper)
[![Build Status](https://travis-ci.org/krisleech/wisper.png?branch=master)](https://travis-ci.org/krisleech/wisper)
[![Coverage Status](https://coveralls.io/repos/krisleech/wisper/badge.png?branch=master)](https://coveralls.io/r/krisleech/wisper?branch=master)
-Wisper was extracted from a Rails codebase but is not dependant on Rails.
+* Decouple core business logic from external concerns in Hexagonal style architectures
+* Use as an alternative to ActiveRecord callbacks and Observers in Rails apps
+* Connect objects based on context without permanence
+* React to events synchronously or asynchronously
-It is commonly used as an alternative to ActiveRecord callbacks and Observers
-to reduce coupling between data and domain layers.
+Note: Wisper was originally extracted from a Rails codebase but is not dependant on Rails.
## Installation
Add this line to your application's Gemfile:
@@ -27,202 +28,145 @@
to subscribed listeners. Listeners subscribe, at runtime, to the publisher.
### Publishing
```ruby
-class MyPublisher
+class CancelOrder
include Wisper::Publisher
+
+ def call(order_id)
+ order = Order.find_by_id(order_id)
+
+ # business logic...
- def do_something
- # ...
- publish(:done_something)
+ if order.cancelled?
+ broadcast(:cancel_order_successful, order.id)
+ else
+ broadcast(:cancel_order_failed, order.id)
+ end
end
end
```
-When a publisher broadcasts an event it can pass any number of arguments which
-are to be passed on to the listeners.
+When a publisher broadcasts an event it can include number of arguments.
-```ruby
-publish(:done_something, 'hello', 'world')
-```
+The `broadcast` method is also aliased as `publish` and `announce`.
+You can also include `Wisper.publisher` instead of `Wisper::Publisher`.
+
### Subscribing
-#### Listeners
+#### Objects
-Any object can be a listener and only receives events it can respond to.
+Any object can be subscribed as a listener.
```ruby
-my_publisher = MyPublisher.new
-my_publisher.subscribe(MyListener.new)
-```
+cancel_order = CancelOrder.new
-#### Blocks
+cancel_order.subscribe(OrderNotifier.new)
-Blocks are subscribed to single events only.
+cancel_order.call(order_id)
+```
+The listener would need to implement a method for every event it wishes to receive.
+
```ruby
-my_publisher = MyPublisher.new
-my_publisher.on(:done_something) do |publisher|
- # ...
+class OrderNotifier
+ def cancel_order_successful(order_id)
+ order = Order.find_by_id(order_id)
+
+ # notify someone ...
+ end
end
```
-### Asynchronous Publishing
+#### Blocks
+Blocks can be subscribed to single events and can be chained.
+
```ruby
-my_publisher.subscribe(MyListener.new, async: true)
+cancel_order = CancelOrder.new
+
+cancel_order.on(:cancel_order_successful) { |order_id| ... }
+ .on(:cancel_order_failed) { |order_id| ... }
+
+cancel_order.call(order_id)
```
-Please refer to
-[wisper-celluloid](https://github.com/krisleech/wisper-celluloid) or
-[wisper-sidekiq](https://github.com/krisleech/wisper-sidekiq).
+### Handling Events Asynchronously
-### ActiveRecord
-
```ruby
-class Bid < ActiveRecord::Base
- include Wisper::Publisher
+cancel_order.subscribe(OrderNotifier.new, async: true)
+```
- validates :amount, presence: true
+Wisper has various adapters for asynchronous event handling, please refer to
+[wisper-celluloid](https://github.com/krisleech/wisper-celluloid),
+[wisper-sidekiq](https://github.com/krisleech/wisper-sidekiq) or
+[wisper-activejob](https://github.com/krisleech/wisper-activejob).
- def commit(_attrs = nil)
- assign_attributes(_attrs) if _attrs.present?
- if valid?
- save!
- publish(:create_bid_successful, self)
- else
- publish(:create_bid_failed, self)
- end
- end
-end
-```
+Depending on the adapter used the listener may need to be a class instead of an object.
### ActionController
```ruby
-class BidsController < ApplicationController
- def new
- @bid = Bid.new
- end
-
+class CancelOrderController < ApplicationController
+
def create
- @bid = Bid.new(params[:bid])
+ cancel_order = CancelOrder.new
- @bid.subscribe(PusherListener.new)
- @bid.subscribe(ActivityListener.new)
- @bid.subscribe(StatisticsListener.new)
+ cancel_order.subscribe(OrderMailer, async: true)
+ cancel_order.subscribe(ActivityRecorder, async: true)
+ cancel_order.subscribe(StatisticsRecorder, async: true)
- @bid.on(:create_bid_successful) { |bid| redirect_to bid }
- @bid.on(:create_bid_failed) { |bid| render action: :new }
+ cancel_order.on(:cancel_order_successful) { |order_id| redirect_to order_path(order_id) }
+ cancel_order.on(:cancel_order_failed) { |order_id| render action: :new }
- @bid.commit
+ cancel_order.call(order_id)
end
end
```
-A full CRUD example is shown in the [Wiki](https://github.com/krisleech/wisper/wiki).
+### ActiveRecord
-### Service/Use Case/Command objects
+If you wish to publish directly from ActiveRecord models you can broadcast events from callbacks:
-A Service object is useful when an operation is complex, interacts with more
-than one model, accesses an external API or would burden a model with too much
-responsibility.
-
```ruby
-class PlayerJoiningTeam
+class Order < ActiveRecord::Base
include Wisper::Publisher
+
+ after_commit :publish_creation_successful, on: :create
+ after_validation :publish_creation_failed, on: :create
- attr_reader :player, :team
-
- def initialize(player, team)
- @player = player
- @team = team
- end
-
- def execute
- membership = Membership.new(player, team)
-
- if membership.valid?
- membership.save!
- email_player
- assign_first_mission
- publish(:player_joining_team_successful, player, team)
- else
- publish(:player_joining_team_failed, player, team)
- end
- end
-
private
- def email_player
- # ...
+ def publish_creation_successful
+ broadcast(:order_creation_successful, self)
end
- def assign_first_mission
- # ...
+ def publish_creation_failed
+ broadcast(:order_creation_failed, self) if errors.any?
end
end
```
-### Example listeners
+There are more examples in the [Wiki](https://github.com/krisleech/wisper/wiki).
-These are typical app wide listeners which have a method for pretty much every
-event which is broadcast.
+## Global Listeners
-```ruby
-class PusherListener
- def create_thing_successful(thing)
- # ...
- end
-end
+Global listeners receive all broadcast events which they can respond to.
-class ActivityListener
- def create_thing_successful(thing)
- # ...
- end
-end
+This is useful for cross cutting concerns such as recording statistics, indexing, caching and logging.
-class StatisticsListener
- def create_thing_successful(thing)
- # ...
- end
-end
-
-class CacheListener
- def create_thing_successful(thing)
- # ...
- end
-end
-
-class IndexingListener
- def create_thing_successful(thing)
- # ...
- end
-end
-```
-
-## Global listeners
-
-If you become tired of adding the same listeners to _every_ publisher you can
-add listeners globally. They receive all broadcast events which they can respond
-to.
-
-Global listeners should be used with caution, the execution path becomes less
-obvious on reading the code and of course you are introducing global state and
-'always on' behaviour. This may not desirable.
-
```ruby
Wisper.subscribe(MyListener.new)
```
In a Rails app you might want to add your global listeners in an initalizer.
Global listeners are threadsafe.
-### Scoping to publisher class
+### Scoping by publisher class
You might want to globally subscribe a listener to publishers with a certain
class.
```ruby
@@ -230,11 +174,11 @@
```
This will subscribe the listener to all instances of `MyPublisher` and its
subclasses.
-Alternatively you can also do exactly the same with a publisher class:
+Alternatively you can also do exactly the same with a publisher class itself:
```ruby
MyPublisher.subscribe(MyListener.new)
```
@@ -247,13 +191,14 @@
# do stuff
end
```
Any events broadcast within the block by any publisher will be sent to the
-listeners. This is useful if you have a child object which publishes an event
-which is not bubbled down to a parent publisher.
+listeners.
+This is useful for capturing events published by objects to which you do not have access in a given context.
+
Temporary Global Listeners are threadsafe.
## Subscribing to selected events
By default a listener will get notified of all events it can respond to. You
@@ -305,17 +250,13 @@
report_creator.subscribe(MailResponder.new, on: :create_report_failed,
with: :failed)
```
-## Chaining subscriptions
+You could also alias the method within your listener, as such
+`alias successful create_report_successful`.
-```ruby
-post.on(:success) { |post| redirect_to post }
- .on(:failure) { |post| render action: :edit, locals: { post: post } }
-```
-
## RSpec
### Broadcast Matcher
```ruby
@@ -344,57 +285,64 @@
publisher.execute
```
### Stubbing publishers
-Wisper comes with a method for stubbing event publishers so that you can create
-isolation tests that only care about reacting to events.
+You can stub publishers and their events in unit (isolated) tests that only care about reacting to events.
Given this piece of code:
```ruby
-class CodeThatReactsToEvents
- def do_something
+class MyController
+ def create
publisher = MyPublisher.new
+
publisher.on(:some_event) do |variable|
return "Hello with #{variable}!"
end
+
publisher.execute
end
end
```
You can test it like this:
```ruby
require 'wisper/rspec/stub_wisper_publisher'
-describe CodeThatReactsToEvents do
+describe MyController do
context "on some_event" do
before do
stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo")
end
it "renders" do
- response = CodeThatReactsToEvents.new.do_something
+ response = MyController.new.create
expect(response).to eq "Hello with foo!"
end
end
end
```
-This becomes important when testing, for example, Rails controllers in
-isolation from the business logic. This technique is used at the controller
-layer to isolate testing the controller from testing the encapsulated business
-logic.
+This is useful when testing Rails controllers in isolation from the business logic.
You can use any number of args to pass to the event:
```ruby
stub_wisper_publisher("MyPublisher", :execute, :some_event, "foo1", "foo2", ...)
```
See `spec/lib/rspec_extensions_spec.rb` for a runnable example.
+
+## Clearing Global Listeners
+
+If you use global listeners in non-feature tests you _might_ want to clear them
+in a hook to prevent global subscriptions persisting between tests.
+
+```ruby
+after { Wisper.clear }
+```
## Compatibility
Tested with MRI 1.9.x, MRI 2.0.0, JRuby (1.9 and 2.0 mode) and Rubinius (1.9
mode).