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

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 Johnston wrote:

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=?

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 Johnston 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

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

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

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

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

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

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