-
-
Save knewter/3561348 to your computer and use it in GitHub Desktop.
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 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.
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.
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.