Skip to content

Instantly share code, notes, and snippets.

@skwp
Last active December 15, 2015 18:09
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save skwp/5302250 to your computer and use it in GitHub Desktop.
Save skwp/5302250 to your computer and use it in GitHub Desktop.
Hexagonal extraction of a controller action from reverb.com, showing reuse between controller and Grape/Roar based API. This is sample code stripped down to the essentials and is not guaranteed to work as-is :)
class Reverb::Actions::WatchListing
def self.watch(user, product, listener)
if product.owner?(user)
listener.failure(I18n.t('flash.watchlist.error_own'))
else
Reverb::Analytics.track(user, :watch_product) # FIXME, this doesn't belong here
user.user_watch_products.create(:product_id => product.id)
listener.success
end
end
end
module Reverb
class Wants < Grape::API
post '/wants' do
class WatchListingResponder < SimpleDelegator
def success
{"success" => true}
end
def failure(message)
error!({ "error" => message}, 412)
end
end
Reverb::Actions::WatchListing.watch(current_user, Product.find(params[:id]), WatchListingResponder.new(self))
end
end
end
class Dashboard::Buying::WatchedProductsController < Dashboard::BaseController
def create
product = Product.find(params[:id])
Reverb::Actions::WatchListing.watch(current_user, product, WatchListingResponder.new(self))
redirect_to product_url(product)
end
private
class WatchListingResponder < SimpleDelegator
def success
flash[:success] = "Listing has been added to #{link_to_watchlist}."
end
def failure(message)
flash[:error] = message
end
def link_to_watchlist
view_context.link_to 'your watchlist', dashboard_buying_watched_products_url
end
end
end
describe Dashboard::Buying::WatchedProductsController do
stub_user
let(:product) { mock('product', :id => 1) }
before { Product.stub(:find).with("1") { product } }
describe "#create" do
let(:responder) { Dashboard::Buying::WatchedProductsController::WatchListingResponder.new(subject) }
it "watches the listing" do
Reverb::Actions::WatchListing.should_receive(:watch).with(user, product, anything)
post :create, :id => product.id
response.should redirect_to product_url(product)
end
it "handles error display" do
responder.failure("foo")
flash[:error].should == "foo"
end
it "handles success display" do
responder.success
flash[:success].should =~ /Listing has been added to.*your watchlist.*/
end
end
end
@skwp
Copy link
Author

skwp commented Apr 3, 2013

I'd like to move the analytics logic out of the business use case, however there is no good place for it. This is maybe the case where something like AOP would win. I can put a wiring to an analytics service listener in the controller, but then I'd have to duplicate it in the api, the console, rake tasks, and any other place that would care to reuse the business logic.

@mattwynne
Copy link

I'm not sure I fully got my head around your domain, but hopefully enough to offer you some useful insight.

It seems to me that Reverb::Analytics should be another listener. So your listener API needs to change so that it sends out enough information that the analytics code could become another consumer of that API.

If you're worried about duplicating the wiring, then maybe the thing that does the wiring deserves it's own abstraction, so that you can reuse it.

@skwp
Copy link
Author

skwp commented Apr 4, 2013

Thank you for the comments, @mattwynne. Agreed, the analytics should be a listener. As far as abstracting the wiring, that's definitely an idea. I mean essentially what i'm going for is that Reverb::Analytics would eventually be wired into pretty much every business class.

I'm also looking toward the future and implementing activity streams. Maybe the listener api is that the business case sends out a success with a sort of actor/action/properties stream which can then be picked up by listeners such as analytics, activity stream processors, etc.

Again the biggest question is where the wiring happens. Your statement about abstracting the wiring makes sense to me in principle but..would it look something like this? A base class that sets up default listeners and children then add to that...

class AnalyticsListener
  def success(actor, action, properties)
  end
end

class ActivityStreamListener
  def success(actor, action, properties)
  end
end


class BaseAction
  def initialize
    @listeners << [AnalyticsListener.new, ActivityStreamListener.new]
  end
end

class SomeBusinessCase < BaseAction
  def initialize(params, listeners)
   @listeners << listeners
  end
end

SomeBusinessCase.new(params, my_actual_listener)

@rinaldifonseca
Copy link

What about that approach: https://github.com/krisleech/wisper ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment