General Ruby OOP Question - using inheritance or include for shared attributes

J

james.d.masters

I have a generic Ruby OOP question. Which is the more "correct" way
to share methods between two similar classes that do not necessarily
need to inherit from anything else: inheritance or include (as a mix-
in)? I realize that this question may be subject to opinion; however,
I'm getting ready to formally release a library and I'd like to do the
"right" thing. I like the inheritance route but I'm not sure that
creating a "Base" class that will never be instantiated directly by
anything externally is "proper".

Here are examples of both approaches:

### EXAMPLE 1 (inheritance):

# Here Base.new will (should) never be called directly; only through
inheriting classes
class Base
def initialize()
@records = {}
end

def get_record(key) ; @records[key] ; end

def set_record(key, value) ; @records[key] = value ; end
end


class RecType1 < Base ; end
class RecType2 < Base ; end


### EXAMPLE 2 (mix-in):

module Base
def init_record()
@records = {}
end

def get_record(key) ; @records[key] ; end

def set_record(key, value) ; @records[key] = value ; end
end

class RecType1
include Base
def initialize()
init_record
end
end

class RecType2
include Base
def initialize()
init_record
end
end


Thanks in advance,
-James
 
T

Trans

I have a generic Ruby OOP question. Which is the more "correct" way
to share methods between two similar classes that do not necessarily
need to inherit from anything else: inheritance or include (as a mix-
in)? I realize that this question may be subject to opinion; however,
I'm getting ready to formally release a library and I'd like to do the
"right" thing. I like the inheritance route but I'm not sure that
creating a "Base" class that will never be instantiated directly by
anything externally is "proper".

Here are examples of both approaches:

### EXAMPLE 1 (inheritance):

# Here Base.new will (should) never be called directly; only through
inheriting classes
class Base
def initialize()
@records = {}
end

def get_record(key) ; @records[key] ; end

def set_record(key, value) ; @records[key] = value ; end
end

class RecType1 < Base ; end
class RecType2 < Base ; end

### EXAMPLE 2 (mix-in):

module Base
def init_record()
@records = {}
end

def get_record(key) ; @records[key] ; end

def set_record(key, value) ; @records[key] = value ; end
end

class RecType1
include Base
def initialize()
init_record
end
end

class RecType2
include Base
def initialize()
init_record
end
end

When it comes to Ruby there is a huge consideration here: Do you or
will you ever wish to pass on class level methods? Or more technically
speaking, do you want class singleton methods to be in the inheritance
chain? If so then you have to use a class, since modules don't allow
it (to my eternal dismay). For example:

class X; def self.x; "x"; end; end
class Y < X; end
Y.x #=> x

module X; def self.x; "x"; end; end
class Y; include X; end
Y.x #=> error

IMHO It's unfortunate that this is the case --or at least that there's
not a method other than #include that can do it. I guess it's because
I wrote annotations, which turned out to be very tricky b/c of this.

T.
 
J

John Wilger

When it comes to Ruby there is a huge consideration here: Do you or
will you ever wish to pass on class level methods? Or more technically
speaking, do you want class singleton methods to be in the inheritance
chain? If so then you have to use a class, since modules don't allow
it (to my eternal dismay). For example:

class X; def self.x; "x"; end; end
class Y < X; end
Y.x #=> x

module X; def self.x; "x"; end; end
class Y; include X; end
Y.x #=> error

Modules may not allow it directly, but it seems that you can work
around it easily enough:

module A
def self.included( other )
def other.foo
puts 'bar'
end
end
end

class B
include A
end

class C < B
end

#> B.foo
#=> bar
#
#> C.foo
#=> bar

Or am I missing something important here?

Really, the choice of whether to use inheritance or module inclusion
comes down to the question of whether you are sharing identity or just
behavior. For instance, do you want:

class WalkingThing
def walk
puts 'walking'
end
end

class Human < WalkingThing
end

class Robot < WalkingThing
end

or:

module WalkingThing
def walk
puts 'walking'
end
end

class Human
include WalkingThing
end

class Robot
include WalkingThing
end

A bit contrived, sure -- but hopefully makes the point.
 
B

Bob Hutchison

Modules may not allow it directly, but it seems that you can work
around it easily enough:

module A
def self.included( other )
def other.foo
puts 'bar'
end
end
end

class B
include A
end

class C < B
end

#> B.foo
#=> bar
#
#> C.foo
#=> bar

module A
def self.included(other)
super
other.extend(ClassMethods)
end

module ClassMethods
def foo
puts "bar"
end
end
end

Is a little cleaner I think. And you can do all kinds of other weird
stuff in the self.included method.

Another consideration is that in a single inheritance language like
Ruby, inheritance is valuable. I tend to use mixins until I'm forced
to use inheritance. In a language like Ruby the 'type hierarchy'
isn't all that important (duck typing makes it mostly irrelevant).

Cheers,
Bob
 
T

Trans

Modules may not allow it directly, but it seems that you can work
around it easily enough:

module A
def self.included( other )
def other.foo
puts 'bar'
end
end
end

class B
include A
end

class C < B
end

#> B.foo
#=> bar
#
#> C.foo
#=> bar

Or am I missing something important here?

Sure. You can do all sorts of tricks to get the desired behavior. The
point is simply "it ain't natural".

I don't really hold a theoretical view of classes and modules. I work
with this on strictly practical level. A simple example is
ActiveSupport's mattr methods. I haven't looked at them in a while so
maybe they've _hacked_ a solution since then, but if you use mattr in
a module it'll break if included in a class or other module.
Really, the choice of whether to use inheritance or module inclusion
comes down to the question of whether you are sharing identity or just
behavior. For instance, do you want:

There's a duck around here that doesn't know the difference ;-)

T.
 
J

james.d.masters

module X; def self.x; "x"; end; end
class Y; include X; end
Y.x #=> error

IMHO It's unfortunate that this is the case --or at least that there's
not a method other than #include that can do it. I guess it's because
I wrote annotations, which turned out to be very tricky b/c of this.

In the case of pulling in module methods as class (singleton) methods,
you can use extend instead of include. Your second example rewritten
with "extend" would work:

module X; def x; "x"; end; end
class Y; extend X; end
Y.x #=> "x"

Unfortunately, I'm fairly sure that would require separate modules for
class methods (through extend) and instance methods (through include).
 
I

Ilan Berci

unknown said:
module X; def x; "x"; end; end
class Y; extend X; end
Y.x #=> "x"

Unfortunately, I'm fairly sure that would require separate modules for
class methods (through extend) and instance methods (through include).

Although the approach above is the one that I use, for the sake of
completenes there is also this...

irb(main):001:0> module X; def x; "x"; end; end
=> nil
irb(main):002:0> class Y
irb(main):003:1> class << self
irb(main):004:2> include X
irb(main):005:2> end
irb(main):006:1> end
=> #<Class:Y>
irb(main):007:0> Y.x
=> "x"
 
T

Trans

In the case of pulling in module methods as class (singleton) methods,
you can use extend instead of include. Your second example rewritten
with "extend" would work:

module X; def x; "x"; end; end
class Y; extend X; end
Y.x #=> "x"

Unfortunately, I'm fairly sure that would require separate modules for
class methods (through extend) and instance methods (through include).

Have a look at module/class_extension in Facets. This sophisticated
code was developed by Daniel Schierbeck with the help of a number of
people including myself, Nobu Nakada, Ulysses and Matz.


class Module


alias_method :append_features_without_class_extension, :append_features

# = class_extension
#
# Normally when including modules, class/module methods are not
# extended. To achieve this behavior requires some clever
# Ruby Karate. Instead class_extension provides an easy to use
# and clean solution. Simply place the extending class methods
# in a block of the special module method #class_extension.
#
# module Mix
# def inst_meth
# puts 'inst_meth'
# end
#
# class_extension do
# def class_meth
# "Class Method!"
# end
# end
# end
#
# class X
# include Mix
# end
#
# X.class_meth #=> "Class Method!"
#

def class_extension(&block)
@class_extension ||= Module.new do
def self.append_features(mod)
append_features_without_class_extension(mod)
end
end
@class_extension.module_eval(&block) if block_given?
@class_extension
end

private :class_extension

def append_features(mod)
append_features_without_class_extension(mod)
mod.extend(class_extension)
if mod.instance_of? Module
mod.__send__:)class_extension).__send__:)include,
class_extension)
end
end

end

class Class
undef_method :class_extension
end


T.
 
J

John Wilger

module A
def self.included(other)
super
other.extend(ClassMethods)
end

module ClassMethods
def foo
puts "bar"
end
end
end

Is a little cleaner I think.

Indeed. Both the code and the indentation (I swear I don't know where
all those spaces came from!) I suppose that's what I get for trying to
program in an email at the end of a very long day. :-(
 
J

John Wilger

Sure. You can do all sorts of tricks to get the desired behavior. The
point is simply "it ain't natural".

I guess that's subjective. I find it completely "natural". :)
There's a duck around here that doesn't know the difference ;-)

The duck might not, but the programmer does. The difference between
identity and behavior can make a complex domain easier to grok if used
appropriately.

Note that I am /not/ a proponent of static typing, and /am/ a
proponent of duck typing. The issue of inheritance vs. inclusion that
I mentioned regards human understanding of the code rather than the
compiler's.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,744
Messages
2,569,483
Members
44,903
Latest member
orderPeak8CBDGummies

Latest Threads

Top