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
@jjulian
Copy link

jjulian commented Mar 8, 2012

I never like to open and extend a class that I also control. I'd vote to make the job classes bigger and do your best to reduce the smells of Feature Envy by putting some of the stuff in User (if it's re-usable outside of the job).

@nevans
Copy link
Author

nevans commented Mar 8, 2012

In the past, I've always just put the code in User at the first whiff of Feature Envy. But these methods really will only ever be used in this context by this task. I'd like to document that and keep the User class and user.rb from bloat. But it's definitely a tradeoff; this is more indirect than just sticking the methods on User.

@jjulian
Copy link

jjulian commented Mar 8, 2012

Yeah. Start by sticking all the methods in the job, and move them to user only if they can hold their weight there (ie, are used by someone else)

@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