Created
March 3, 2014 21:10
-
-
Save BiggerNoise/9334673 to your computer and use it in GitHub Desktop.
Command Pattern at work
This file contains 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 AssignCaseCommand < Command | |
attribute :case, Case | |
attribute :owner, User | |
attribute :created_by, User | |
attribute :comments, String | |
attribute :distribute_at, DateTime | |
attribute :distribute_rule_name, String | |
attribute :require_initial, Boolean | |
validates :case, :owner, presence: true | |
def initialize(params) | |
self.attributes = { | |
case: params[:case] || Case.find(params[:case_id]), | |
owner: params[:owner] || (params[:owner_id] && User.find(params[:owner_id])), | |
created_by: params[:created_by] || (params[:created_by_id] && User.find(params[:created_by_id])), | |
comments: params[:comments], | |
distribute_at: params[:distribute_at], | |
distribute_rule_name: params[:distribute_rule_name], | |
require_initial: params.has_key?(:require_initial) ? params[:require_initial] : false | |
} | |
end | |
protected | |
def execute_self | |
return false unless valid? | |
@previous_owner = self.case.owner | |
self.case.owner = owner | |
self.case.distribute_at = distribute_at | |
self.case.distribute_rule_name = distribute_rule_name | |
self.case.save | |
end | |
def format_activity_comment | |
result = "Case assigned to #{owner.full_name}" | |
result << "; #{comments}" if comments | |
end | |
def prepare_sub_commands | |
sub_commands = [] | |
sub_commands << CreateActivityCommand.new(case: self.case, created_by: created_by, comments: format_activity_comment, activity_type: 'CASEASSIGNED') | |
# All non-closed tasks for the current owner of the case are assigned to the new owner of the case | |
open_tasks_for_current_owner = self.case.tasks.select { |t| t.owner == @previous_owner && t.status != TaskStatus::CLOSED } | |
unless @previous_owner.nil? | |
open_tasks_for_current_owner.each { |t| sub_commands.push AssignTaskCommand.new(task: t, owner: owner, created_by: created_by, comments: comments) } | |
end | |
# if there wasn't an initial contact task, create one if require_initial == true | |
if require_initial && !open_tasks_for_current_owner.any? { |t| t.task_type == Task::INITIAL_TASK_TYPE } | |
sub_commands.push CreateTaskCommand.new(case: self.case, owner: owner, task_type: Task::INITIAL_TASK_TYPE, created_by: created_by, comments: comments) | |
end | |
sub_commands | |
end | |
end |
This file contains 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 AssignTaskCommand < Command | |
attribute :task, Task | |
attribute :owner, User | |
attribute :due_at, DateTime | |
attribute :created_by, User | |
attribute :comments, String | |
attribute :distribute_at, DateTime | |
attribute :distribute_rule_name, String | |
attribute :assign_case, Integer | |
validates :task, :owner, presence: true | |
def initialize(params) | |
self.attributes = { | |
task: params[:task] || (params.has_key?(:task_id) && Task.find(params[:task_id])), | |
owner: params[:owner] || (params.has_key?(:owner_id) && User.find(params[:owner_id])), | |
due_at: params[:due_at] || Task.default_due_date, | |
created_by: params[:created_by] || (params[:created_by_id] && User.find(params[:created_by_id])), | |
distribute_at: params[:distribute_at], | |
distribute_rule_name: params[:distribute_rule_name], | |
comments: params[:comments], | |
assign_case: params[:assign_case] | |
} | |
end | |
protected | |
def format_activity_comment | |
result = "Task assigned to #{owner.full_name}" | |
result << "; #{comments}" if comments | |
end | |
def format_case_assignment_comment | |
result = 'Case assignment followed task assignment' | |
result << "; #{comments}" if comments | |
end | |
def execute_self | |
@previous_owner = task.owner | |
task.owner = owner | |
task.due_at = due_at if task.due_at.nil? || due_at.nil? || due_at > task.due_at | |
self.task.distribute_at = distribute_at | |
self.task.distribute_rule_name = distribute_rule_name | |
task.save | |
end | |
def prepare_sub_commands | |
cmds = [CreateActivityCommand.new(case: task.case, task: task, created_by: created_by, activity_type: 'TASKASSIGNED', comments: format_activity_comment)] | |
case | |
when assign_case == AssignCase::UNASSIGNED && task.case.owner.nil? | |
cmds << AssignCaseCommand.new(case: task.case, owner: owner, comments: format_case_assignment_comment) | |
when assign_case == AssignCase::ALWAYS | |
cmds << AssignCaseCommand.new(case: task.case, owner: owner, comments: format_case_assignment_comment) | |
end | |
cmds | |
end | |
end |
This file contains 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
#implement execute_self which returns true/false for success. Do not throw. | |
# optionally implement prepare_sub_commands and return an array of subcommands | |
class Command | |
include ActiveModel::Validations | |
include ActiveRecord::Serialization | |
include Virtus | |
def execute | |
return false unless valid? && execute_self | |
!!execute_sub_commands(prepare_sub_commands) | |
end | |
def execute! | |
(valid? && execute_self) || raise(ActiveRecord::RecordInvalid.new(self)) | |
execute_sub_commands!(prepare_sub_commands) | |
end | |
def attributes_except(*exclusions) | |
attr = attributes.dup | |
exclusions.flatten.each { |e| attr.delete(e) } | |
attr | |
end | |
protected | |
def prepare_sub_commands | |
[] | |
end | |
def execute_sub_commands(sub_commands) | |
sub_commands.each do |c| | |
break nil unless c.execute | |
end | |
end | |
def execute_sub_commands!(sub_commands) | |
sub_commands.each { |c| c.execute! } | |
end | |
end |
This file contains 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 CreateActivityCommand < Command | |
attr_reader :activity | |
attribute :created_by, User | |
attribute :parent_activity, Activity | |
attribute :member, Case | |
attribute :case, Case | |
attribute :task, Task | |
attribute :activity_type, String | |
attribute :comments, String | |
attribute :case_closed, Boolean | |
attribute :task_closed, Boolean | |
attribute :due_at, DateTime | |
attribute :allow_closed_case_activity, Boolean | |
attribute :allow_closed_task_activity, Boolean | |
validates :member, :activity_type, presence: true | |
validate :case_cannot_be_closed, :task_cannot_be_closed | |
def initialize(params = {}) | |
self.created_by = params[:created_by] || User.where(id: params[:created_by_id]).first | |
self.parent_activity = params[:parent_activity] # Lookups do not make sense here | |
self.member = params[:member] || Member.where(id: params[:member_id]).first | |
self.case = params[:case] || Case.where(id: params[:case_id]).first | |
self.task = params[:task] || Task.where(id: params[:task_id]).first | |
self.activity_type = params[:activity_type] || params[:activity_type_code] | |
self.comments = params[:comments] | |
self.case_closed = !!params[:case_closed] | |
self.task_closed = !!params[:task_closed] | |
self.due_at = params[:due_at] && params[:due_at].to_datetime | |
self.allow_closed_case_activity = params.has_key?(:allow_closed_case_activity) ? params[:allow_closed_case_activity] : false | |
self.allow_closed_task_activity = params.has_key?(:allow_closed_task_activity) ? params[:allow_closed_task_activity] : false | |
self.member ||= (self.case && self.case.member) | |
end | |
def execute_self | |
return false if invalid? | |
create_activity | |
self.case.last_activity = @activity if self.case | |
self.task.last_activity = self.case.last_activity if self.task | |
if case_closed && self.case | |
close_case self.case | |
elsif task_closed && self.task | |
close_task self.task | |
elsif task && due_at && due_at != task.due_at | |
postpone_task task | |
end | |
# Task may already be saved by close/postpone | |
self.task.save! if task && task.changed? | |
self.case.save! if self.case | |
true | |
end | |
private | |
STORED_ATTRIBUTES = [:member, :case, :task, :activity_type, :created_by, :parent_activity, :comments, :task_closed, :case_closed] | |
def create_activity | |
@activity = Activity.create!(attributes.select {|a,_| STORED_ATTRIBUTES.include?(a)}) | |
end | |
def close_case(c) | |
close_open_tasks c | |
c.closed_at = Time.current | |
c.closed_by = created_by | |
c.status = CaseStatus::CLOSED | |
c.save! | |
end | |
def close_open_tasks(c) | |
c.tasks.find_all { |t| t.status != TaskStatus::CLOSED }.each { |t| close_task t } | |
end | |
def close_task(t) | |
return unless t | |
t.closed_at = Time.current | |
t.closed_by = created_by | |
t.status = TaskStatus::CLOSED | |
t.save! | |
end | |
def postpone_task(t) | |
return unless t && due_at | |
t.due_at = due_at | |
t.save! | |
end | |
def case_cannot_be_closed | |
if self.case and !allow_closed_case_activity and self.case.status == CaseStatus::CLOSED | |
errors.add :case_id, 'cannot have new activity when its closed' | |
end | |
end | |
def task_cannot_be_closed | |
if self.task and !allow_closed_task_activity and self.task.status == TaskStatus::CLOSED | |
errors.add :task_id, 'cannot have new activity when its closed' | |
end | |
end | |
def persisted? | |
false | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Trail Guide
Overview
This represents three subclasses of a base command class and the base class. Commands can be composed of themselves and child commands (Composite).
There are three user visible models being created and/or operated upon by these commands: Case, Task, and Activity. Don't make the mistake of assuming that Activity is some sort of system logging entity. It is a user visible entity.
How it works
Similar to method calls in a popular web framework in Ruby, commands are constructed with a hash of options. The initializer of each command is responsible for parsing this hash into the attributes that make sense for the command. I thought it made the most sense to handle a wide variety of inputs since the parse to attributes is handled in exactly one place.
By virtue of ActiveModel::Validations and Virtus, these attributes can be easily declared and validated. Validations work just like ActiveModel validations because they are ActiveModel validations. Virtus adds nice type checking and coercion for attributes.
When a command is execute() or execute!(), the base class will ensure that the actual command object is validated, performs its own operation, constructs any sub commands, and executes those as well. Since it is a composite, the sub command is simply told to execute and the process repeats.
The execute_self and prepare_sub_commands are just ordinary Ruby methods, I thought that formatting the comment string that went into creating the sub commands obscured the logic in prepare_sub_commands, so I pulled that formatting into separate methods.