mardi 4 août 2015

How to create a nested class when subclassing

I'm wanting to create a pair of classes. The second of the pair sub-classes the first, is nested within the first, and only has the addition of being an Enumerator. Like so:

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

Further to this, when sub-classing MyClass, the subclass should get its own iterator which is also nested within it. Like so:

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

I've actually created the code and it works, but I'm wondering if there is a better way or an existing design pattern to achieve this. The code I've written to make this work is the following:

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 has to be a better way. I've tried other methods and this is the only one that I've gotten to work correctly so far.

Does any one know the canonical way to achieve this, or a better design pattern?

Thanks in advance!

Aucun commentaire:

Enregistrer un commentaire