http://qs321.pair.com?node_id=465614

mrborisguy has asked for the wisdom of the Perl Monks concerning the following question:

This week is Learn-How-To-Program-OO-Correctly week for me, so here's another question about it!

I want to make a class called "Node", like the nodes you find in trees or lists (more for playing than actually for anything useful). This Node class has a "parent" attribute. I want my node class to be inheritable, but ultimately, the "parent" of any object should at least be of type Node.

if( $value->isa( "Node" ) ) { # It's okay to put $value into the parent attribute }

However, I've also got a test that I'm building along with this node class, in which I have these lines:

$obj->parent( 0 ); is( $obj->parent, undef );

(undef because I haven't changed the parent, and since 0 isn't a Node, the value doesn't get changed).

Okay, so I guess I better get to the question soon. I want to check if $value-isa( "Node" ), but the problem is that $value can be anything, and if it's a SCALAR, then isa doesn't work! I tried this code, which works (I think):

if( ref( $value ) && ref( $value ) ne "SCALAR" && ref( $value ) ne "AR +RAY" && ref( $value ) ne "HASH" && $value->isa( "Node" ) ) {

I'm wondering if there is a better way that doesn't look quite as ugly. Anybody have any advice for me?

    -Bryan

Replies are listed 'Best First'.
Re: isa() on any scalar
by revdiablo (Prior) on Jun 10, 2005 at 17:56 UTC
    I want my node class to be inheritable, but ultimately, the "parent" of any object should at least be of type Node.

    Personally, I don't like using class membership to determine whether an object is "ok" or not. I mean, someone could use Node as a base class, but override all the methods to do anything they want anyway, so what's the point of looking for it? I'd rather go with what is sometimes called duck typing. That is, if it walks like a duck, and talks like a duck, it must be a duck. In Perl terms, this means I like to use can to check if an object has the methods I am going to call on it. For example:

    sub use_object_somehow { my ($obj, $foo, $bar) = @_; carp "Object doesn't quack right" and return unless $obj->can("foo") and $obj->can("bar"); $obj->foo($foo); $obj->bar($bar); }

    This test is simple enough that I don't think it's too onerous to include in any subroutine that handles an incoming object, but it can be abstracted into a subroutine if so:

    sub _check_obj { my ($obj, @methods) = @_; for (@methods) { return unless $obj->can($_); } return 1; }

    Then it would be used like:

    sub use_object_somehow { my ($obj, $foo, $bar) = @_; carp "Object doesn't quack right" and return unless _check_obj($obj, qw(foo bar)); $obj->foo($foo); $obj->bar($bar); }

      Thank you for your post. I'm going to spend some time re-thinking my design, and seeing if this is a better way to do things.

      Bear with me as I reply to your post... I'm certainly not replying because I think your way is inferior to the way I do it, because I'm new to this, so I'm not too set in my ways. But here I go...

      Personally, I don't like using class membership to determine whether an object is "ok" or not. I mean, someone could use Node as a base class, but override all the methods to do anything they want anyway, so what's the point of looking for it?

      Alright, so what's the point of even using my Node as a base class? If it's going to override my methods and do whatever it wants, there's really no point in inheriting from my class. But I know, this was probably just an overstatement to make a point.

      I'd rather go with what is sometimes called duck typing. That is, if it walks like a duck, and talks like a duck, it must be a duck. In Perl terms, this means I like to use can to check if an object has the methods I am going to call on it.

      This duck typing seems to be allowing for objects that aren't inheriting my class to use my classes methods? What's the point of that? If it's going to use them, why not just inherit?

      However, I don't think I LOSE anything by using your duck typing either. This is what I like about your method, I gain the ability to cater to other classes, as long as they impliment a certain interface (basically, the specific interface of can("foo") and can("bar") ), but I don't lose anything. Basically what I'm looking for to see if an object is or at least inherits from my class probably really does mean I just want to be able to use the methods in this theoretical interface. It probably wouldn't be very good to be able to play with the internals of the object anyway (which might be what I was actually subconsciously thinking about doing).

      Wait... just thinking this through quick, you're free to comment on this thinking (well, actually you're free to comment on anything in this post). I have my methods, which are an interface for users and other classes, right? But I can change how things are stored and how things are done within my class, as long as I don't break "the contract" that the methods won't change. So what about playing with the internals of a class that should be the same as mine? What I'm thinking is that, what if later on I decide that I tend to try to find a Node's children more often than it's parent? I've made a promise that I won't change my 'parent' method, but if I just modify some things, I can make 'node' store an array of children, so my $self->parent( $parent ) call might do something like push @$parent->{ children } $self;. Would that be a bad thing? (I'm not sure, I'm actually asking.) [Oh, turns out I WAS subconsciously considering playing with the internals of another object, even if it's supposed to be the same class]. Okay, so if I did end up thinking this was better, how can I modify my classes internally without breaking the contract? Or does this mean I have to do a little bit more planning up front, and that actual "not breaking the contract" deal is more for optimization of functions and storage and the like?

      Once again, I re-iterate that I'm not questioning your judgement, in fact I respect it. I'm just asking questions and probing to see if I can a) think some things through myself, and b) learn from more experienced monks' wisdom.

          -Bryan

        I've made a promise that I won't change my 'parent' method, but if I just modify some things, I can make 'node' store an array of children, so my $self->parent( $parent ) call might do something like push @$parent->{ children } $self;. Would that be a bad thing?

        Yes it would. Better would be to have the parent itself take responsibility for such caching, by supplying a suitable method:

        sub children { my $self = shift; $self->{cached_children} ||= do { # find the children, and end up with (say) an array \@result; }; @{ $self->{cached_children} }; }

        By having the method that knows how to find the children be responsible for caching it, someone using a different implementation gets to override the children method, supply the appropriate logic for locating the children, and make their own judgement on whether to cache or not.

        In an ideal world, then, any instance method would never modify the innards of any object except the instance it was called on - any other objects should be accessed by method calls only.

        Hugo

        Alright, so what's the point of even using my Node as a base class? If it's going to override my methods and do whatever it wants, there's really no point in inheriting from my class.

        That's exactly right. Sometimes there really is no point in inheriting from your class. By checking for isa("Node"), you're requiring the user to do so, even if there is no other reason. The duck typing lets them use inheritance if it makes sense, or a complete reimplementation if that makes more sense. It gives the user more flexibility.

        PS: Sorry I didn't reply to your later thoughts, I haven't had a chance to digest them all the way. I might revisit this post later and see if I have anything more to say.

      One potential caveat is that can() will report false if the method in question is handled by autoload, and is not in the symbol table.

      Of course AUTOLOAD is pure evil and must be stopped. (being facetious). But seriously, this can be an issue

        can() will report false if the method in question is handled by autoload

        Anybody who uses AUTOLOAD in this way should also override can appropriately.

      Personally, I don't like using class membership to determine whether an object is "ok" or not. I mean, someone could use Node as a base class, but override all the methods to do anything they want anyway, so what's the point of looking for it? I'd rather go with what is sometimes called duck typing. That is, if it walks like a duck, and talks like a duck, it must be a duck.

      This is reminiscent of Java interfaces. The techniques to do Java-like interfaces in Perl that I know of rely on creating a base class in which all the mandatory methods die, which forces the children classes to implement them. With this approach, however, the simplest way to test for conformance to the interface would still be an isa-type test. See Java-style interfaces in Perl.

      My main objection to simply testing for the ability to execute certain methods is that, except for the simplest interfaces, it is unlikely that a class will fully implement an interface, including giving the correct names to all the methods, without the author being previously aware of the specs for the interface. If the author is aware of the specs, and these specs are enforced through an interface-like class as described above, then they may as well stick this interface-class in the appropriate @ISAs, and use isa to check for compliance.

      the lowliest monk

        This is reminiscent of Java interfaces.

        That's not what I was getting at. I didn't mean to check for every method that the object "should" have every time you take it in. Notice what I said in my original post:

        I like to use can to check if an object has the methods I am going to call on it

        I have highlighted the point you seem to have missed. I only check for the methods I am actually going to use. This way, someone can implement only the part of the interface that they need to, instead of the whole thing. Like I said in a later reply to the OP, it's about flexibility for the end user.

Re: isa() on any scalar
by tlm (Prior) on Jun 10, 2005 at 17:45 UTC

    Update: I agree with chromatic's point. When I thought of the solution below, I erroneously conflated the OP's problem with a different one (one resulting from using ref to detect whether something is an 'ARRAY').


    Use UNIVERSAL::isa instead. I.e.

    UNIVERSAL::isa( $value, "Node" )

    the lowliest monk

      No, don't do that.

      Assume that UNIVERSAL::new existed. Would you suggest always creating objects with UNIVERSAL::new( $class_name, %data ); too?

      Try instead:

      my $is_node = eval { $value->isa( 'Node' ) }; # handle not node case if $@ is true or if $is_node is false

      This has the advantage of allowing subclasses of Node to override isa() as they see fit, of allowing objects that perform the Node role (see Class::Roles, for example) to work appropriately, and of catching an error if $value is an invalid invocant. It also avoids calling a parent class method specifically on a potential instance of a derived class, which is a bad idea.

      Update: Expanded code slightly.

        No don't do this either. An exception should only be used to catch exceptional events. Since it seems that $value is just as likely not to be a 'Node' as it is to be one you should not use an exception to test it.
        -- gam3
        A picture is worth a thousand words, but takes 200K.

      Thank you, this is exactly what I wanted. You get my ++ for answering the question. But after reading revdiablo's post below, I'm not sure if this is really what I want anymore. Maybe I'll have to spend some time rethinking my design.

          -Bryan

      No no no!

      Every time a module does that a kitten using Test::MockObject dies.

      Please think of the kittens!

      -nuffin
      zz zZ Z Z #!perl
        Perhaps the kitten should choose a better way of mocking then. I suggest using the Parisian::Waiter::Scornful module.
Re: isa() on any scalar
by gam3 (Curate) on Jun 11, 2005 at 02:37 UTC
    You can use Scalar::Util::blessed to test the reference
    use Scalar::Util qw (blessed); if (blessed($value) and $value->isa('Node')) { # }
    -- gam3
    A picture is worth a thousand words, but takes 200K.
      Two things: i) are you building an OO module for phylogenetics? (If yes, let's join forces); ii) here's how I did it:
      sub parent { my $self = $_[0]; if ( $_[1] ) { my $parent = $_[1]; if ( $parent->can('parent') ) { $self->[PARENT] = $parent; } else { my $ref = ref $self; carp "parents can only be $ref objects"; return; } } return $self->[PARENT]; }
      This follows the "if it walks like a duck" paradigm. Not saying that this is the best way to do it, but it works for me.

        Phylogeny: (n.)
        1. The evolutionary development and history of a species or higher taxonomic grouping of organisms. Also called phylogenesis.
        2. The evolutionary development of an organ or other part of an organism: the phylogeny of the amphibian intestinal tract.
        3. The historical development of a tribe or racial group.

        Okay, just looked it up... nope, that's not what I'm doing at all. Sorry! Thanks for the piece of code though!

            -Bryan

      After looking around, trying to figure out what I want to do, I decided to use the 'walk like a duck' approach and use can. However, the same problem occured! So I use this solution together with can. Thanks for this one!

      use Scalar::Util qw(blessed); if ( blessed( $value ) and $value->can( 'parent' ) ) { # }

          -Bryan