Anyone who’s done much Ruby metaprogramming (or even just skimmed the source code to Rails) should be intimately familiar with the following idiom:
klass = Foo::Bar
# ...
klass.extend(Some::Module)
This is a programmatic mixin — it adds the methods defined in Some::Module to the class Foo::Bar for the duration of the current Ruby process. It also does so for all instances of Foo::Bar, whether defined in some scope known to the caller of extend or not.
If may not look like it, but this is yet another case of monkeypatching. The only reason you don’t see it being denounced up and down the ‘tubes like, say, overriding method_missing on NilClass is that it’s usually confined to an application or framework-specific class, like ActiveRecord::Base.
Now, I’m all for the judicious use of powerful language constructs like open classes, but they can be a problem for large-scale projects, or those where collaboration between team members is less-than-perfect. The global and persistent scope of a class-level #extend call like the above can cause unexpected side-effects, too.
As an alternative, I humbly propose the use of instance-scoped extensions. In cases where not all instances of a class may need the additional functionality provided by the mixin, try calling #extend on just that instance. It keeps your namespace clean, doesn’t introduce as many potential pitfalls for code in other scopes, and is reversible: just nil your current reference to the object, and re-create it without the mixin.
Here’s an example, cribbed from some refactoring I’m doing of a web service implementation:
def change_password
token = AuthToken.find(params[:token])
active_user = Account.find(token.identity)
password = params[:password]
raise ArgumentError, "passwords did not match" unless password = params[:confirm_password]
if token.has_privilege?(:password_reset)
active_user.extend(PrincipalManagement)
active_user.change_password(password)
end
render
ml => Account.to_xml
end
One reason this is expecially handy for me is that I can re-use the same model core model classes (like Account) in different applications, only some of which may have the privileges necessary to change passwords, delete accounts, etc.
By limiting the mixin to a single model instance within the scope of a single request, I also protect myself from coding errors that might expose dangerous mutators to untrusted callers. Any attempt to call change_password (or a similarly-restricted method) from outside a scope which explicitly included the mixin will raise a MethodNotFound error, without any additional access-control checks on my part.