-
-
Save nevans/2001570 to your computer and use it in GitHub Desktop.
# I have a new feature that primarily deals with a single class. It's a | |
# relatively small and self contained feature; I don't expect that other | |
# features will ever develop dependencies on it, but it will be highly | |
# dependant on the single class that it deals with. For various reasons, it | |
# isn't appropriate to make an Observer class. I'd like to keep all of the | |
# code pertaining to this feature highly cohesive (localized, in one file if | |
# possible). But it does make some specific demands of the class that it's | |
# coupled with. | |
module FooTask | |
# resque job: this job would be enqueued by cron or resque-scheduler... | |
# | |
# Please consider this usage of resque as just an implementation detail to | |
# illustrate the *actual* question, which is below. :) | |
module EnqueueAll | |
extend self | |
def queue; :priority end | |
def perform | |
User.each_needing_foo_task do |user| | |
Resque.enqueue FooTaskJob, user.id | |
end | |
end | |
end | |
# resque job per user, for parallelism and fault tolerance | |
# | |
# Please consider this usage of resque as just an implementation detail to | |
# illustrate the *actual* question, which is below. :) | |
module FooTaskJob # resque job | |
extend self | |
def queue; :priority end | |
def perform(user_id) | |
user = User.find(user_id) | |
user.do_the_foo_task! | |
end | |
end | |
# The following module is the crux of my question: | |
# | |
# In the past, I've simply placed all of the following methods directly into | |
# the User class, in user.rb, even though they are only used by this one | |
# task. In this approach, they are placed in this file and included into | |
# User. They are still "burdening" the User class, but they are only really | |
# used by this file, so they are placed in this file. Is this indirection | |
# (code that lives in User but not placed in user.rb) a good tradeoff? | |
# | |
# Alternately, I could selectively extend user objects at runtime with this | |
# code, only when needed (in the job modules below). If you have experience | |
# with that approach, can you explain the pragmatic pros/cons versus always | |
# including the module in User? And how could that approach work for the | |
# class (e.g. scope) methods? | |
# | |
# Another (very similar) approach is to create two decorators; one on the | |
# class for the finder methods and one on the object for the task methods. | |
# | |
# Another approach is to quit over-thinking this OO stuff, and make the job | |
# classes bigger and smelling of Feature Envy and Inappropriate Intimacy. | |
# I really don't like that approach, but is it substantively different from | |
# what I've done here? | |
# | |
# What do you think? | |
module UserExtensions | |
extend ActiveSupport::Concern | |
included do | |
# add scopes, etc, which are only ever used in this file... | |
end | |
module ClassMethods | |
def each_needing_foo_task | |
# SQL which is highly coupled to DB details of users... | |
where(foo, bar, baz).find_each do |user| | |
yield user | |
end | |
end | |
end | |
def do_the_foo_task! | |
# which is highly coupled to internal details of the user... | |
end | |
end | |
end |
class User < ActiveRecord::Base | |
include FooTask::UserExtensions | |
# ... | |
end |
I usually end up making a custom class that contains the behavior you're talking about moving to User. Like:
class UserFooTaskDoer
def self.foo_this(user)
user_foo_task = self.new(user)
user_foo_task.do_the_foo
user_foo_task
end
def initialize(user)
@user = user
end
end
# usage
UserFooTaskDoer.foo_this(User.first)
That preserves a lot of options for you later (in fact this pattern usually starts with something simpler, where UserFooTaskDoer is a module that just has a foo_this method, which I later grow into being its own object, mostly to help unit tests). I don't think there's a silver bullet here, but I am strongly against adding anything to your model objects that doesn't relate to individual object persistence. named_scopes are so rad that I use those, but any custom finder logic that I don't need/want to cooperate with named_scopes I will shove into some type of presenter.
To me models are the most vulnerable part of a rails app to code bloat.
It doesn't sound like it.
I like that idea. If I'm picturing it correctly, this would be cleaner and clearer I'm curious to get @subelsky's thoughts on this. He and I have discussed this topic before.