Sometimes I can stay content with the mental "right, so if it gets this message, this is what it will do". But other times one of two things happens. The most prevalent is I'm looking at the code that generates the message and asking "ok... where does that take me?" and its not clear or simple how to track it down. In the case of an ActiveRecord foo.save, I can be looking straight at the call and not realize that a callback is going to be invoked.
This is like the GOOS concept of a 'matchmaker' class whose sole responsibility is to wire up other objects. That way your lower-level objects become a DSL, and the matchmaker uses the DSL to compose a working program.@Perry maybe that's what you've been missing in the javascript apps?
Here is a sneak peek into the chapter on matchmaker in GOOS Book http://goo.gl/kXJqW
I'm wondering if the place where that information should live is in the functional tests, not the code. I have felt the same pain though.
The thing is, you can't have it both ways. If you want to see how the code all flows together, then the best way to do that is have it all in One Big Method. But there are other maintenance costs with that. So you break things up.My experience of breaking things up is not one of splintering, but of simplifying. But it does depend how you break things up. I've definitely got it wrong before, and that's when you find yourself hunting around trying to trace where messages go. I think one symptom of a good design is where you stop feeling the need to do that.cheers,Matt
Reading the code is knowing the internals.
Maybe using metasyntactic variable names was a poor choice. My point
was that to understand the one call, now you have to look at the two
other calls, and understand them... which shouldn't be necessary.
Maybe using metasyntactic variable names was a poor choice. My point
was that to understand the one call, now you have to look at the two
other calls, and understand them... which shouldn't be necessary.Perhaps another way to put this point might be that trying to understand what code does based on the names of methods is inherently fragile. It relies on the coder's subjective point of view, and their ability to reason correctly about the code they are writing.
The unreliable narrator is possibly an often seen, if unintended, feature of code, especially with respect to naming.Tests can suffer from this problem too, but this can be ameliorated by striving to have dead simple tests, composed of easy to understand chunks (not always possible in the codebase). Even better if the chunks that name, are themselves executable, e.g. rspec's 'executable natural language' approach, e.g. its(:foo) { should be_empty }
That said, I'm sure there's more to 'intention revealing code' (I've not yet read Jim's book) than naming and organising your code for what strikes the coder as human consumption, it seems to me that your code *should* be easy to understand. However tests bring an extra level of knowledge about the code that can't be gained otehrwise (for Steve's client of your code point), and hopefully drive the code in that direction.Cheers,
Nothing about this limits it to ONLY tests. Why would you work to achieve all of this in code?
Nothing about this limits it to ONLY tests. Why would you work to achieve all of this in code?Oops. That should say "Why wouldn't you..."
I agree entirely, and said as much in the last paragraph of my post. Easy to understand code is the best, but tests give you knowledge you can't get by that route alone.Nothing about this limits it to ONLY tests. Why would you work to achieve all of this in code?Oops. That should say "Why wouldn't you..."
You're right. I replied too quickly. Sorry for skipping your point there.
You're right. I replied too quickly. Sorry for skipping your point there.=|:-) (my attempt at a 'you are a gentleman' smiley)
This thread piqued my interest because I too have experienced stuff similar to 'callback hell' in recent months, and am constantly gripped by the urge to refine.It's interesting to think about how the structural features of a coding style (callback 'hell' vs promise 'heaven'?, tell don't ask, even goto vs gosub etc) impacts, in a general way, on our ability to reason about code written in that style. Does anyone know of any studies on this stuff, it strikes me that someone must have tried to do some empirical physiological research on it.
Cheers,ps. This really is a great list, and I'd like to thank everyone for their contributions.
It's a step down the ladder of abstraction.
This is what my controllers currently look like:
class ChatMessagesController < ActionController::Base
class ChatMessageCreatedResponse < SimpleDelegator
include StandardResponses
def success(message)
render json: ChatMessageSerializer.new(message)
end
end
def create
uc = PostChatMessage.new(params[:message], current_user)
uc.add_subscriber ChatMessageCreatedResponse.new(self)
uc.add_subscriber SearchIndexer.new, :success => :chat_message_created
uc.add_subscriber ChatMessageEmails.new(current_user)
uc.add_subscriber PusherNotifications.new(request.headers['X-Pusher-Socket']), :success => :chat_message_created
uc.call
end
end
This is working quite well for us so far, at a glance of the controller I can see the broad strokes of what's going to happen and can look at the individual subscribers to dig deeper into what messages they respond to (usually at least :success).
All my use-cases and the majority of the subscribers are out in a separate gem, as are all the serializers, so the Rails app is purely glue code whose controllers coordinate plugging together the appropriate use-cases and subscribers.
Some subscribers are generic, so we can re-name the message (:success => :chat_message_created) if we want to route it to a different method in the subscriber.
The StandardReponses module handles generic messages like :permission_denied, :not_found or :failure and generate the appropriate JSON and HTTP status codes and also has a default :success which sends a blank 204 response.
I currently mixin a PubSub module into all use cases, I'm tempted to extract that out to a separate Listener/PubSub object, but I don't feel too bad that use cases know about publishing for now.
Cheers.
Another thing I'm not clear on is where things like analytics would go. While I like the idea that the business case publishes events that others subscribe to, where does the wiring happen? The answer is not the controller because I want this to happen in all of: [controller, console, api, etc]. So do I just hardcode the analytics into the business action? Feels dirty. This feels like some aspect oriented programming territory.
Yan Pritzker
On Wednesday, April 3, 2013 10:47:59 AM UTC-5, y...@reverb.com wrote:Hi guys,I liked the idea of wrapping up the callbacks in a delegator class because it doesn't pollute the controller (or other layer) with random methods. Here's a sample hexagonal refactoring I've done which reuses some business logic across a rails controller and Grape API. Would love some feedback. One thing that is not good about this still is that my business logic still hits an AR method, which will be remedied soon.
thanks,
Yan Pritzker
--
I recently created a gem which wraps a pattern I've been using for a little while: https://github.com/krisleech/wisper
It subscribes 'listeners' to a publisher, but also allow subscribing of a block to a specific published event. This is less verbose than wrapping the controller in a decorator and passing it in as a regular subscriber.
If you scroll down to the ActionController section of the README you will see that instead of using decorating the controller I can use lambda's. I've kind of gone full circle with this as I started passing in lambda's to my service object, but got frustrated that I had to reassign instance var's for the view.
Then I tried SimpleDelegator around the controller, but:
1. Verbose, extra code, cluttering up the controller file
2. Private methods not accessible (e.g current_user)
3. You can't assign instance var's for use in the view, you must pass the var's render's local hash.
Finally with (much * 100) inspiration from others I can up with the code that allows both subscribing of either an object or a block, so I could have the best of both worlds.
- Kris.
P.S Would be interested in some feedback.
y...@reverb.com wrote:Hi guys,
I liked the idea of wrapping up the callbacks in a delegator class because it doesn't pollute the controller (or other layer) with random methods. Here's a sample hexagonal refactoring I've done which reuses some business logic across a rails controller and Grape API. Would love some feedback. One thing that is not good about this still is that my business logic still hits an AR method, which will be remedied soon.
http://gist.github.com/skwp/5302250
thanks,
Yan Pritzker
reverb.com | yanpritzker.com
--
You received this message because you are subscribed to the Google Groups "Objects on Rails" group.
To unsubscribe from this group and stop receiving emails from it, send an email to objects-on-rai...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
--
You received this message because you are subscribed to the Google Groups "Objects on Rails" group.
To unsubscribe from this group and stop receiving emails from it, send an email to objects-on-rai...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
Hi Kris,
On Sat, Apr 6, 2013 at 5:58 PM, Kris Leech <kris....@gmail.com> wrote:
> I recently created a gem which wraps a pattern I've been using for a little
> while: https://github.com/krisleech/wisper
I use the Publisher gem https://rubygems.org/gems/publisher for the same thing.
But I often find that refactoring can push the publication part down
deeper into the middle, away from direct contact with the controller.
So I find I more often use my own EventBus gem
https://rubygems.org/gems/event_bus for notifications when I don't
care which object raises them.
Kris,
Thanks for the link to wisper. I refactored one of my controllers and business actions to use that gem and am enjoying it. I especially like the block syntax because it removes the need for creating self shunts in the controller (the SimpleDelegator wrappers). Great job on that.
I am still trying to figure out a good way to inject default listeners into every action (in my case, specifically an analytics listener).
A few options:
1. all actions share a base class. each listener must call super in its initializer. cons: it's easy to forget to call super2. all actions include a base module. the module somehow taps into initialize to inject listeners immediately after the initialize call. cons: very monkeypatchy. don't even know if it's possible3. use template method pattern - base class provides a method called "execute" which injects the default listener chain, then calls some standardized private method in the child
don't really love any of those. Any more ideas?
Yan
On Sunday, April 7, 2013 9:10:46 AM UTC-5, Kris Leech wrote:On Sunday, 7 April 2013 11:35:20 UTC+1, Kevin Rutherford wrote:
Hi Kris,
On Sat, Apr 6, 2013 at 5:58 PM, Kris Leech <kris....@gmail.com> wrote:
> I recently created a gem which wraps a pattern I've been using for a little
> while: https://github.com/krisleech/wisper
I use the Publisher gem https://rubygems.org/gems/publisher for the same thing.But I often find that refactoring can push the publication part down
deeper into the middle, away from direct contact with the controller.
So I find I more often use my own EventBus gem
https://rubygems.org/gems/event_bus for notifications when I don't
care which object raises them.
event_bus was an influence on the implimentation within wisper :)
--
I recently created a gem which wraps a pattern I've been using for a little while: https://github.com/krisleech/wisper
It subscribes 'listeners' to a publisher, but also allow subscribing of a block to a specific published event. This is less verbose than wrapping the controller in a decorator and passing it in as a regular subscriber.
If you scroll down to the ActionController section of the README you will see that instead of using decorating the controller I can use lambda's. I've kind of gone full circle with this as I started passing in lambda's to my service object, but got frustrated that I had to reassign instance var's for the view.
Then I tried SimpleDelegator around the controller, but:
1. Verbose, extra code, cluttering up the controller file
2. Private methods not accessible (e.g current_user)
3. You can't assign instance var's for use in the view, you must pass the var's render's local hash.
Finally with (much * 100) inspiration from others I can up with the code that allows both subscribing of either an object or a block, so I could have the best of both worlds.
- Kris.
P.S Would be interested in some feedback.
Hi guys,
I liked the idea of wrapping up the callbacks in a delegator class because it doesn't pollute the controller (or other layer) with random methods. Here's a sample hexagonal refactoring I've done which reuses some business logic across a rails controller and Grape API. Would love some feedback. One thing that is not good about this still is that my business logic still hits an AR method, which will be remedied soon.
http://gist.github.com/skwp/5302250
thanks,
Yan Pritzker
reverb.com | yanpritzker.com
--
You received this message because you are subscribed to the Google Groups "Objects on Rails" group.
To unsubscribe from this group and stop receiving emails from it, send an email to objects-on-rails+unsubscribe@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
--
You received this message because you are subscribed to the Google Groups "Objects on Rails" group.
To unsubscribe from this group and stop receiving emails from it, send an email to objects-on-rails+unsubscribe@googlegroups.com.
y...@reverb.com wrote:Kris,Thanks :)
Thanks for the link to wisper. I refactored one of my controllers and business actions to use that gem and am enjoying it. I especially like the block syntax because it removes the need for creating self shunts in the controller (the SimpleDelegator wrappers). Great job on that.
I've been thinking about exactly the same thing last night and actually sketched up an idea in a branch: https://github.com/krisleech/wisper/commit/b4840c9653b6c80b5f4471133b9c0f109e4f27d2
I am still trying to figure out a good way to inject default listeners into every action (in my case, specifically an analytics listener).
A few options:
1. all actions share a base class. each listener must call super in its initializer. cons: it's easy to forget to call super2. all actions include a base module. the module somehow taps into initialize to inject listeners immediately after the initialize call. cons: very monkeypatchy. don't even know if it's possible3. use template method pattern - base class provides a method called "execute" which injects the default listener chain, then calls some standardized private method in the child
don't really love any of those. Any more ideas?
Basically I allow adding of "global subscribers" to Wisper which are automatically added to every publisher. I'm not sure it the right way to go yet.
What do you think?
Yan--
On Sunday, April 7, 2013 9:10:46 AM UTC-5, Kris Leech wrote:On Sunday, 7 April 2013 11:35:20 UTC+1, Kevin Rutherford wrote:
Hi Kris,
On Sat, Apr 6, 2013 at 5:58 PM, Kris Leech <kris....@gmail.com> wrote:
> I recently created a gem which wraps a pattern I've been using for a little
> while: https://github.com/krisleech/wisper
I use the Publisher gem https://rubygems.org/gems/publisher for the same thing.But I often find that refactoring can push the publication part down
deeper into the middle, away from direct contact with the controller.
So I find I more often use my own EventBus gem
https://rubygems.org/gems/event_bus for notifications when I don't
care which object raises them.
event_bus was an influence on the implimentation within wisper :)
You received this message because you are subscribed to the Google Groups "Objects on Rails" group.
To unsubscribe from this group and stop receiving emails from it, send an email to objects-on-rai...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
--
You received this message because you are subscribed to the Google Groups "Objects on Rails" group.
To unsubscribe from this group and stop receiving emails from it, send an email to objects-on-rai...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
--
You received this message because you are subscribed to a topic in the Google Groups "Objects on Rails" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/objects-on-rails/5dEvKAUmuV4/unsubscribe?hl=en.
To unsubscribe from this group and all its topics, send an email to objects-on-rai...@googlegroups.com.
--
You received this message because you are subscribed to a topic in the Google Groups "Objects on Rails" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/objects-on-rails/5dEvKAUmuV4/unsubscribe?hl=en.
To unsubscribe from this group and all its topics, send an email to objects-on-rai...@googlegroups.com.
Yes I was looking at the template method approach also. But the superclass also has to call some child method to actually "do the work" besides doing the listener invocations. The point of the base is not to do something with the listeners, it's to provide the listeners to the child.
Very roughly..(and using wisper's syntax)...
class Base
def self.executesubclass_instance = create_a_child_instancesubclass_instance.subscribe(default_listener_list)subclass_instance.do_actual_work # this is either a private method or a public oneendendclass SomeChild < Basedef do_actual_work#..this is where the fun happenspublish(:some_event)endendMeh...I like what Kris is suggesting with the GlobalListener work described here: https://github.com/krisleech/wisper/commit/b4840c9653b6c80b5f4471133b9c0f109e4f27d2Yes it's approaching DI framework land. However we have to be realistic about real world requirements. There often is a case for having a listener on every possible invocation of your code. Specifically Analytics, Emails, and etc come to mind. Things that are really core to your business. I want to make sure these listeners are invoked no matter whether I'm running my code from a controller, console, rake task, etc. Our business is built on analytics and omitting an analytics listener is just not an option. In this sense, having globally injected defaults feels pretty good to me.
(that could probably be simplified by not using class methods..please ignore my early morning ramblings). Kris, I would love to see that global listener branch merged.
On Tuesday, 9 April 2013 15:34:07 UTC+1, Yan Pritzker wrote:(that could probably be simplified by not using class methods..please ignore my early morning ramblings). Kris, I would love to see that global listener branch merged.I need this myself, so I will take a look tomorrow, I think I just need decide on the API and tidy up the specs. I also need to see what happens in Rails development environment, with class reloading. It should be okay because Wisper is a gem and the global listeners can be added in an initializer.The branch has an API for adding global listeners which looks like this:Wisper::GlobalRegistrations.add_listener(ActivityStream.new)^ It needs renaming to GlobalListeners, registrations is an internal thing really. Unless there is a better suggestion.
Hey Kris - I posted an issue on Wisper on github. Unless I'm missing something, I am finding the wispered code very difficult to test, because it's hard to tap into the objects with the blocks
You received this message because you are subscribed to a topic in the Google Groups "Objects on Rails" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/objects-on-rails/5dEvKAUmuV4/unsubscribe?hl=en.
To unsubscribe from this group and all its topics, send an email to objects-on-rai...@googlegroups.com.
On Tuesday, 9 April 2013 15:29:53 UTC+1, y...@reverb.com wrote:Yes I was looking at the template method approach also. But the superclass also has to call some child method to actually "do the work" besides doing the listener invocations. The point of the base is not to do something with the listeners, it's to provide the listeners to the child.
Very roughly..(and using wisper's syntax)...
class Base
def self.executesubclass_instance = create_a_child_instancesubclass_instance.subscribe(default_listener_list)subclass_instance.do_actual_work # this is either a private method or a public oneendendclass SomeChild < Basedef do_actual_work#..this is where the fun happenspublish(:some_event)endendMeh...I like what Kris is suggesting with the GlobalListener work described here: https://github.com/krisleech/wisper/commit/b4840c9653b6c80b5f4471133b9c0f109e4f27d2Yes it's approaching DI framework land. However we have to be realistic about real world requirements. There often is a case for having a listener on every possible invocation of your code. Specifically Analytics, Emails, and etc come to mind. Things that are really core to your business. I want to make sure these listeners are invoked no matter whether I'm running my code from a controller, console, rake task, etc. Our business is built on analytics and omitting an analytics listener is just not an option. In this sense, having globally injected defaults feels pretty good to me.My use cases for global listeners are Analytics and Activty Stream. These two listeners are added to all publishers and its a bit boring to keep doing `command.subscribe(ActivityListener.new)`. The flip side is you might forget to add a method for every event to the global listeners. As a safety net I could see making these listeners `respond_to?` always return true when under test, thus a missing method for an event would raise NoMethodError. Bit hacky, but might work.I don't know the specifics of what a DI framework is, other than it sounds like some way to ask for the correct class to use. Global listeners wouldn't be anything like that. Maybe global listeners isn't the right name...
--
You received this message because you are subscribed to the Google Groups "Objects on Rails" group.
To unsubscribe from this group and stop receiving emails from it, send an email to objects-on-rai...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
On 9 Apr 2013, at 21:40, Kris Leech <kris....@gmail.com> wrote:On Tuesday, 9 April 2013 15:29:53 UTC+1, y...@reverb.com wrote:
Yes it's approaching DI framework land. However we have to be realistic about real world requirements. There often is a case for having a listener on every possible invocation of your code. Specifically Analytics, Emails, and etc come to mind. Things that are really core to your business. I want to make sure these listeners are invoked no matter whether I'm running my code from a controller, console, rake task, etc. Our business is built on analytics and omitting an analytics listener is just not an option. In this sense, having globally injected defaults feels pretty good to me.My use cases for global listeners are Analytics and Activty Stream. These two listeners are added to all publishers and its a bit boring to keep doing `command.subscribe(ActivityListener.new)`. The flip side is you might forget to add a method for every event to the global listeners. As a safety net I could see making these listeners `respond_to?` always return true when under test, thus a missing method for an event would raise NoMethodError. Bit hacky, but might work.I don't know the specifics of what a DI framework is, other than it sounds like some way to ask for the correct class to use. Global listeners wouldn't be anything like that. Maybe global listeners isn't the right name...The danger of a DI framework is that the setup of the dependencies - in this case your Analytics or Activity Stream listeners - is done somewhere off to the side. That means that if I read the code, I'm unlikely to realise that the ActivityListener is being invoked.
What I'd suggest is that if you feel the need to have a domain concept that always includes an activity listener, then either have that object add the activity listener itself as it's constructed, or wrap it in a higher level concept that does that for you, and then use that higher-level concept everywhere instead. That way, you'll have code that anyone can read and understand what's going on.
--
You received this message because you are subscribed to a topic in the Google Groups "Objects on Rails" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/objects-on-rails/5dEvKAUmuV4/unsubscribe?hl=en.
To unsubscribe from this group and all its topics, send an email to objects-on-rai...@googlegroups.com.
But that puts the onus on every caller to remember to use the factory with the default listeners rather than invoking the command directly. I'm not sure we've won anything.Yes - decoupling the global listeners thing has the danger of hiding code and making things magical, but I think the alternative (having every caller aware that default listeners exist) feels too easy to screw up for the callers.
You received this message because you are subscribed to the Google Groups "Objects on Rails" group.
To unsubscribe from this group and stop receiving emails from it, send an email to objects-on-rai...@googlegroups.com.
For more options, visit https://groups.google.com/groups/opt_out.
On Sun, Nov 25, 2012 at 12:17 AM, Nikolay Sturm <goo...@erisiandiscord.de> wrote:
Hi,
I tried to apply the ideas of callbacks as described by Matt Wynne in
his "Hexagonal Rails" talks and Kevin Rutherford in his blog
(http://silkandspinach.net/2012/07/06/hexagonal-rails-hiding-the-finders/)
to some code at work for error handling.
The code has several layers (controller -> service -> use case object ->
ActiveRecord model). It is written with exceptions, which the controller
catches to render an error page. At points it felt as going in the
direction of using exceptions for flow control, which is the reason I
tried callbacks.
Callbacks, however didn't work any better IMHO. The code was littered
with failure callbacks and had I added success callbacks as well,
program flow would have been totally obfuscated.
I am wondering if I misunderstood something or if I just happened to use
callbacks in the wrong context.
cheers,
Nikolay
--
"It's all part of my Can't-Do approach to life." Wally