Skip to content

Instantly share code, notes, and snippets.

@knewter
Created August 31, 2012 23:52
Show Gist options
  • Save knewter/3561348 to your computer and use it in GitHub Desktop.
Save knewter/3561348 to your computer and use it in GitHub Desktop.
command controller with a callback style
class FoodFightPlayCommandsController < LoggedInController
def create
command = FoodFightPlayCommand.new(params[:food_fight_play_command])
command.person_id = current_person.id
# Set up success / failure callbacks
command.on_success = method(:on_success)
command.on_failure = method(:on_failure)
command.execute!
end
def on_success(command)
redirect_to choose_food_games_food_fight_path, flash: { success: "Answered successfully." }
end
def on_failure(command)
flash.now[:error] = "Incorrect answer."
question_statistics = Games::QuestionStatisticsPresenter.new(command.question)
render '/games/food_fights/incorrect', locals: { food_fight_play_command: command, question_statistics: question_statistics }
end
end
require_relative './active_model_command'
require 'delegate'
class FoodFightPlayCommand < ActiveModelCommand
attr_accessor :question_id, :answer_id, :person_id, :on_success, :on_failure
validates :question_id, numericality: true, presence: true
validates :answer_id, numericality: true,
presence: true,
inclusion: { in: lambda{|o| o.answer_ids } }
validates :person_id, numericality: true, presence: true
delegate :body, to: :question, prefix: :question
def initialize params={}
@question_id = params[:question_id]
@answer_id = params[:answer_id].to_i
end
def question_repository
Games::Question.where(game_type: "FoodFight")
end
def question
question_repository.find(question_id)
end
def question_answer_repository
Games::QuestionAnswer.where(question_id: question_id)
end
def person_answer_repository
Games::PersonAnswer
end
def question_answers
question_answer_repository.all
end
def answer_options
question_answers.map do |qa|
answer = qa.answer
AnswerOption.new(answer).tap do |ao|
ao.chosen = (answer == chosen_answer)
ao.correct = (answer == correct_answer)
end
end
end
def correct_answer
correct_question_answer = question_answers.detect{|a| a.correct? }
return nil unless correct_question_answer
correct_question_answer.answer
end
def chosen_answer
return nil unless chosen_question_answer
chosen_question_answer.answer
end
def chosen_question_answer
question_answers.detect{|a| a.answer_id == answer_id}
end
def answer_ids
answer_options.map(&:id)
end
def correct?
chosen_answer == correct_answer
end
def person_answer_args
{
person_id: person_id,
question_answer_id: chosen_question_answer.id,
question_id: question_id
}
end
def execute!
return on_failure.call(self) unless valid?
answer = person_answer_repository.create(person_answer_args)
return on_success.call(self) if answer.valid? && correct?
return on_failure.call(self)
end
class AnswerOption < SimpleDelegator
attr_accessor :chosen, :correct
def chosen?
chosen == true
end
def correct?
correct == true
end
def incorrectly_chosen?
chosen && !correct?
end
def html_class
return 'incorrectly-chosen' if incorrectly_chosen?
return 'chosen' if chosen?
return 'correct' if correct?
return ''
end
end
end
@wallace
Copy link

wallace commented Sep 2, 2012

I like that this is 'tell, don't ask' but I'm not totally sold because it feels like FoodFightPlayCommand is now the recipient of some controller level concerns like redirecting.

But if I think about it in this way, "hey FoodFightPlayCommand, here's the message to send to me on success and here's the message to send to me on failure" I can get behind it a little more.

@knewter
Copy link
Author

knewter commented Sep 21, 2012

@wallace that's the whole point. FoodFightPlayCommand explicitly has zero knowledge of controller level concerns - otherwise I wouldn't be doing this. You could use this with any callbacks you wanted - the command just supports callbacks. This way I can build separable commands, and compose them into larger commands that might call out to sub-commands, or hook them up to an event-stream, or whatever I really feel like doing. The typical rails method of doing this sort of thing is just to have logic for this in the controller, or to do some kind of skullduggery with activerecord callbacks.

One silly thing that happened here is that in my example I actually left out the bit where I...save the answer. :-\

Updating the gist with the actual finished-ish version of this, which has a bunch more stuff in it and doesn't show the important point re: callbacks I was trying to make...it also needs to have at least one object extracted out of it still. Forthcoming.

@knewter
Copy link
Author

knewter commented Sep 21, 2012

So yeah, pushed the latest example - since it actually does the non-callbacky bits, it's substantially larger. However, I can use the commands to write up nice integration tests that just involve running the commands over and over, and I know that the controller's just going to run the command, so my 'driving a browser' style tests don't need to be verifying the entire system like most people's cukes end up doing...

And as I said, I can also compose these which is pleasant.

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