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
andC
also pulling from config filesclass_b.json
andclass_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
toBaseC
, throughBaseB
. - The inheritance chain between classes
A
,B
andC
, as well as between the classesX
,Y
andZ
, 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 useincluded(&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 thatRake
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!
Z < BaseC < Y < BaseB < X < BaseA
. Otherwise, I am still completely unclear on what you are trying to achieve.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 itmultiple-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).