Skip to content

Instantly share code, notes, and snippets.

@BiggerNoise
Created March 3, 2014 21:10
Show Gist options
  • Save BiggerNoise/9334673 to your computer and use it in GitHub Desktop.
Save BiggerNoise/9334673 to your computer and use it in GitHub Desktop.
Command Pattern at work
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
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
#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
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
@BiggerNoise
Copy link
Author

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.

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