When an application grows organically it accrues features like a snowball rolling down a dirty hill. With a little careful engineering, this works great (since the snowball is actually quite willing to be guided) but sometimes you need to stop and rethink things, do a little refactoring. We’ve done this (many times) with all of our products.
Most recently, I’ve been working on some cleanups and optimizations to the person avatar and company logo code in Basecamp. The evolutionary snowball (for the avatar feature, at least) progressed something like this:
- In the beginning, there were no people avatars.
- Then one day, we added the feature. This introduced methods on the Person model like
avatar?
andavatar_path
andattach_avatar
. There were something like seven new methods that were added to Person as a result of this. - Some time later, David went and cleaned it up, moving those new methods to their own module. This was great, as it kept the definition of the Person model clean, and kept the avatar-related code separate.
This brings us to today. The code remains really clean, overall, because we’ve continued to follow the pattern of moving related methods into modules, and just including those modules in the base model.
However, there are two issues with this approach. First, when a class includes lots of modules (as some of ours do), it becomes hard to keep track of where different methods are defined, and even which methods are defined. This can lead to confusion when trying to find the definition of a method, and (worst case) can allow two or more methods with the same name, which is bad. There is no warning if you have methods with the same name in multiple modules, so it isn’t hard to wind up with bugs where you’ve clobbered a method with another. You wind up double-namespacing your methods (e.g., method names in the Avatar module being prefixed with avatar_
), to avoid collisions.
(Don’t misunderstand me, though. Modules are awesome. We use them a bunch, and love them. It just helps to be aware of their weaknesses, too.)
There are more issues with the module approach. What if you have different kinds of avatars (e.g., person versus group, or company)? You can’t subclass modules in Ruby, so you wind up finding other ways to solve it (like including modules in modules, etc.). Also, you can’t reference the avatar as a separate entity; instead, controllers that deal with avatars need to deal with the entity to which the avatar is attached.
Can anything be done about these issues? The avatar is a part of the Person model itself, after all, with attributes in the people table that define it. Can the avatar code be split out further, somehow?
Yes, it can. We’ll do this by making avatars a model of their own and using aggregation (rather than module inclusion, or inheritance) to pull it into Person. For example:
class Avatar
attr_reader :master
def initialize(master)
@master = master
end
def exists?
master.has_avatar?
end
def create(file)
master.update_attribute :has_avatar, true
# thumbnail and install the given avatar
end
def destroy
master.update_attribute :has_avatar, false
# delete avatar file
end
# etc.
end
class Person < ActiveRecord::Base
# ...
def avatar
@avatar ||= Avatar.new(self)
end
end
What are the benefits of this? Here are a few that occur to me:
- We can say things like “person.avatar.exists?” and “person.avatar.create(io)”.
- The Avatar class can be subclassed and extended, to support different avatar types.
- The Avatar class can be tested independently of Person.
- We don’t need to worry about methods on the Avatar model shadowing methods on the Person model.
- It’s much more obvious where to look for the definition of “destroy” in “person.avatar.destroy”.
Also, we can now pass the avatar itself around, separate from the person:
class AvatarsController < ApplicationController
before_filter :find_avatar
def create
@avatar.create(params[:avatar])
end
def destroy
@avatar.destroy
end
protected
def find_avatar
person = Person.find(params[:id])
@avatar = person.avatar
end
end
I definitely am not suggesting that this pattern replace the use of modules. Modules are very handy, and as I said before, we still use them extensively. But if you find yourself bumping into some of the limitations of modules in a specific instance, extracting the module in question into a model can provide some really nice benefits.
Karmen Blake
on 04 Feb 09I use modules extensively and can see where this pattern may help in some cases. Thanks!!
zero0x
on 04 Feb 09That was very useful article. Thank you.
Daniel
on 04 Feb 09I’m stuck in PHP-land, so it hurts me whenever I see pretty Ruby code. Modules are especially a sore point, as PHP has no functionality of that kind, and its terrible scoping rules lead to huge and complicated class definitions.
For that reason, I’ve been using the pattern you describe extensively, to extend classes without using inheritance.
It’s nice to see that the pattern makes sense even in a langauge that’s miles – lightyears – nicer than PHP.
Neeraj
on 04 Feb 09As you rightly alluded including too many modules itself kinds of tells that the model is being burneded with too many responsibilities.
Whenever I see a class including too many modules, I start thinking of creating a new class to take some of the responsibilities away from the original class.
As you mentioned the benefits include ability to pass this new class/object around and it can easily be tested. It also helps in keeping the original model test file small.
lexx
on 04 Feb 09That’s a great read, even though I’m not into ruby because I’m still digging in cake- I think the info I got here would be handy in the long run :)
Paul
on 04 Feb 09Can you do @avatar.create (params[:avatar]) even if Avatar class does not inherit from ActiveRecord::Base?
Does this strategy forces you to modify your database too?
Jamis
on 04 Feb 09@Paul, it’s far too easy to confuse the general term “model” with Rails’ ActiveRecord “model”. The former refer to any class, of any inheritance tree, that encapsulates data and logic without trying to present the data in any specific format. The latter refer specifically to models that are database-backed.
So, to answer your question, you can definitely do avatar.create(params[:avatar]), even if the Avatar class is not inherited from AR::Base. You just need to define your own create method, which I did.
To your second question: no. This pattern can be used even in the absence of a database. The Avatar class, you’ll see from the examples, refers to its ‘master’, which is the class that contains the backing data. The backing data itself may be persisted or not, and if persisted, may be persisted via database, memcached, HTTP cookies, or whatever else floats your boat.
andy
on 04 Feb 09Superb post. Modules are great, but they have a limit. The same can be said about inheritance. It’s clear from the examples to see the benefits of composition over both modules and inheritance. Also, nice to see some posts, especially for the coders in the crew! One question, would you use the avatar class to solve the company avatar issue as well?
Jamis
on 04 Feb 09@andy, yeah. Although companies in Basecamp don’t have avatars, they do have logos, and I’m giving the logos the same treatment as avatars (extraction to a class, and extending the model via composition). The two classes (Avatar and Logo) have a lot in common, and will probably end up either extending the same superclass, or sharing a module. TBD. :)
Nick
on 04 Feb 09Using your model as an example, do you have a recommendation of the best way to handle the relationship between an avatar model and the different models that have a relationship to that avatar? This is especially tricky when defining that a person has many avatars and that an avatar belongs to one model, such as a person or a company. I am guessing that your model uses the opposite approach in which a person has one avatar and the foreign key is within the person or company model.
I know of three different ways to do this, all of which have their own downsides. The first option is to have a object_type attribute that indicates the model that the avatar is tied to. I don’t know of anything in Rails that would support this sort of relationship. The most significant downside of this approach is that you cannot enforce foreign keys on the database. A second approach is to have separate PersonAvatar and CompanyAvatar models, each of which has a foreign key that is tied to a Person or a Company. This approach would require using a module or a parent class if it is to share functionality between individual avatar models. A third approach would be to create multiple foreign keys to each object that is associated with the Avatar model, so that the Avatar model would have both a person_id and company_id attribute, but only one of which would be used at once. This method also has its own downsides because it is not easy to immediately determine who the avatar belongs to.
Do you know of a better way to handle these relationships? I have been trying to find an optimal solution to this problem for a while.
Nick
on 04 Feb 09Also, to better demonstrate my point. Replace the Avatar model/module with a Notes module which needs to be attached to several other data models. The ability to add a note to another object should be re-usable without requiring lots of duplicated code. The note belongs to only one object of varying types.
Matt McKnight
on 04 Feb 09It’s interesting to me that I would have never considered using Modules for this sort of thing. I think that shows a bit of my object oriented coding background in so much as I am not used to including Modules, but aggregation seems natural.
Jamis
on 04 Feb 09Nick, your question goes way beyond the scope/topic of this article. :) In my case, a person has one avatar, AND the data for the avatar is stored in the people table itself, so the pattern I described fits very well.
Rails actually does support the “object_type” approach. It’s called “polymorphic associations.” Do some googling on that phrase together with rails and you should find plenty of documentation.
Jamis
on 04 Feb 09@Matt, be careful not to confuse “object oriented programming” (which can easily include module-based inheritance) with the specific training you had. More likely, you were trained in an OO language or environment that had no support for modules (Java, or C++). If your training had been in a language like Objective-C or Ruby, it would probably seem more normal to use them. But just because someone uses them, does not mean they don’t have OO background or experience. :) It’s just a different culture of OOP.
Nick
on 04 Feb 09Jamis, Thanks for telling me about the polymorphic associations feature of Rails. I was not aware that existed. I will definitely check that out. I still dislike this approach because of the lack of enforceable foreign keys, but it is otherwise probably the best way to go.
Yeah, this issue is definitely outside the scope of your article, but it is definitely an important implementation issue when applying this concept.
Ismael
on 04 Feb 09I’ve found that combining modules in a class for extended behaviour ends up in the mess and too many responsibilities you just described. Lately I’ve been using fewer modules, each adding their own little classes to the master class to extend by composition. In fact I try to limit the number of methods in a module as much as possible. So an AvatarBehaviour module would define an avatar method which in turn delegates to other classes as in your example. It’s like each module encapsulates it’s own little domain.
Chas Lemley
on 04 Feb 09Thanks for the article Jamis, as a beginner Rails programmer I enjoy these comments. I had one question.
Is the attr_reader :master, a reserved way of referring to the parent of the model, or any class for that matter? That’s a new one for me and I can’t seem to find any documentation.
Jamis
on 04 Feb 09@Chas, it’s a Ruby method (on the Module class). It’s short-hand for declaring a method that returns the value of an instance variable with the given name.
Benedict Eastaugh
on 04 Feb 09Chas: attr_reader (and its siblings, attr_writer and attr_accessor) are documented in the Ruby docs. There’s also a guide that may be helpful.
Behrang Javaherian
on 05 Feb 09Hi,
Continuous refactoring is one of the main elements of Extreme Programming. I think it worth to mention here that as code is getting bigger programmers tend to do less refactoring as they are not completely aware of the side effects of their changes and complete unit/functional and integration test suite will give the confidence to programmers to do the required refactoring. So comprehensive test suite is a vital factor for continuous refactoring.
Regards
Behrang Javaherian
Intuitive and powerful time tracking software
Rabbit
on 05 Feb 09Ha! That’s funny. I’ve used this pattern quite a few times over the past 18 months. Specifically with a Cart object.
class Person; def cart; @cart ||= Cart.new(self); end; end
This works really well for objects that do not extend AR but “belong to” another object.
(No I don’t code like that but pre and code do nothing here.)
Jose
on 05 Feb 09Keep these posts coming Mr. Jamis.
Thanks.
Matt McKnight
on 05 Feb 09@Jamis You guessed my object oriented background pretty well- c++ and Java (let’s not mention Modula-2), which is what I was referring to. I used Modules quite a bit in VB6, but I find myself living in the Java mental box quite frequently, even with the flexibility of Ruby at my fingertips.
charlie bowman
on 05 Feb 09I have yet too regret using a class to contain specific log and/or functional code. I almost always regret it when I dont.
Clay McIlrath
on 05 Feb 09I’m glad that i came across this post now, I’m new to RoR and my background with OOP is mostly C++ and i would have approached this in a totally undesirable fashion following my prior OOP experience. Thanks for sharing!
Jan
on 05 Feb 09Why not just Person has_one :avatar ? For performance?
Tim
on 05 Feb 09Great read. One question: In which directory does the Avatar model live? app/models? lib?
gustin
on 05 Feb 09We’ve also used a mixture of modules and aggregated objects to better maintain our Rails apps and it has helped tremendously in maintaining large applications.
I feel the hardest part of maintaining a large Rails app is breaking out of the MVC pattern (and directory structure). It is essential to incorporate modules, helper objects and to interweave design patterns with MVC.
We put our app specific business logic in /app/lib and use the default /lib for third-party libraries. The simple act of creating a /lib directory in the app folder makes the classes feel closer to and more part of our application.
This keeps the /app/models directory specific to Active Record (with 100s of tables its nice to keep it isolated to active record and data mapping).
The app/lib classes can be namespaced nicely and create a logic library of easily testable, reusable, and modular objects for use in your applications.
Great article Jamis!
Daniel Berger
on 05 Feb 09The multi-mixin problem has a few solutions. I created the “use” module specifically to deal with it, and there’s also _why’s “mixology” (or is it mixico? – I forget).
Anyway, one of the planned changes for Sapphire is to allow fine grained mixins, and emit warnings when method override occurs.
Trek Glowacki
on 05 Feb 09This reminds me a lot of the thoughtbot article, it’s all about behavior
I love how object composition let’s you decouple data persistence and behavior.
Fredrik
on 05 Feb 09How would you go about and saving the @avatar attribute on the person module? Marshaling it and saving it into the database or do you have some other solution for that?
Jamis
on 06 Feb 09@Jan, we don’t need has_one(:avatar) because an avatar is (essentially) a single database field indicating the existence of an avatar. The location on disk is computed based on the person’s id (among other things). No need to have a separate table to store a single boolean value. :)
@Tim, yeah, the Avatar model resides in app/models. It’s a model, even if it isn’t an ActiveRecord::Base subclass.
@Frederik, we don’t persist the avatar model. Or rather, the persistent data for the Avatar model actually lives in the Person model. The Avatar model is just a convenience for working with that subset of the Person model.
Bertie
on 09 Feb 09@Daniel
If your PHP looks worse than Ruby then you’re obviously doing it wrong. Don’t give up the day job, little man.
This discussion is closed.