February 21, 2010
Older: MongoTips: All Mongo, All The Time
Newer: MongoMapper 0.7: Identity Map
MongoMapper 0.7: Plugins
Whew, been a bit of a whirlwind around here. Lots to do lately, so I have been slow talking about MongoMapper’s 0.7 release from a week ago. I’ll take a crack now. Let’s start with how to extend MongoMapper with plugins and some other fun functionality.
Separation Is Good
Originally, because Document and EmbeddedDocument shared so much, in my head it made sense to just include EmbeddedDocument in Document. What this led to was confusion in the code. Lots of things sharing, some that should not be and some that should. Yes, I wanted to share the functionality between the two modules, but including one in the other is not the best way.
So I sat down and thought about it. I figured the best way would be to split out each bit of functionality into separate modules and then include each of those modules that were shared into both Document and EmbeddedDocument. With this in mind, I went to down. I broke all the refactoring rules and just started ripping stuff out and going to down. What I ended up with is a few tests that, despite all odds, I could not get to pass. My problem was that I took on too much at once.
Thankfully, several hours in, I realized what I had done and stopped. I blew away all my changes and started over. This time, I focused on small changes and made sure to run all the tests after each change. Attacking the code base with this mindset allowed me to slowly and gently remove bits and pieces into modules and begin the process of separating the conjoined twins known as Document and EmbeddedDocument.
Plugins Are Born
As I was doing this, I realized that I was doing the same things over and over. Including a module here, extending one there and occasionally dropping down to class_eval when brute force was needed. That is when it hit me that some sort of system that abstracted this process out would be nice. Thus was born MongoMapper::Plugins. MM::Plugins is the tiniest bit of code, but it led to sweeping changes that make MM far more readable and separated. It is so tiny, in fact, that I will paste it below.
module MongoMapper
module Plugins
def plugins
@plugins ||= []
end
def plugin(mod)
extend mod::ClassMethods if mod.const_defined?(:ClassMethods)
include mod::InstanceMethods if mod.const_defined?(:InstanceMethods)
mod.configure(self) if mod.respond_to?(:configure)
plugins << mod
end
end
end
This small addition made separating out all the shared functionality really easy to complete and really easy to read. For example, when you create a MongoMapper::Document, you include that module. This calls the Ruby included hook, which now looks like this in Document:
def self.included(model)
model.class_eval do
include InstanceMethods
extend Support::Find
extend ClassMethods
extend Plugins
plugin Plugins::Associations
plugin Plugins::Clone
plugin Plugins::Descendants
plugin Plugins::Equality
plugin Plugins::Inspect
plugin Plugins::Keys
plugin Plugins::Dirty # for now dirty needs to be after keys
plugin Plugins::Logger
plugin Plugins::Pagination
plugin Plugins::Protected
plugin Plugins::Rails
plugin Plugins::Serialization
plugin Plugins::Validations
plugin Plugins::Callbacks # for now callbacks needs to be after validations
extend Plugins::Validations::DocumentMacros
end
super
end
As you can see, all I do is declare a bunch of plugins. It is really easy to see what is going on without even looking at those plugins. This little change was huge mentally for me. It has made the project a joy to maintain once again, as I do not have to think about how to implement each new bit of functionality. The new feature just goes into a new or existing plugin.
An Example Plugin
So what does a plugin look like? Below is the logger plugin:
module MongoMapper
module Plugins
module Logger
module ClassMethods
def logger
MongoMapper.logger
end
end
module InstanceMethods
def logger
self.class.logger
end
end
end
end
end
This plugin adds simple methods for accessing MongoMapper’s logging functionality on any Document or EmbeddedDocument. It means if you have a Foo model, you get Foo.logger and Foo.new.logger. Very simple and very readable. Most of the plugins are tiny like this.
Extending All Models
The other cool thing, an idea taken from DataMapper, is append_inclusions and append_extensions. These two methods are added to both Document and EmbeddedDocument and allow you to add any modules to be included or extended on every Document or EmbeddedDocument. Examples work best for me, so check this out:
class Item
include MongoMapper::Document
end
module Fooer
def foo
puts 'Foo'
end
end
MongoMapper::Document.append_extensions(Fooer)
class Sandle
include MongoMapper::Document
end
Item.foo # puts 'Foo'
Sandle.foo # puts 'Foo'
Note that it does not matter whether you append_extensions before or after the model is defined. Either way, anything that includes MongoMapper::Document gets the extension. Behind the scenes, this calls extend Fooer
on each model. append_inclusions behaves the same except that instead of calling extend with the module, it calls include. See below:
class Item
include MongoMapper::Document
end
module Fooer
def foo
puts 'Foo'
end
end
MongoMapper::Document.append_inclusions(Fooer)
class Sandle
include MongoMapper::Document
end
Item.new.foo # puts 'Foo'
Sandle.new.foo # puts 'Foo'
Note on this example that we are calling new and that foo is now an instance method instead of a class method (include vs extend). Also, append inclusions and extensions are added to EmbeddedDocument so you can apply functionality specfically to it if needed.
Adding A Plugin To All Documents
The cool thing about this is if you want to add a plugin to every Document, you can just do something like this, instead of manually calling plugin
in each model:
class Item
include MongoMapper::Document
end
module FooPlugin
module ClassMethods
def foo
puts 'Foo'
end
end
module InstanceMethods
def foo
self.class.foo
end
end
end
module FooPluginAddition
def self.included(model)
model.plugin FooPlugin
end
end
MongoMapper::Document.append_inclusions(FooPluginAddition)
class Sandle
include MongoMapper::Document
end
Item.foo # puts 'Foo'
Sandle.foo # puts 'Foo'
Item.new.foo # puts 'Foo'
Sandle.new.foo # puts 'Foo'
We create the FooPlugin, then we create a module that uses Ruby’s included hook to call plugin on the model. Because append_inclusions is called on each Document, all our models get the new plugin. Fun stuff.
Thanks to plugins and the append extensions/inclusions stuff there is a standardized way of extending MongoMapper models. In fact, people have already hopped on the bandwagon and started adding features as plugins instead of directly into Document or EmbeddedDocument. The first such plugin to be merged in is the protected attributes plugin.
These additions are huge for MongoMapper in general, but I have found them handy for organizing code on Harmony as well.
There is still more work to do in regards to organizing the MM code base, as some plugins are almost catch alls, but this has really cleaned things up in the code, especially for people new to MM.
10 Comments
Feb 21, 2010
Yes! This will simplify some API caching libraries I’ve been writing. Can’t wait to use it.
Feb 21, 2010
@Wynn – Awesome! Can’t wait to see those libraries. :)
Feb 21, 2010
Great post! I’ve been wondering how to split things apart like this, so this really helped me grok the process… Much thanks!
Feb 21, 2010
Hi John,
I really enjoyed the post, but I’m curious why the need for the extra FooPluginAddition module?
Could you not just do the following?
Am I missing something obvious?
Feb 21, 2010
@Michael – Yes, good point. You could do that as well.
Feb 22, 2010
Awesome work! These changes will surely revamp the plugin creation, and will speed up the mongomapper evolution. Thank you!
Feb 22, 2010
That’s some nice refactoring, John. Very cool. @Michael, I wondered the same thing. Glad you asked!
Feb 22, 2010
Good post. It’s very much more readable this way. It also actually gave me some ideas. Thanks for sharing.
Feb 23, 2010
The plugin module is great, and in general these two latest MongoMapper posts are good, however, wasn’t the idea to post these things on MongoTips from now on? :)
(Especially the Identity Map post seems quite MongoMapper-specific..)
Anyways, keep up the good work!
Feb 25, 2010
Hello John,
I appreciated your work on plugins – it was dead simple to hook my plugin on MongoMapper :
http://github.com/etaque/mongo_squeezer
(basically a port of clean_empty_attributes)
Thank you for your work.
Thoughts? Do Tell...