Search code examples
rubymacrosmetaprogrammingclass-designaccessor

Best practices and implementation for macro and class-level accessor methods?


I'm designing/building a system of classes that all derive from a single base class.

The goal is to have easy-to-use inherited macro methods that look something like this:

class Something < A::Base
  full_name 'Something that goes bump in the night.'
end

Any code should be able to ask the class for this information (or, likely, normalized/derived infomation) later on via class-level accessor method(s).

puts Something.full_name
  # => "Some kind of calculated value that may or may not go bump in the night."

 

Given that A::Base includes/extends/somehow-otherwise-mixes-in both a module with the macro method that works something like this:

module MacroMethods
  private
  def full_name(full_name)
    # non-trivial, one-time-only set-up code exists here in actual usage
  end
end

and a module with the class-level accessor method that works something like this:

module AccessorMethods
  public
  def full_name
    # a non-trivial, runtime-calculated value is returned here in actual usage
  end
end

no matter how I mix them in, I'm continually running into naming conflicts (i.e. ‘wrong number of arguments (1 for 0) (ArgumentError)’) between the two.

Note: full_name is the simplest example of what is needed; other, more-complex macros/accessors ensure the non-flexible constraints of macro methods needing to be declared inside the class and needing to be set once-and-only-once.


My question is two-fold:

  1. Is there a way to make this all work inside of the A::Base class?
  2. Is this the right way to do this in Ruby? Is there a better way go about it, achieving the same result?

Options that have been considered:

  • Calling either the macro or accessor method(s) something else.
    (e.g. in Something class: set_up_full_name 'Something that …')
    Downside is that the naming is confusing and unconventional.

  • Making the accessor method(s) instance-level instead of class-level.
    (e.g. puts a_something.full_name')
    Downside is that the traits set up by the macros are inherent to the class, not to each instance (in some cases, only a reference to the class may be available, not an instance).

  • Creating a single method that handles both macro and accessor functionality.
    (e.g. in A::Base class: def self.full_name(*args) …)
    Downside is that the macro methods can no longer be private and the RDoc looks like sh*t.

  • Using abstact/virtual-ish methods instead.
    (e.g. in Something class: def self.full_name; 'Something that …'; end)
    Downside is that this is more code in sub-classes and is more of a Objective-C (or C++, or Java, …) thing than a good Ruby paradigm.


Solution

  • Slipp, I read your question carefully. There is no way you can have 2 different methods called full_name defined on the same object at the same time. BUT, you could do something like this:

    module MacroMethods
      private
      def full_name(full_name)
        # non-trivial, one-time-only set-up code exists here in actual usage
        # this method can only be called once in the definition of any given class,
        #   because after the first call, it will be redefined!
        extend AccessorMethods
      end
    end
    
    module AccessorMethods
      public
      def full_name
        # a non-trivial, runtime-calculated value is returned here in actual usage
      end
    end
    
    class Base
      extend MacroMethods
    end
    
    class Something < Base
      full_name 'after this call, I will get a new full_name method!'
    end
    
    class SomethingElse < Base
      full_name 'so will I!'
    end