4
\$\begingroup\$

The idea for the code I have written came about due to the acts_as_versioned gem which automatically nests a Version class onto an ActiveRecord model. I wanted to use this same type of concept where a class has a companion class, however, one drawback of the gems implementation is that sub-classes don't get their own companion class.

So I set out to extend this concept so that any subclass also gets it's own companion class without having to specifically define it... It's a guarantee that it will be there.

Here is a bit of a visual of the concept. In this example, the nested class also has the addition of being an Enumerator. Like so:

Decorator                   # this is the parent class
MyClass::Enumerator         # it has an Enumerator which sub-classes MyClass

When sub-classing Decorator, the subclass gets its own iterator which is nested within it. Like so:

MyDecorator = Class.new(Decorator)  # This class sub-classes Decorator
MyDecorator::Enumerator             # It has an Enumerator which sub-classes MySubClass

This is a specific example, but I want to deal with the overall abstract concept of a companion class as there are several instances where pairs of classes belong together. The acts_as_versioned gem is one concrete example. Another concrete example would be a decorator where the main class decorates objects, while the Enumerator companion class decorates collections of objects. Any subclass of the decorator should get its own unique enumerator class that is bound to the implementation of the sub-class its nested within.

Being able to call self.class::Enumerator and knowing that the enumerator will be a subclass of self.class without having to specifically define it is something not really possible in many languages. It could also be possible to extend this concept to make the companion class any type of class (or even a collection of classes).

So now onto the implementation... The code I've written to make the above example work is the following (be aware that this code makes use of the ActiveSupport gem for convenience):

module EnumerableCompanionClass
  extend ActiveSupport::Concern

  # this ensures the parent class gets it's companion class
  included { create_enumerable_class }

  module ClassMethods
    # this ensures an descendants get their companion class
    def inherited(sub)
      # need to make sure this doesn't get called for the companion class.  
      # at this point it's still an anonymous class
      # this is to stop infinite recursion
      unless sub.name.blank? || sub < Enumerable
        sub.create_enumerable_class
      end
    end

    def create_enumerable_class
      # only create the companion class once
      @enumerable_class ||=
        # create the companion class as a subclass of the current class
        # but only if self is a class (not a module) and is not the companion class
        if self.is_a?(Class) && !(self < Enumerable)
          enumerable_class = Class.new(self) do
              include ::Enumerable
              # my custom each method
              def each(&block); end
            end
          # define a constant for the companion class
          const_set('Enumerable', enumerable_class)
        end
    end
  end
end

And this is the code to check that it works as expected:

class A
  include EnumerableCompanionClass
  def test1; 'this should work!'; end
end

A::Enumerable.parent == A   # true
A::Enumerable.new.test1     # 'this should work!'

class B < A
  def test2; 'this should also work'; end
end

B::Enumerable.parent == B   # true
B::Enumerable.new.test1     # 'this should work!'
B::Enumerable.new.test2     # 'this should also work'

class C < B
  def test3; 'this should triple work'; end
end

C::Enumerable.parent == C   # true
C::Enumerable.new.test1     # 'this should work!'
C::Enumerable.new.test2     # 'this should also work'
C::Enumerable.new.test3     # 'this should triple work'

So as you can see from the output, it all works as expected.

But it's messy and not very intuitive. For starters, the included method is only there for the parent. Likewise, the inherited method is only there for the sub-classes. The checks for anonymous classes and unnamed classes are there to prevent infinite recursion (we don't want the Enumerator classes to have it's own Enumerator).

I'm happy that it works but I'm sure there is a better way. I've tried other methods and this is the only one that I've gotten to work correctly so far.

I'm looking for guidance on whether this is the right way to go/how this can be improved. I haven't found any canonical way to achieve this or an existing design pattern.

\$\endgroup\$
  • 1
    \$\begingroup\$ I suggest giving more info on what this pair of classes actually does; what it's for. Perhaps there is a pattern that is an alternative to your pair of classes. \$\endgroup\$ – CL22 Nov 12 '15 at 1:20
  • \$\begingroup\$ Ok, I've given a couple of concrete examples of this concept. Granted, it is a tricky concept, so hopefully this assists the user in their understanding of what this code means, what it's purpose is, and what it's usefulness could potentially be. \$\endgroup\$ – br3nt Nov 12 '15 at 23:13
  • \$\begingroup\$ If you can think of a more compelling/understandable title, be my guest to suggest something :p \$\endgroup\$ – br3nt Nov 12 '15 at 23:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.