Ben Giddings wrote:
I agree with Ryan’s reply to you. If I only need the one method
on the
object I’m interacting with – should that one call not be
allowed
simply because all of the other methods that I don’t need aren’t
consistent with what it should be?Ouch, this is one of the most difficult-to-understand sentences
I’ve seen in a
long time.I’ll pretend I understand it and give an example:
Get the contents of the object, if it acts like a string,
simply return it, if it acts like a file, return its contents,
if it acts like a frobnitz, foozle it.
def getContents(obj)
if obj.respond_to? :read
return obj.read
elsif obj.respond_to? :to_str
return obj.to_str
elsif obj.respond_to? :foozle
return obj.foozle
end
endclass EmailMessageBody < String
attr_accessor :read
def initialize
@read = false # By default, mark the message body as unread
end
endImagine that the “getContents” method is buried deeply within the
source code
to a library somewhere. The programmer would never see that
method, even if
he/she did see the commenting of the method. Now what happens
when I pass an
EmailMessageBody into getContents? It will see that it responds
to “read”
and return a boolean. That’s not what you want. That’s not what
the
documentation says, but that’s what it will do. This could be a
really hard
bug to track down.Testing to see if the “read” method is supported is not really
“duck typing”,
in the sense that you’re not really checking to see if it’s a
duck. Just
because the object implements a certain method doesn’t mean that
that method
does what you expect.In certain cases, checking to see if a method exists is enough.
The method
“to_str” is a prime example. If someone implements that method in
a way that
doesn’t return a string, they deserve whatever problems they get.
There are
plenty of examples of how to_str is used everywhere else, and the
function
name is pretty descriptive. Other functions are less obvious.Somebody else gave this example (though I added the “implements
Duck” that I
think was just accidentally left out):interface Duck
{
void quack();
}class MyDuck implements Duck
{
void quack()
{
System.exit();
}
}Here the “implements Duck” is essentially an agreement saying “I
want other
peole to see this as a Duck”. You’re free to break that
agreement, but if
you do, you can expect trouble.On the other hand:
class Psychiatrist
{
void quack()
{
quack_references++;
}
void shrink()
{
shrink_references++;
}
}Just because someone has a “quack” function/method, doesn’t mean
they want
their object to be treated as a Duck.
But if you have a Psychiatrist, why would you pass
it to a method that expects a Duck? The method
should be documented as expecting a duck, so passing
a Psychiatrist is wrong. The only difference
between Java and Ruby in this respect is that in
Ruby, you’d find out about it at runtime, which is
the best you can do anyway, since implements_interface?
is a method call anyway (granted, it’s easy to figure
out than having quack() do something wrong).
When you create a class and
add
methods to it, should you always have to say: “Hmm, what’s the
most popular
way _____ is used”, and make sure yours acts the same way?
I think that generally you design a class for a
specific purpose, and only pass it to methods that
expect it to act in the way it was designed.
My understanding was always that “implements Foo” in Java meant:
“my class
will define the following methods, in the way that Foo describes,
so you can
treat my class as a Foo”.As for the subject of why it is useful to implement a set of
methods rather
than just the one you care about, consider a function like:def dumpContents(obj)
if obj.respond_to? :write
obj.write(data)
obj.flush if obj.respond_to? :flush
obj.close if obj.respond_to? :close
elsif …
end
endI think it would be much more clear if you could simply say:
def dumpContents(obj)
if obj.implements_interface? :WriteableIO
obj.write(data)
obj.flush
obj.close
elsif …
end
end
Ruby is certainly more verbose in its checking of
type, when looked at this way.
Notice in your above example, the second version
isn’t the same as the first. The first version
accepts something that allows writing, but not
necessarily flush and close, while the second
version only accepts something that does all three.
What if you want to instead output to a string?
Flush and close don’t make sense in that case. So
you’d end up with an interface for each, which
would look like:
def dumpContent(obj)
if obj.implements_interface? :Writable
obj.write(data)
if obj.implements_interface? :Flushable
obj.flush
end
if obj.implements_interface? :Closable
obj.close
end
end
end
Which could be shortened, but it’s almost no better
than duck typing. The only thing that you gain is
that implementing Flushable implies that flush
has certain semantic meaning, which needn’t be the
case, and in any case can be solved by documenting
obj.
If you don’t want to do it that way, you’d need to
have separate cases for WritableIO and StringIO
which involves code duplication, so it’s not ideal
either.
Furthermore, what if you want a one-shot object?
Take the idea of anonymous inner classes in Java.
You don’t want to define a whole listener class
just to handle System.exit on window closing,
so you define a one-shot anonymous class like this:
addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e)
{
System.exit(0);
}
});
You could do this in ruby as follows:
obj = Object.new
def obj.windowClosed(e)
exit(0) # I’m not sure if this is the right method, but you get the idea
end
addWindowListener(obj)
Now, there’s no way to make obj extend a particular
interface other than using WindowListener.new as the
base instantiation. However, consider that you could
have something like:
exitObj = Object.new
def exitObj.windowClosed(e)
exit(0)
end
def exitObj.actionPerformed(e)
exit(0)
end
…
addWindowListener(exitObj)
closeButton.addActionListener(exitObj)
(this is all very Swing/AWT style)
So you could use the same object for all your close
operations (or all your open file operations, etc.).
However, if you used interfaces, exitObj would have
to implement both WindowListener and ActionListener,
and there’s no way to make an anonymous inner class
that does that (I think), so you’d need to define
a real class. In ruby it’s worse, because it would
require multiple inheritance, since it’d have to
extend both WindowListener and ActionListener, as
there are no interfaces, per-se in Ruby.
If I may comment on your getContents example, please take no offense, it seems a bit too general an
example for me to see it as a real problem. It
tests several dubiously related methods and and
returns their results. It doesn’t seem like something
that would be in real code.
It reminds me of a discussion I read on here from,
say, a year ago. Someone was talking about how in
ruby, because you can dynamically change methods,
you can never know whether your code will work or
not. Their point was that someone could re-define
how a method works and break a bunch of your code.
The question is, why would someone do that? And
even if they did, it would likely happen in testing,
and would produce errors. Unless you’re reading in
user input and eval()ing it, you won’t have this
problem, as long as you document and test.
I can’t fully answer your getContents method
because it lacks context. Something above getContents
would have to specify how objects passed in should
behave in documentation, and if someone doesn’t
follow that, then it breaks. Java interfaces
recommend how the implementation of their methods
behave. That needs to be recommended in Ruby
somehow, whether in documentation or in some other
way. read() could have two different meanings in
two different modules, so you shouldn’t pass an
object from the first to a function from the second.
Anyhow, I’m rambling on too much, so I’ll stop here
and possibly resume later.
- Dan
···
----- Original Message -----
From: Ben Giddings ben@thingmagic.com
Date: Friday, August 8, 2003 3:14 pm
Subject: Re: Ducktype, right?
On Fri August 8 2003 2:36 pm, Chris Morris wrote: