Subclassing vs include

Eric Hodel | Wed, 23 Aug 2006 18:15:00 GMT

Posted in , ,

In memcached Basics for Rails Rob Sanheim asked:

[W]hy make the cached model a class to extend instead of a module? Whether or not a model is cached should be an implementation detail, and shouldn’t define the hierarchy for a class. I know I would rather not use the power of (single!) inheritance just to cache something, when a mix-in should be plenty powerful to do it.

The short answer is:

Using a class the correct way to overlay features on top of another class.

Here’s my long answer:

When you use a class you get super, and super is a beautiful thing. It automatically walks your class’ ancestors and calls the right method in the right order.

A module doesn’t have this property so you can’t use it to overlay features on top of a class. The class’ implementation will always be called before the module’s implementation.

Now you’re going to say something about using alias to shuffle methods around. You could do that, but you’ll have to do this for each method you want to overlay (five in CachedModel) which involves lots of extra typing “do_the_thingy_without_the_stuff” that you could have had for free (and more-prettily) with “super”.

You also get another problem that may cause subtle bugs. When you use alias to overlay features the order of execution is dependent upon the order the files are required in! Intentionally writing code where the behavior may change file load order gives me the heebie-jeebies.

Having to do all that work to get the benefits of a subclass tells me that a module isn’t powerful enough to do what a subclass can, so a module isn’t the right way to add a cache.

I avoid* using modules to overlay features of a class, but do use them to add orthogonal or complementary features. Typically when I write a module it ends up being used like Comparable, Enumerable or Singleton. When I need to do something invasive a subclass is better.

Finally, making the argument that adding caching to ActiveRecord::Base via a subclass shouldn’t define the inheritance argument is very subjective. I could justify using a subclass by saying that ActiveRecord is a data storage class, and CachedModel is just another data store. If you want caching, inherit from CachedModel. If you don’t want caching, inherit from ActiveRecord::Base.

Except void or where prohibited by law. Your milage may vary. Break glass in case of emergency. 9 comments

Comments RSS FEED

I tried out cached_model and it worked really great, until I started running my unit tests.

I was unsure how to turn it off, so that my unit tests could run without cached data from previous test runs interfering. I looked through the docs, but I didn’t see any mention about this.

If cached_model was a module, I could conditionally include it only in production, and leave it out in development and test.

What would be the best solution to this type of problem?

Dan said about 23 hours later

The easiest way is to disable memcache in your tests. Add :readonly => true to your memcache options.

You can also disable CachedModel completely. In your config/environments/test.rb:

CachedModel.use_local_cache = false
CachedModel.use_memcache = false

You can read about these options in the CachedModel documentation

Eric Hodel said about 24 hours later

Hi Eric,

I’m not sure I’m following your reasoning.

Eric: When you use a class you get super, and super is a beautiful thing.

Agree about the beauty thang. But modules participate in super chaining as well.

Eric: A module doesn’t have this property so you can’t use it to overlay features on top of a class. The class’ implementation will always be called before the module’s implementation.

The current clases methods will be called before both the module and the parent class. The module functions will be called before the parent class.

Which seems to be the right thing to do.

Unless you are talking about something more subtle where the module is mixed-in in more than one place in the inheritance heirarchy.

Thanks.

—Jim Weirich

Jim Weirich said 1 day later

Hi Eric,

Thanks for the detailed response. I’m curious as to why a module wouldn’t work wrt the point Jim made. I’m aware of the amount of magic going on in the finder methods, so I agree 100% things could get very nasty if you had to alias and chain things…

Regarding the inheritance issue – it just seems to me that having a “Cacheable” module makes much more sense (assuming the implementation isn’t horrible). In a perfect world, I’d also prefer including ActiveRecord::Base so my business models could extend from anything, and possibly also mixin ActiveCVS, ActiveLDAP, etc. I see the models main concern being the business logic and data, and how that data gets persisted is secondary.

That said, I’ll admit my real world code hasn’t yet seen the need for abstract business superclasses, so Ruby’s power might allow metaprogramming to take the place of deeper class hiearchies.

Rob Sanheim said 1 day later

Eric H. The easiest way is to disable memcache in your tests. Add :readonly => true to your memcache options.

If memcache is readonly, then the tests fail when the model tries to insert a value into the cache.

.../usr/lib/ruby/gems/1.8/gems/memcache-client-1.0.3/lib/memcache.rb:137:in `set’: Update of readonly cache (MemCache::MemCacheError)

Eric H. You can also disable CachedModel completely. In your config/environments/test.rb:

When CachedModel is disabled, the test throws /usr/lib/ruby/gems/1.8/gems/activesupport-1.3.1/lib/active_support/dependencies.rb:123:in `const_missing’: uninitialized constant CachedModel (NameError)

Don Park said 1 day later

Don:

You’ll get similar errors if you use CACHE directly and memcached is not running. In memcache_util is a wrapper for CACHE called Cache. cached_model uses this to ignore errors.

In your second error it looks like you forgot to require ‘cached_model’.

Eric Hodel said 2 days later

Jim: Lately I’ve seen lots of use of modules for layering functionality on top of classes implemented so that you don’t have to add include Blah into every subclass, particularly from rails. (For example flash.rb, from an older revision so its more clear.) I find this particularly messy.

CachedModel needs to override both instance and class methods, and include only adds instance methods. To work around this Rails adds a ClassMethods module and automatically includes it as appropriate. I find a separate module for extending class methods isn’t nearly as readable or organized as having everything in one place. I also can’t remember when which callbacks get called when, but super is simple and easy to understand.

Eric Hodel said 2 days later

Rob: Unfortunately the world isn’t perfect. ActiveLDAP doesn’t have a similar-enough interface to ActiveRecord to write a clean caching module. (Also, ActiveRecord is SQL-centric in its implementation, so a simplest-thing-that-works implementation is unlikely to be suitable for anything else.)

We do have an abstract class for 43 Things and it inherits from CachedModel. If we were to tie in to other data sources like ActiveLDAP I’d hope for a duck-typing solution.

Eric Hodel said 2 days later

Perhaps the more pertinent question is – should ActiveRecord::Base be a class? It could be argued that both persistence and caching are orthogonal concerns relative to the business logic of your domain.

James Mead said 11 days later

Comments are disabled