http://www.perlmonks.org?node_id=550334

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

I've been taking my first steps with merlyn's CGI::Prototype, which I find pretty nice overall, but I'm running into a conceptual block here. In summary: inheritance doesn't work as I expect. Not even Class::Prototyped-type inheritance. In fact, I can't figure out how inheritance works with CGI::Prototype!

The script and module below illustrate the problem.

I'll describe the script first. Most of it is assignments and definitions. Things don't get moving until the call to activate at the very end. Users of CGI::Prototype will find this pattern familiar. They will also recognize the methods defined in package Zero in the middle block as being among those called by CGI::Prototype::activate.

# cgi.pl use strict; use warnings; @One::ISA = @Two::ISA = 'Zero'; { package Zero; use base 'CGI::' . shift; sub dispatch { shift->param ? 'Two' : 'One'; } sub render { print my $self = shift, "\n"; print "$_: " . $self->param( $_ ) . "\n" for $self->param; } } @MY_Zero::ISA = 'Zero'; shift->activate; __END__

Passing the string 'Prototype' as the first argument to the script will cause Zero to be a subclass of CGI::Prototype. (The package I define below is called CGI::Classic, so 'Classic' is another meaningful value to pass as the first argument to the script.)

The dispatch method, which is called by the inherited activate method, returns 'One' if there are no CGI parameters, and 'Two' otherwise. Eventually this will result in the execution of either One->render or Two->render, as the case may be. The render method as defined in Zero prints the caller and the CGI params, if any.

In its next-to-last step the script defines another subclass of Zero, called 'MY_Zero'. Finally, it calls activate on the class passed as the script's second argument. Subsequent arguments, if any, should be in the form of 'key=value' strings.

With 'Prototype' as first argument, things work as expected if the second argument is 'Zero':

% perl cgi.pl Prototype Zero
One
% perl cgi.pl Prototype Zero foo=1 bar=2
Two
foo: 1
bar: 2

...but bomb if the second argument is 'MY_Zero':

% perl cgi.pl Prototype MY_Zero
One
Content-type: text/plain

ERROR: Two->initialize_CGI not called at /usr/lib/perl5/CGI/Prototype.pm line 173.

To show what I had expected to happen, I defined CGI::Classic. This module is, first, a drastically simplified toy version of CGI::Prototype. In particular, its (tiny) activate method doesn't even follow the original's logic (e.g. no testing of the result of the respond method, etc.). But the most fundamental difference between CGI::C and CGI::P is that CGI::C does not use Class::Prototype at all:

# CGI/Classic.pm use strict; use warnings; { package CGI::Classic; use CGI; my $CGI; sub activate { $CGI = CGI->new; shift->dispatch->render; } sub param { shift; $CGI->param( @_ ) }; sub dispatch { die 'subclass responsibility' } sub render { die 'subclass responsibility' } } 1; __END__

Now the script works with both 'Zero' and 'MY_Zero' as second argument.

% perl cgi.pl Classic MY_Zero
One
% perl cgi.pl Classic MY_Zero foo=1 bar=2
Two
foo: 1
bar: 2
% perl cgi.pl Classic Zero baz=3 frobozz=4
Two
baz: 3
frobozz: 4

I guessed that the problem resulted from the fact that Class::Prototyped uses a model of inheritance that is different from Perl's standard model, so I also tried a version of the script in which the last call to activate has the following form:

shift; # discard script's second argument Zero->new( 'parent*' => 'Zero' )->activate;

...but I get the same error as before.

Here's where I run out of steam. I find the innards of Class::Prototyped pretty difficult to understand, and I'm not even sure that that's were the problem is.

I ran into this problem when I tried to write a test script for some CGIP-based classes. I wanted to override some methods of the main class for testing, and that's when I created a class analogous to 'MY_Zero' above.

So I have a question and a comment. The question is: how do I do what I want to do (i.e. create a subclass of my main class for the purpose of overriding its methods for testing)? The comment is that the problem illustrated above strikes me as pretty serious, because it completely defeats reasonable expectations about how inheritance should work.

(Something tells me I'm going to learn a lot of Perl soon...)

the lowliest monk

Replies are listed 'Best First'.
Re: Subclassing and CGI::Prototype
by merlyn (Sage) on May 19, 2006 at 16:39 UTC
    The problem is that there's also "instance" data in the class that gets the "activate"... in this case, the CGI object ends up in your "My_Zero" class, which is not in the inheritance path for One or Two, and so those pages don't have access to the CGI object.

    I've thought about fixing that by forcing the CGI slot to always be located up in CGI::Prototype, but I have to think through that carefully to make sure that doesn't break some other assumption.

    -- Randal L. Schwartz, Perl hacker
    Be sure to read my standard disclaimer if this is a reply.

      Thanks.

      I've thought about fixing that by forcing the CGI slot to always be located up in CGI::Prototype, but I have to think through that carefully to make sure that doesn't break some other assumption.

      Hey, that's what unit tests are for! :)

      Anyway, in the meantime, I've added a few notes to the CGI::Prototype page at AnnoCPAN.

      the lowliest monk

        Try this patch. Change $self->reflect->addSlot to __PACKAGE__->reflect->addSlot in CGI/Prototype.pm in the initialize_CGI subroutine. I think that'll do it right. But I'm trying to think of how to have subclasses also be able to select where the CGI slot should be.

        -- Randal L. Schwartz, Perl hacker
        Be sure to read my standard disclaimer if this is a reply.

Re: Subclassing and CGI::Prototype
by tlm (Prior) on May 18, 2006 at 21:22 UTC

    OK, I'm beginning to figure out what the problem is. I'm not quite to the point where I feel I fully understand what's going on, but adding the following line to the code before the call to activate fixes the problem:

    unshift @$_, 'MY_Zero' for \@One::ISA, \@Two::ISA;

    the lowliest monk