The Case for Multiple-Inheritance

T

Trans

On the other hand, if you're reusing them in multiple places, e.g.:

class FooWithBarAndBaz
include Foo
include Bar
include Baz
end

class FooWithBarAndHoge
include Foo
include Bar
include Hoge
end

...you've got to create a distinct class up front for every permutation
of features. If you were composing objects instead, you'd be able to
compose objects dynamically, instead of being limited to the particular
compositions that you've baked into classes.

How would you do it? Ok, say we create classes for everything instead,
then how do create this dynamic composer? Is it a factory?

FooWithBarAndHoge = Composer.factory(Foo, Bar, Hoge)

So what's really that difference between this and the above? Or do you
have something else in mind?

Seems like in the end it's formally equivalent no matter how we do it,
only the underlying implementation differs. Of course there are
different trade-offs to consider for different implementations. But I
doubt any one is better than another in all ways. For instance using
delegation, we initialize four new objects in memory for every one
FooWithBarAndHoge, depending on what's more important to us, that
might not be as desirable.
I've come to recognize a tying of the internal representation to the
serialized form as a code smell, because usually (especially) when the
serialized form is designed to be human-editable, the requirements for
the structure of the two can be very different.

There doesn't need to be just one representation for all cases, does
there? A one-to-one map is great for a first layer. Then an adapter
can be created for any different internal representation that's
needed. But in my case, the whole point of the class is really to
provide read-access to that serialized data.

T.
 
T

Trans

[snip]
class Package
def self.from_yaml(yaml)
pkg = Package.new

YAML.load(yaml).each do |key, value|
case key
when 'package' then # ...
when 'version' then # ...
when 'created' then # ...
when 'homepage' then # ...
when 'devsite' then # ...
when 'authors' then # ...
when 'description' then # ...
when 'libpaths' then # ...
else
# save the unknown pairs somewhere so that subclasses can deal
# with them, too.
end
end
end
end

Just because your storage representation is flat doesn't mean your
in-memory object has to be. (And you can define your own #to_yaml on the
Package class that flattens things for saving.)

This is no different than what I wrote for Ruwiki four years ago (I
eschewed YAML because Syck was broken in a couple of different releases
of Ruby.)

Good. this is a much better approach. Obviously we can argue until
we're blue in the face and insult each other all day long and make
assumptions about what each is saying. So lets just scrap all that and
lets deal with some concrete code.

You have thus far claimed that any class with 50 attributes is dumb,
and that you would fire any employee of yours who used modules as I
have suggested. You have also so far shown a delegating form of the
Package class (which is a real production class for me) and have above
given an unfinished rendition of how you'd load it from the YAML
serialization (which is an import human readable part of this classes
purpose). So how about completing the whole thing?

Lets do this. I'll give you a complete list of attributes, their
aliases, default values and their categorizations. Then you use your
way and I'll use mine. Will present them here and let others judge
whose it more maintainable, reusable, easy to read, faster, etc.

And lets get a third party to arbitrate, so neither has to post their
solution before the other. James, since this is sort of like a Ruby
Quiz, would you like to do the honors?

What do you say, Austin?

T.
 
J

James Edward Gray II

James, since this is sort of like a Ruby Quiz, would you like to do
the honors?

I'll pass. I don't judge Ruby Quiz solutions. I just try to find
interesting things to talk about from some of them. We're all
winners that way. ;)

James Edward Gray II
 
M

Mauricio Fernandez

]
In Java/C++ inheritance is primarily a means of expressing type
relationships, and secondarily a means of sharing implementation.
Because of the statically typed nature of these languages, related
types HAVE to be related by inheritance. [...]
In a statically typed language the coupling of type specification with
implementation leads to a particular set of pressures on the design of
both the language and programs in that language which are different
from the pressures on Ruby and it's programs.

This is getting OT, but I just wanted to clarify that not all the languages
with a statically typed OO system confound subclassing and subtyping the way
C++ and Java do. In OCaml, for instance, two unrelated classes can represent
the same type, and a class might not be a subtype of its parent(s).
In fact, you can get what amounts to a sort of duck-typing, with the
guarantee that there will never be a "NoMethodError".

If in Ruby you have

def foo(x); x.bar end

in OCaml

let foo x = x#bar

and the foo function will work on any object with a 'bar' method, regardless
of the class (there are also "immediate objects"). The existence of that
method is checked statically.
 
A

Austin Ziegler

How would you do it? Ok, say we create classes for everything instead,
then how do create this dynamic composer? Is it a factory?

FooWithBarAndHoge = Composer.factory(Foo, Bar, Hoge)

So what's really that difference between this and the above? Or do you
have something else in mind?

Um.

class Foo; end

foo_with_bar_and_baz = Foo.new
foo_with_bar_and_baz.extend(Bar)
foo_with_bar_and_baz.extend(Baz)

foo_with_bar_and_hoge = Foo.new
foo_with_bar_and_hoge.extend(Bar)
foo_with_bar_and_hoge.extend(Hoge)

David Black has to repeat every so often for new folks that class only
implies the starting type of an object, not its type-at-the-moment.
Seems like in the end it's formally equivalent no matter how we do it,
only the underlying implementation differs. Of course there are
different trade-offs to consider for different implementations. But I
doubt any one is better than another in all ways. For instance using
delegation, we initialize four new objects in memory for every one
FooWithBarAndHoge, depending on what's more important to us, that
might not be as desirable.

Um. You're confusing delegation and composition again. There's a
difference, although composition is necessary for delegation. One does
not need to delegate if one composes. One only needs to delegate in an
Acts-As situation. The compositional approach has benefits as well, in
that you can reuse your objects, not just your class design.
There doesn't need to be just one representation for all cases, does
there? A one-to-one map is great for a first layer. Then an adapter
can be created for any different internal representation that's
needed. But in my case, the whole point of the class is really to
provide read-access to that serialized data.

Then why do you bother with a class? Why not just use the deserialized
hash? Unless, of course, you're adding methods that manipulate the
stored state. If so, then if you're grouping related values together
with modules, you're *still* better off using classes and composition
instead.

[snip]
Just because your storage representation is flat doesn't mean your
in-memory object has to be. (And you can define your own #to_yaml on
the Package class that flattens things for saving.)

This is no different than what I wrote for Ruwiki four years ago (I
eschewed YAML because Syck was broken in a couple of different
releases of Ruby.)
[snip]

You have thus far claimed that any class with 50 attributes is dumb,
and that you would fire any employee of yours who used modules as I
have suggested. You have also so far shown a delegating form of the
Package class (which is a real production class for me) and have above
given an unfinished rendition of how you'd load it from the YAML
serialization (which is an import human readable part of this classes
purpose). So how about completing the whole thing?

Why should I? I'm not interested in working on your project, whatever it
is. If I really had time to work on stuff that would become production
code, it would be PDF::Writer. Which I don't.

Psychological research has suggested that the number of things that most
humans can think about simultaneously is limited. The exact limit is
unclear and varies from person to person, but I haven't seen anyone that
suggests an upper limit higher than twelve or so.

Therefore, it will be easier on the developer and the people who use
your class to keep your classes conceptually smaller. It's better to
layer the classes because then each of those layers is a separate object
limiting the number of items that any individual has to think about.
What do you say, Austin?

Here's your ruler.

-austin
 
T

Trans

I'll pass. I don't judge Ruby Quiz solutions. I just try to find
interesting things to talk about from some of them. We're all
winners that way. ;)

Oh, not to judge. Just to hold the answers and present them on ruby-
talk at the same time. But I understand if you'd prefer not to.

T.
 
M

Martin DeMello

Wrong. Look VERY CAREFULLY at your code, Trans. How many of your modules
are used in more than one place?

If the answer is few, then you're doing it wrong and you're introducing
negative consequences in terms of maintainability, while not actually
increasing reuse. Most methods are specific behaviour for a given state.
Pretending that by extracting that into modules that youre increasing
reuse is nonsense. Unless you're actually reusing the code, you're not
increasing reuse.

Here I have to disagree. This is from some actual code I've written:
---------------------------------------------------------------------------
monitor.rb:

class Monitor
....
....
end

---------------------------------------------------------------------------
win_monitor.rb:

require 'monitor'
require 'win_metrics'

class Monitor
include WindowsMetrics
end

Monitor.new.start
---------------------------------------------------------------------------
lin_monitor.rb:

require 'monitor'
require 'lin_metrics'

class Monitor
include LinuxMetrics
end

Monitor.new.start
---------------------------------------------------------------------------

Now WindowsMetrics and LinuxMetrics are modules that exist solely for
the purpose of being included in Monitor, and you could argue that
instead I could have simply made win_metrics and lin_metrics reopen
the class and add the methods when required. The benefit I've gained
here is that for the cost of an extra layer of indirection and a few
extra lines of code, I've got a huge increase in readability for
someone who looks through my code, since the natural entry points are
win_monitor.rb and lin_monitor.rb. Also, should I ever wish to make a
program that is deployed on multiple platforms and dispatches at
runtime, I can trivially require both metrics files and not have to
worry that they'll stomp on each other by the mere act of requiring
the. I may not be promoting reuse, but I'm definitely increasing
maintainability.

martin
 
A

Austin Ziegler

Here I have to disagree. This is from some actual code I've written:

[snip]

Fair enough. I think it's an odd enough case, though. Even then, I'm
of the opinion that some of your design could have been different.
Specifically, why not something more transparent?

monitor = Monitor.new:)linux)

class Monitor
def self.new(platform, *args, &block)
mon = Monitor.alloc
mon.initialize(*args, &block)
case platform
when :windows
require 'win_metrics'
mon.extend(WindowsMetrics)
when :linux
require 'lin_metrics'
mon.extend(LinuxMetrics)
end
end
end
Now WindowsMetrics and LinuxMetrics are modules that exist solely for
the purpose of being included in Monitor, and you could argue that
instead I could have simply made win_metrics and lin_metrics reopen
the class and add the methods when required. The benefit I've gained
here is that for the cost of an extra layer of indirection and a few
extra lines of code, I've got a huge increase in readability for
someone who looks through my code, since the natural entry points are
win_monitor.rb and lin_monitor.rb. Also, should I ever wish to make a
program that is deployed on multiple platforms and dispatches at
runtime, I can trivially require both metrics files and not have to
worry that they'll stomp on each other by the mere act of requiring
the. I may not be promoting reuse, but I'm definitely increasing
maintainability.

Your pseudo-code also suggests that you're not just using
WindowsMetrics and LinuxMetrics to provide the entire body of the
Monitor class, which is *exactly* what Trans was advocating.

With WindowsMetrics and LinuxMetrics, you may not be sharing
code/implementation, but you're definitely sharing interface. Your
example is probably not much different than:

class AbstractMonitor; end
class WindowsMonitor < AbstractMonitor; end
class LinuxMonitor < AbstractMonitor; end

(The latter approach, or even the Monitor.new approach I showed may be
better if you ever wanted to support remote monitoring. But that's a
different issue entirely and the decision on the architecture
shouldn't be based on what you think you might need.)

-austin
 
A

Austin Ziegler

Wrong. Look VERY CAREFULLY at your code, Trans. How many of your
modules are used in more than one place?

If the answer is few, then you're doing it wrong and you're
introducing negative consequences in terms of maintainability,
while not actually increasing reuse. Most methods are specific
behaviour for a given state. Pretending that by extracting that
into modules that youre increasing reuse is nonsense. Unless you're
actually reusing the code, you're not increasing reuse.
Here I have to disagree. This is from some actual code I've written:
[snip]
[double snip]

I would, I think, do it differently than either of you. I suppose I'm
wrong then?

If that's what you've taken out of the discussion, then you need to work
on your reading comprehension.

There are many ways to implement similar functionality. Some ways are
better than others. Some ways abuse facilities that weren't meant for
that. Some ways feel "native" to the language. And sometimes that's not
an important feature.

But there's definite code smell if all you do is what Trans does with
respect to composing classes exclusively from modules. I certainly
wouldn't want to maintain it.

-austin
 
T

Todd Benson

Wrong. Look VERY CAREFULLY at your code, Trans. How many of your
modules are used in more than one place?

If the answer is few, then you're doing it wrong and you're
introducing negative consequences in terms of maintainability,
while not actually increasing reuse. Most methods are specific
behaviour for a given state. Pretending that by extracting that
into modules that youre increasing reuse is nonsense. Unless you're
actually reusing the code, you're not increasing reuse.
Here I have to disagree. This is from some actual code I've written:
[snip]
[double snip]

I would, I think, do it differently than either of you. I suppose I'm
wrong then?

If that's what you've taken out of the discussion, then you need to work
on your reading comprehension.

I smell defensiveness. Maybe I need to work on being more hard-core
like yourself.

Todd
 
M

Martin DeMello

Specifically, why not something more transparent?
case platform
when :windows
require 'win_metrics'
mon.extend(WindowsMetrics)
when :linux
require 'lin_metrics'
mon.extend(LinuxMetrics)
end

Mostly because the metaphor I was going for was a set of building
blocks to assemble a monitor, so it made sense (or at least it seemed
like the most aesthetic design to me) for the "actual" program to
explicitly pull in all its pieces and put them together.

But what I was trying to get at there was that modules form a
convenient way to put a bunch of functionality together and then drop
it into a class in a very self-documenting manner - it's not about
reuse so much as packaging.

martin
 
R

Robert Dober

Um.

class Foo; end

foo_with_bar_and_baz = Foo.new
foo_with_bar_and_baz.extend(Bar)
foo_with_bar_and_baz.extend(Baz)

foo_with_bar_and_hoge = Foo.new
foo_with_bar_and_hoge.extend(Bar)
foo_with_bar_and_hoge.extend(Hoge)
Difficult to judge

Readability: ~0 (interesting approach)
Maintainability: -5 (a nightmare if someone forgets the extend on new objects)
Reusability: -1 ( not bad but why make me so much type???)
Actually I wonder where is the catch here

class FooBar < Foo
include Bar
end

%w{ What Ever }.each do
| another |
class FooBar<another> < FooBar
include <another>
end
end

Now I do not think that the above is the best way to do it, I'd be
happy to delegate
to @<another> or even to @bar, actually I was dying for a delegation
solution from you Austin, because I am an absolute newbie concerning
the usage of delegation, but no such luck

BTW reuse is not the *only* reason to factorize, readability is a very
important one
so if I had 50 methods in my Module/Class I had desperately seek
logical units to be
factorized into submodules, even if they where not used anywhere else.
I generalize this from method decomposition which really made a difference in
my coding style.

Robert
 
R

Rick DeNatale

]
In Java/C++ inheritance is primarily a means of expressing type
relationships, and secondarily a means of sharing implementation.
Because of the statically typed nature of these languages, related
types HAVE to be related by inheritance. [...]
In a statically typed language the coupling of type specification with
implementation leads to a particular set of pressures on the design of
both the language and programs in that language which are different
from the pressures on Ruby and it's programs.

This is getting OT, but I just wanted to clarify that not all the languages
with a statically typed OO system confound subclassing and subtyping the way
C++ and Java do. In OCaml, for instance, two unrelated classes can represent
the same type, and a class might not be a subtype of its parent(s).

Yes, I suppose I should have used something like "in languages in the
C++ (or perhaps to give due credit Simula-67) family..." But I
suspect that many of those craving MI come from THAT particular branch
of experience.

There are certainly statically typed languages which push the envelope
on what can be expressed via types, but they also push the design
pressures in different directions.
 

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

Forum statistics

Threads
473,968
Messages
2,570,149
Members
46,695
Latest member
StanleyDri

Latest Threads

Top