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