What to do when two mixins need to hook into attribute= of a

I can override an ActiveRecord attribute in an mixin as follows:

module MyMixin
def attribute=(new_value)
write_attribute :attribute, new_value
end
end

But what do I do if I have two mixins which may be included in
arbitrary order and are somewhat orthogonal in purpose and who both
need to customize attribute=?

Cheers,
Michael

(specifically I am trying to figure out how to make act_as_attachment
pass all its tests. It is broken right now because two mixins –
InstanceMethods and FileSystemMethods – both override filename= )

Ok, this actually easy, because they can both do:

def attribute=(new_value)
# stuff
super
end

and all mixed in definitions will get called.

I was led astray by the fact that all the examples of overriding an
attribute writer have it calling write_attribute so I sillily thought
that super would return a nomethoderror in the last mixin in the
chain. But in actual fact you can just call super in an overridden
attribute write, you don’t have to call write_attribute (unless
someone knows a reason you do???).

However, figuring out what to do if you couldn’t count on the method
being mixed_in already having a definition in the superclass helped
me to learn more about ruby. Here is my example, in case anyone is
curious:

class Superthing #this stands in for ActiveRecord::Base
def dohook
puts “whatever happens, this must happen”
end
end

module Doer #this is in one file in a plugin
module Mary
def doit
puts “Mary says doit”
super
end
end
end

module Doer # this is in another file in a plugin, and is somewhat
orthogonal to “Mary”
module Bob
def doit
puts “Bob says doit”
super
end
end
end

module Doer # this initializes the plugin

def self.included(base)
base.extend SetupMethods
end

module MakeSureTheresASuper # this is necessary because the
superclass only defines “dohook” not “doit”
def doit
dohook
end
end

module SetupMethods
def setmeup
include MakeSureTheresASuper
if rand(2) == 1
puts “Bob first”
include Bob
include Mary
else
puts “Mary first”
include Mary
include Bob
end
end
end
end

Superthing.send(:include, Doer::SetupMethods)

class Thing < Superthing
include Doer

setmeup

def initialize()
end
end

#will always do Bob & will always do Mary, but order is random
Thing.new.doit

Michael J. wrote:

need to customize attribute=?
Would a method chain work. The new release candidate has a method built
to make this easy (I think called alias_method_chain) but to simple
alias_method calls will work as well.

Eric

Michael J. wrote:

But what do I do if I have two mixins which may be included in
arbitrary order and are somewhat orthogonal in purpose and who both
need to customize attribute=?

Would a method chain work. The new release candidate has a method built
to make this easy (I think called alias_method_chain) but two simple
alias_method calls will work as well.

Eric

On 28-Nov-06, at 6:07 AM, Eric A. wrote:

Would a method chain work. The new release candidate has a method
built
to make this easy (I think called alias_method_chain) but two simple
alias_method calls will work as well.

On 28-Nov-06, at 6:17 AM, Rob S. wrote:

def my_method_with_bar_feature
# do stuff
end
end

This is completely untested. If the order in which these things
happens is important, you’ll have to be more careful and maybe
explicitly require things.

  • rob

Thanks Eric & Rob. This does seem like a more robust way of doing it,
although I wouldn’t want to break the downward compatibility just yet.

I’m curious to know if there is anything inherently wrong with my way
of doing it, by calling super in each overloaded method, and letting
the ancestors callchain take care of it?

I found one wrinkle in my solution, which is that I do need to define
a bare filename= method because sometimes the model objects do not
persist the filename attribute to the database, in which case super
is of course not defined for filename= (but write_attribute still works)

Cheers,
Michael

On 11/28/06, Michael J. [email protected] wrote:

arbitrary order and are somewhat orthogonal in purpose and who both
need to customize attribute=?

Cheers,
Michael

(specifically I am trying to figure out how to make act_as_attachment
pass all its tests. It is broken right now because two mixins –
InstanceMethods and FileSystemMethods – both override filename= )

You’ll have to do a little metaprogramming and method chaining. This
is assuming rails edge, so you can use alias_method_chain.

So in mixin one:

module Foo
def self.included(klass)
klass.alias_method_chain :my_method, :with_foo_feature
end

def my_method_with_foo_feature
# do stuff
end
end

module Bar
def self.included(klass)
klass.alias_method_chain :my_method, :with_bar_feature
end

def my_method_with_bar_feature
# do stuff
end
end

This is completely untested. If the order in which these things
happens is important, you’ll have to be more careful and maybe
explicitly require things.

  • rob

http://www.ajaxian.com

  • rob
    Hmmm, I see one disadvantage with the aliases: the consumer class
    can’t override attribute=, unless they make sure the def stays
    physically before the inclusion of the modules.

Using the callchain method, the consumer class can override
attribute= as long as they call super instead of write_attribute.

Unless I did something wrong. Here is my test:

class Superthing #this stands in for ActiveRecord::Base
def dohook
puts “whatever happens, this must happen”
end
end

module Doer #this is in one file in a plugin
module Mary
def self.included(base)
base.class_eval do
alias_method :doit_without_mary, :doit unless method_defined?
(:doit_without_mary)
alias_method :doit, :doit_with_mary
end
end
def doit_with_mary
puts “Mary says doit”
doit_without_mary
end
end
end

module Doer # this is in another file in a plugin, and is somewhat
orthogonal to “Mary”
module Bob
def self.included(base)
base.class_eval do
alias_method :doit_without_bob, :doit unless method_defined?
(:doit_without_bob)
alias_method :doit, :doit_with_bob
end
end
def doit_with_bob
puts “Bob says doit”
doit_without_bob
end
end
end

module Doer # this initializes the plugin

def self.included(base)
puts “I just got included”
base.extend SetupMethods
end

module MakeSureTheresASuper # this is necessary in my example
def doit
puts “I’m just standing in”
dohook
end
end

module SetupMethods
def setmeup
include MakeSureTheresASuper
if rand(2) == 1
puts “Bob first”
include Bob
include Mary
else
puts “Mary first”
include Mary
include Bob
end
end
end
end

Superthing.send(:include, Doer::SetupMethods)

class Thing < Superthing
include Doer

setmeup

end

class CustomThing < Superthing
include Doer

setmeup

def doit()
puts “CustomThing says doit”
super
end

if we do setmeup here instead, it will work. but that is yucky

end

#will always do Bob & will always do Mary, but order is random
puts “Thing:”
Thing.new.doit
puts
puts “OOPS!!! CustomThing loses all the good love”
puts “CustomThing:”
CustomThing.new.doit

persist the filename attribute to the database, in which case super
is of course not defined for filename= (but write_attribute still works)

Cheers,
Michael

I don’t think there is anything wrong with your implementation,
strictly. Its just more code, which means more to test and to
maintain. Using a Rails feature like alias_method_chain will be a
more obvious idiom to other experienced Rails developers, and will
also have the advantage of using a tested, supported api from Rails.

OTOH, doing it yourself did help you learn more about what is going on
underneath, which is a good thing. :slight_smile:

  • rob

http://www.ajaxian.com