This is an example of how I combine interaction/service classes with Wisper event broadcasting in Rails.
In this example, I show a UsersController#create
API, a corresponding service object, and all the test code/listeners to make it all happen.
The outcome is:
- Concepts in your system ("Signing up a user", "Creating an order") have a single entry point in your codebase, vs. making raw ActiveRecord calls to
object.save
in dozens of places. - Since your concept has one entry point (the service class), you can easily
grep
for usage of it. - Stupid easy to attach listeners to the service class
- All event listeners are very small and easily unit tested
- Controllers have zero
if/else
logic and are very dumb - Unit tests stay fast
- Acceptance tests still exercise the whole system, including event listeners
- Plain old Ruby objects rule
Hey - I like the code and premise behind it 👍 I have some questions; isn't turning off broadcasting in tests (which are a clients of your services,
SignupUser
) in some way defeating the object of having publisher and subscriber which aren't hard coupled? Personally I prefer to subscribe listeners to an instance in the same context (e.g. controller) in which the service is run.I understand this can be inconvenient for cross-cutting concerns like gathering statistics where you want to be sure that the listener is subscribed is all cases where the service is used. In which case I'd normally reach for a global subscriber. I know you have reservations, perhaps using a global subscriber scoped to a service class would help give some meaning as to which listeners are subscribed to which publishers, e.g.
SignupUser.subscribe(AnalyticsListener.new)
. I do this in an initalizer in Rails apps.On the other hand I wouldn't globally subscribe
UserEmailListener
as this would not allow me to sign up a user without sending an email - which might be the case for things like importing data.Overall I'd be careful with the pattern of subscribing listeners within a publisher, it is convenient, but comes at a cost - revealed later as the application grows.