Created
March 8, 2012 15:42
-
-
Save nevans/2001570 to your computer and use it in GitHub Desktop.
good or bad ruby on rails OO?
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
# 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 |
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 User < ActiveRecord::Base | |
include FooTask::UserExtensions | |
# ... | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I usually end up making a custom class that contains the behavior you're talking about moving to User. Like:
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.