Skip to content

Instantly share code, notes, and snippets.

@nevans
Created March 8, 2012 15:42
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nevans/2001570 to your computer and use it in GitHub Desktop.
Save nevans/2001570 to your computer and use it in GitHub Desktop.
good or bad ruby on rails OO?
# 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
@scottmessinger
Copy link

  # Is this indirection
  # (code that lives in User but not placed in user.rb) a good tradeoff?

It doesn't sound like it.

  # 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.

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.

@subelsky
Copy link

subelsky commented Mar 9, 2012

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.

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