Created
August 31, 2012 23:52
-
-
Save knewter/3561348 to your computer and use it in GitHub Desktop.
command controller with a callback style
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
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
@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.