2

How would be possible to re-use a full inheritance chain by changing configuration parameters? (i.e. constants)

The base inheritance chain

So let's say we have this inheritance chain C < B < A

class A
  BEHAVE = 'Foo'.freeze

  def how?
    "I behave like '#{behave[:a]}'"
  end

  protected

  def behave
    {a: self.class::BEHAVE}
  end
end

class B < A
  WITH = 'Bar'.freeze

  def how?
    "#{super} with '#{behave[:b]}'"
  end

  protected

  def behave
    super.merge(b: self.class::WITH)
  end
end

class C < B
  AT = 'C'.freeze

  def how?
    puts "#{super} as '#{behave[:c]}'"
  end

  protected

  def behave
    super.merge(c: self.class::AT)
  end
end

Usage example:

puts A.new.how?
# I behave like 'Foo'
puts B.new.how?
# I behave like 'Foo' with 'Bar'
puts C.new.how?
# I behave like 'Foo' with 'Bar' as 'C'

Trying to re-use the inheritance chain

What I would like to achieve is to have a parallel inheritance chain called Z < Y < X that behaves like C < B < A:

          inherited            inherited
 Class A -----------> Class B -----------> Class C
        \                    \                    \
         \ inherited          \ inherited          \
          \                    \                    \
     Class X -----------> Class Y  -----------> Class Z
             inherited              inherited

  • Tagged the question under multiple-inheritance due to the above.
  • Something like this:
    • A > X > Y > Z
    • A > B > Y > Z
    • A > B > C > Z

If multiple inheritance was supported by ruby, these would be the results:

C.new.is_a?(A) # true
C.new.is_a?(X) # false

Z.new.is_a?(A) # true
Z.new.is_a?(Y) # true
Z.new.is_a?(C) # true

Y.new.is_a?(C) # false
Y.new.is_a?(X) # true

X.new.is_a?(B) # false

... given that I can change the configuration (i.e. constants), to make them behave differently (i.e. pull from different base folders or source files, push as an specific user group for permissions, use different public gpg keys, etc.).

Expected Result

The expected result result would look like this:

puts X.new.how?
# I behave like 'Foz'
puts Y.new.how?
# I behave like 'Foz' with 'Baz'
puts Z.new.how?
# I behave like 'Foz' with 'Baz' as 'Z'

Option A: Convention over configuration

First thing that comes to mind is to rather than trying to create a parallel chain, this can be achieved via convention by getting A, B and C instances pulling from expected configuration files, and initializing A with a source base folder that B and C will know via A instance object.

So something like this:

class_a.json

{"a": "Foz"}

a.rb

class A
  DEFAULT_CONFIG = File.join('config', 'class_a.json').freeze

  attr_reader :base_folder

  def initialize(base_folder: '.')
    @base_folder = base_folder
  end

  def how?
    "I behave like '#{behave[:a]}'"
  end

  protected

  def behave
    a_config
  end

  private

  def a_config
    @a_config ||= JSON.parse(
      File.read(to_file_path(DEFAULT_CONFIG))
    ).symbolize_keys # Ruby on Rails >= 3.0.0
  end

  def to_file_path(filename)
    File.join(base_folder, filename)
  end
end
  • And classes B and C also pulling from config files class_b.json and class_c.json.

class_b.json

{"b": "Baz"}

class_c.json

{"c": "Z"}

Well, I still don't know how this approach improves anything in terms of usability. And I think that introduces more configuration needs than convention. But it may be me just missing how to use any convention at all ¯_(ツ)_/¯

Option B: Via Module Definitions

  • Here is some discussion.

Defining the base modules BaseA, BaseB and BaseC and including them in the inheritance chain of X, Y and Z. Something like this:

                      inherited                 inherited
          Class A  >------>------>  Class B  >------>------>  Class C
               /                         /                         /
              /                         /                         /
             /                         /                         /
 Module BaseA  - - - - --> Module BaseB  - - - - --> Module BaseC
             \  included               \  included               \
              \                         \                         \
               \                         \                         \
          Class X  >------>------>  Class Y  >------>------>  Class Z
                      inherited                 inherited
  • The modules already create an inclusion chain from BaseA to BaseC, through BaseB.
  • The inheritance chain between classes A, B and C, as well as between the classes X, Y and Z, is explicit and they include the base code from the modules.

Modules

base_a

module BaseA
  BEHAVE = 'Foo'.freeze

  def how?
    "I behave like '#{behave[:a]}'"
  end

  protected

  def behave
    {a: self.class::BEHAVE}
  end
end

base_b

module BaseB
  include BaseA

  WITH = 'Bar'.freeze

  def how?
    "#{super} with '#{behave[:b]}'"
  end

  protected

  def behave
    super.merge(b: self.class::WITH)
  end
end

base_c

module BaseC
  include BaseB

  AT = 'C'.freeze

  def how?
    puts "#{super} as '#{behave[:c]}'"
  end

  protected

  def behave
    super.merge(c: self.class::AT)
  end
end

Inheriance chain implementation (by modifying the behaviour):

class X
  include BaseA
  BEHAVE = 'Foz'.freeze
end

class Y < X
  include BaseB
  WITH = 'Baz'.freeze
end

class Z < Y
  include BaseC
  AT = 'Z'.freeze
end

We meet the expected result:

puts X.new.how?
# I behave like 'Foz'
puts Y.new.how?
# I behave like 'Foz' with 'Baz'
puts Z.new.how?
# I behave like 'Foz' with 'Baz' as 'Z'

And the inheritance chain seems to be natively/virtually reflected (although on the modules):

c_obj = Object.new.extend(BaseC)
c_obj.is_a?(BaseA) # true
c_obj.is_a?(X)     # false

Z.new.is_a?(BaseA) # true
Z.new.is_a?(Y)     # true
Z.new.is_a?(BaseC) # true

Y.new.is_a?(BaseC) # false
Y.new.is_a?(X)     # true

X.new.is_a?(BaseB) # false

Question

While Option B (modules) remains my preferred approach, due to its versatility, it feels like you get to know the relationship between modules very well (i.e. BaseC includes BaseB, which in turn includes BaseA). For this reason, I would like to know if I am missing some other approach or convention to get things easier when trying to achieve inheritance chain re-usability.

Is there any other way to achieve this in ruby?

And if NOT (besides specs & comments) is there any guidance on how get the code a bit more self-explanatory when building on modules? (one thing that comes to mind is to offer the base implementation A > B > C as an example on how to implement other behaviours via BaseA, BaseB and BaseC?).

  • Aside note: I am aware that Rails framework offers easier ways to define those things (i.e. FeatureAble ActiveSupport::Concern extended modules where you can use included(&block), etc.). Although the example here tries to be simple, working with modules in native ruby can get as a result code that makes you trying to draw a schema of relationship between modules that sometimes is justified (I think that Rake is a good example of this) but other times feels more complex than it should really be (i.e. def included(base); super; base.send :include, SomethingElse).

Well, the question is on the table. Let's see if we can get some pearl.

Thanks!

4
  • Just so we are clear your module implementation is interleaved. e.g. Z < BaseC < Y < BaseB < X < BaseA. Otherwise, I am still completely unclear on what you are trying to achieve. Commented Mar 25 at 17:17
  • 1
    It looks like you are trying to implement virtual inheritance, virtual classes, and class hierarchy inheritance in Ruby, is that correct? I don't think that will be possible. If you need those features for your design, it is probably better to use a language which supports them in the first place. They were originally introduced in Beta, then generalized in gBeta. Both Beta and gBeta haven't been maintained in a long time, unfortunately. But they also exist in Newspeak, which is still maintained, and is a pretty great language. Commented Mar 26 at 7:25
  • @engineersmnky I have updated the question by improving the diagram of the section Trying to re-use the inheritance chain to better reflect the relationships. Jorg W Mittag spotted the aim correctly. Commented Mar 26 at 9:51
  • @JörgWMittag thanks for your answer. I read you are literate in programming languages. The aim is to extend libraries in ruby in a sustainable and re-usable way. You are correct is about class hierarchy inheritance. I am unsure how come you don't name it multiple-inheritance though. Question: besides traceability/complexity on "non-explicit" relationships (due to not being native), would you say that virtually implementing this via module inclusions chain (kind of "hierarchy"; via Option B - modules) can have any drawbacks? (I might change the question, just for the sake of it). Commented Mar 26 at 9:57

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.