1

I have the following situation:

class A < CommonParent
  ... some code ...

  class IdenticalDescendent < self
    identical_statement_0
    identical_statement_1
  end
end

class B < CommonParent
  ... some other code ...

  class IdenticalDescendent < self
    identical_statement_0
    identical_statement_1
  end
end

I have this situation a lot. Like, there are about forty IdenticalDescendent classes in my app. I like the pattern, it allows me to call A::IdenticalDescendent or B::IdenticalDescendent or whatever to access certain related behaviours in different domains (specified by A or B). For reasons, I can't just completely abstract the problem away by re-designing the behaviour clustering.

So the general form of my question is how do I automate the generation of IdenticalDescendent in all of these. There ARE descendants of CommonParent that don't invoke this pattern, so the action probably shouldn't happen there. I imagine it should happen in a mixin or something, but I find that if I just try to do:

class A < CommonParent
  include CommonBehaviour

  ... some code ...
end

module CommonBehaviour
  ... what ...
end

I can't figure out how to write CommonBehaviour to allow for the IdenticalDescendent to descend from the including class.

Help me StackOverflow, you're my only hope.

wmjbyatt
  • 686
  • 5
  • 15

3 Answers3

1

The answer I was looking for is to use block notation for Class.new inside a self.included callback. I have this now:

module CommonDescendant
  def self.included(base)
    descendant_class = Class.new(base) do
      ... put my desired common behavior here ...
    end

    base.const_set :Descendant, descendant_class
  end
end

class A
  include CommonDescendant

  ... unique behavior ...
end

class B
  include CommonDescendant

  ... unique other behavior ...
end

And this gives us the design I want!

wmjbyatt
  • 686
  • 5
  • 15
  • Looks good. You can put `include CommonDescendent` in the `included` method. (See my answer.) I noticed you decided to change the spelling of "descendent". Sorry, but "descendant" only applies to plants and animals (including humans). Wait: what happened to `CommonParent`? – Cary Swoveland Jun 26 '15 at 19:16
  • I don't follow your code here, in part because there is no reference to `CommonParent`. In any event, `included` will be triggered each time you create a subclass of `CommonParent` or a subclass of that subclass, etc., resulting in "stack level too deep". (See my answer for how to avoid that.) Secondly, `include CommonDescendant` doesn't do anything, because that module contains no instance methods. – Cary Swoveland Jun 27 '15 at 07:17
0

I believe you can automate your pattern by using the callback (hook) Class#inherited:

class CommonParent
  def self.inherited(klass)
    return unless klass.superclass == CommonParent
    klass.const_set(:Descendent, Class.new(klass) do
      def x
        puts "in x"
      end
    end)
  end
end

class A < CommonParent
  def a
    puts "in a"
  end   
end

d = A::Descendent.new #=> #<A::Descendent:0x007f99620172e8> 
d.a                   #   in a
d.x                   #   in x

class B < CommonParent
  def b
    puts "in b"
  end
end

d = B::Descendent.new #=> #<B::Descendent:0x007f99618b18f0> 
d.b                   #   in b
d.x                   #   in x
d.a                   #=> NoMethodError:... (as expected)

Note that, without:

return unless klass.superclass == CommonParent

the creation of A::Descendent would trigger inherited with klass => Descendent, causing an anonymous subclass of Descendent to be created, etc., resulting in a "stack level too deep exception."

Cary Swoveland
  • 106,649
  • 6
  • 63
  • 100
-1

I would suggest separating descendant class and method generation. Of course, you could toss everything into a class_eval block (which will positively reek).

Something like the following (completely untested)

module CommonDescendants
  Descendant = Class.new(self) do 
    include CommonDescendantMethods
  end
end

module CommonDescendantMethods
end

class A < CommonParent
  extend CommonDescendants
end
fylooi
  • 3,840
  • 14
  • 24
  • The behavior that you're going for around `Descendant = Class.new(self)` is what I'm missing, though. That code doesn't work, because in the context `self` is the module `CommonDescendants`, not the including class. That having been said, I forgot that the `Class.new` syntax accepted a block, so I may play around with that to see if I can work it from there. – wmjbyatt Jun 26 '15 at 08:59
  • Parsing the module CommonDescendants raises the exception, `"TypeError: superclass must be a Class (Module given)"`. That's because `self` is `CommonDescendents` (I've corrected spelling), which is not a class. `extend CommonDescendents` makes no sense because `CommonDescendents` contains no methods and class methods are not desired anyway. I've downvoted your answer, which I rarely do, not just because it's wrong, but because you presented it without testing. – Cary Swoveland Jun 26 '15 at 20:14