A simple alternative to namespaced models

— May 6, 2008 at 08:17 PDT


A project I'm working on now is up to 57 model classes and is still growing. That's a lot of classes - welcome to domain modeling. In my opinion, the number of classes is a fair tradeoff that keeps each class simple enough to understand. In some ways it moves complexity out of the model class internals into the inheritance hierarchy, which is an important part of object-oriented design. I've worked on projects with many more model classes than that too. (Financial applications seem to require a lot of classes to model the complicated workflow and permission systems.)

The place where it starts to get hard to manage is when I look at the file system and see so many files in one directory. My brain usually starts to overload when I see more than a dozen or so classes in a directory. My first inclination is to throw some related class files into a subdirectory. The problem is that the standard way to do that in Rails is to put those models classes in a namespace (module). Rails used to have big problems with namespaced models, mainly with the dependency auto-loading code that finds class files based on the model class name. Most of those problems have been fixed, but there are still some usability issues with namespaced models.

The first problem, and the biggest one, is foxy fixtures. I like foxy fixtures a lot and the feature makes fixtures much easier to work with. But namespaced models just don't work with foxy fixtures. To get them to sort of work, you have to insert calls to Fixtures.identify anywhere you'd use a normal association. You also can't use the fixture helpers in your tests, so you have to do an explicit find, again using Fixtures.identify. It's pretty ugly, and it's actually worse than it used to be with pre-foxy fixtures.

The other problem is setting up associations. You have to use the :class_name option to tell the association how to find the model class. Again, it's a bit ugly, but at least it works well once you tell the association which class to use.

There's also an issue with STI and polymorphic class names being saved un-namespaced, but it looks like that's been fixed on edge now.

So what's a developer to do? The workaround is really quite simple. Don't use namespaces.

By default, Rails looks for models only in the app/models directory, and in subdirectories of app/models for namespaced models. But if you just want to put the model class files in a subdirectory, all you have to do is add that subdirectory to the load paths. If you do that, you don't need to put the model class in a namespace.

In your environment.rb file you'll find a commented out line with an example of setting config.load_paths (in the initializer run block). Uncomment and adjust that line, or if it's not there, just add the following line.

config.load_paths += %W( #{RAILS_ROOT}/app/models/role #{RAILS_ROOT}/app/models/user )

That incantation adds the role and user subdirectories to the load paths collection, letting Rails find the various role and user class files. No namespacing required, and it works with or without inheritance.

UPDATE: It's true, there are no original ideas. Boy meets girl, boy loses girl, boy gets girl back. Man against nature, man against man, man against himself. Same thing for blog posts. Chris Wanstrath blogged this up last year in a post on errtheblog. Still, good ideas like good stories are worth a retelling now and then...

12 commentsactiverecord, fixtures, rails

Comments
  1. Jack Nutting2008-05-06 09:22:44

    Great tip! I'm using STI in the system I'm working on, and some of those inheritance trees are getting awfully big; This presents a nice way to pack away those subclasses into directories of their own, without the variety of annoyances that come with namespaced models.

  2. Lourens Naude2008-05-06 09:38:20

    To compliment the above suggestion ... when the models themselves grow too large, here's a snippet to refactor those into concerns ( credit to technoweenie ) :

    class << ActiveRecord::Base
    
      # Macro to aid in refactoring large Models into concerns
      def concerned_with(*concerns)
        concerns.each do |concern|
          require_dependency "#{name.underscore}/#{concern}"
        end
      end
    
    end
    
    
    class Order < AR::Base
       concerned_with :payment, :dispatch, :discounts #etc.
    end
    
    
    class Order
      #payment related methods
    end
    
    class Order
      #dispatch methods
    end
    
  3. Matt Ittigson2008-05-06 09:52:11

    Doesn't this break if you have the same model name in two different sub-directories?

  4. Geoffrey Grosenbach2008-05-06 10:34:00

    That's a great tip!

    I use STI all over the place and it would have made more sense to use namespaces with STI, except for the technical difficulties up until now. Subdirectories will make it more manageable.

    The only caveat is that it fools autotest, but I'm sure that it could be configured to match subdirectories, too.

  5. Josh Susser2008-05-06 10:37:13

    @Matt: Good point, and the answer is yes. If you have the same name for two models, you'll need to use namespacing, not just separate subdirectories. Caveat emptor!

  6. jm2008-05-06 13:36:19
  7. Chris Wanstrath2008-05-06 13:59:00

    I actually stole this post from Josh before he wrote it. Sorry Josh!

  8. Tom Harrison2008-05-06 14:04:47

    Thanks for this tip. It's a good one for a number of cases.

    As you point out, its main purpose is to help the logical organization of files (it's not real name spacing). I don't think 57 models is particularly large for any real application, though, and I think this is an area Rails needs to get better at.

    One of the significant benefits of Rails is that application directory structure is defined. Doing stuff like moving things around and tricking Rails means we're out in the brave new world again. Also, this could be potentially confusing to other developers (who might assume that the subdirs are modules).

    What would be cool would be if Rails let us declaratively link modules such that all the cool ORM stuff still worked without resorting to :class => 'foo", and so on. Today, there's a pretty significant penalty for refactoring (model, view, controller, helper, app helper, test, fixtures, migrations ... everything needs to change if you move or rename something.

    It should be really easy to re-factor an application into modules as it grows, but it currently seems like it gets harder, meaning the great productivity we get out of the box gets bogged down in administrivia as the app gets larger.

  9. Nuwan Chaturanga2008-05-07 21:12:46

    Another great tip!

    There is no wrong telling good old stories again and again. We tell the good stories to our children that our parents told us. That's how the generations get built.

  10. Julio Santos2008-05-08 14:31:15

    Loved this one. I bumped into something though. It looks like having a model with the same name as the directory it contains, causes some grief. Even worse, the tests pass just fine. Things only fail in development.

    I had to rename some of my directories to avoid name clashes.

  11. Yossef2008-05-14 17:32:50

    Lourens,

    It took me a little while to understand that example, especially with the lack of file names/paths. (I'm assuming those last two snippets are in app/models/order/payment.rb and app/models/order/dispatch.rb).

    It looks like it could definitely help out when dealing with domains involving lots of models.

  12. Yehuda Katz2008-05-31 03:13:59

    Hey Josh,

    Just FYI, this is a direct consequence of the black-magic that is Rails dependency loading. Merb, which uses deterministic loading, does not have this problem. All files under models, including subdirectories of models, are loaded at boot time, so there is never any need to put files in a special place for them to be picked up by the magical black box.

    I wonder if there would be interest in a plugin/patch for Rails which replaced const_missing loading with deterministic loading.

Sorry, comments for this article are closed.