Created
July 16, 2012 21:06
-
-
Save rlivsey/3125054 to your computer and use it in GitHub Desktop.
Separation of concerns sketch
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
# when a task is saved we want to: | |
# * store in database | |
# * index in elasticsearch | |
# * notify resque to queue up sending emails | |
# * etc... | |
# | |
# when a task is completed by someone we want to: | |
# * update in the database | |
# * notify resque to send email to other assignees | |
# | |
# How to wire all this stuff up so the PORO's don't have to know | |
# about the database etc... | |
## Rails action | |
# I'm fairly happy with the controller | |
class TasksController < ApplicationController | |
# eg POST /tasks/123/complete | |
# or PUT / tasks/123/assignees/456 with {status: "completed"} | |
# etc... | |
def complete | |
# add responder as listener, or could subscribe etc... | |
# task could be the actual task, or pass through the ID | |
TaskCompleter.new(TaskCompletedResponder.new(self)).complete_task(task, person) | |
end | |
class TaskCompletedResponder < SimpleDelegator | |
def succeeded(task) | |
# render success JSON etc... | |
end | |
def denied(task) | |
# render permission error | |
end | |
def failed(task) | |
# render failure JSON etc... | |
end | |
end | |
end | |
## TaskCompleter | |
# this starts off ok, but I'm not 100% happy that it knows about indexing & persistence & emailing | |
# but if this doesn't know about these things, who does? | |
# can we inject such concepts into the completer without complicating the controller? | |
# IE how to hookup ports/adaptors at this level? | |
class TaskCompleter | |
def initialize(listener) | |
@listener = listener | |
end | |
def complete_task(task, person) | |
# if task_id instead of passing in the actual task | |
# unless TaskRepository.find(task_id) | |
# @listener.not_found # or raise exception, interface starting to get wide? | |
# return | |
# end | |
# permissions stuff here too maybe? | |
unless TaskPermissions.new(task).completable_by?(person) | |
@listener.denied(task) | |
return | |
end | |
# update the PORO, if good then trigger off persistence stuff? | |
if task.complete_by(person) | |
# do these belong here? where do persistence / services live? | |
# they could be registered as listeners / subscribers but it doesn't feel like | |
# it should be the controller/users job to wire these up | |
TaskRepository.task_completed(task, person) | |
# maybe these can be listeners in the controller? | |
# but again, should the user of this class know about wiring up search indexing? | |
Resque.enqueue(TaskCompletedEmailJob, task.id, person.id) | |
TaskSearch.index_task(task) | |
@listener.succeeded(task) | |
else | |
@listener.failed(task) | |
end | |
end | |
end | |
## PORO's | |
class Person | |
attr_accessor :name | |
attr_accessor :email | |
end | |
class Assignee | |
attr_accessor :person | |
attr_accessor :completed_at | |
def completed? | |
!! self.completed_at | |
end | |
end | |
class Task | |
attr_accessor :description | |
attr_accessor :assignees | |
attr_reader :completed_at | |
def initialize(description, assignees) | |
@description = description | |
@assignees = assignees | |
end | |
def completed? | |
self.completed_at? || @assignees.all?{|a| a.completed? } | |
end | |
# obviously this can be refined | |
# return true/false on success? | |
# raise exceptions for AlreadyCompleted or NotAssignee etc..? | |
# but general idea is that it updates the in memory representation of the task | |
# and knows not a jot about persistence etc... | |
def complete_by(person, time=Time.now) | |
return if completed? | |
if assignee = @assignees.detect{|a| a.person == person } | |
return if assignee.completed? | |
assignee.completed_at = time | |
# if this were ActiveRecord, we'd probably persist now | |
# assignee.save | |
end | |
if completed? | |
self.completed_at = time | |
# again if this were AR we could save | |
# self.save | |
# we could have after_save callbacks to index in ElasticSearch | |
# and trigger Resque job to send email, but it's not this classes responsibility | |
end | |
true | |
end | |
end | |
## Persistence / Services | |
# these are illustrative more than anything | |
# general concept of persistence, searching, email/queuing all handled by their own units of code | |
# DAO/DataMapper style pattern? | |
class TaskRepository | |
include Repository::Mongo | |
collection_name 'tasks' | |
# ... | |
def self.find(id) | |
data = collection.find_one( ... ) | |
# assemble the task, use Virtus to clean this up etc... | |
Task.new(data["description"], data["assignees"].map{|a_data| Assignee.new(a_data) }) | |
end | |
def self.task_completed(task, assignee, time) | |
collection.update({_id: task.id, 'assignees._id' => assignee.id}, | |
{:$set => {"assignees.$.completed_at": time}}) | |
end | |
# ... | |
end | |
class TaskSearch | |
include Search | |
index_name 'tasks' | |
# ... | |
def self.index_task(task) | |
index.store(task.id, json_for(task)) | |
end | |
# ... | |
end | |
class TaskCompletedEmailJob | |
def self.perform(task_id, person_id) | |
# send the email to everyone except the person who marked as complete | |
task = TaskRepository.find(task_id) | |
task.assignees.each do |assignee| | |
next if assignee.person_id == person_id | |
TaskMailer.new(task, assignee).deliver | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment