A wonderful OO Gotcha

jjohn on 2002-11-07T20:54:43

I like object-oriented programming. It can really simplify develpment when used judiciously. OOP class hierarchy are best kept simple and linear. Here's some fictionalized code that captures the essence of a bug the caused the Linux server it was running on to lock up so tightly, it needed to be cold booted.

###################
# File: Parent.pm #
###################
package Parent;

sub new {
  my $proto = shift;
  my $class = (ref $proto || $proto);

  if (my $r = Apache->request) {
     require Child;
     $b = Child->new;
  } 

  return bless {}, $class;
}
__END__

##################
# File: Child.pm #
##################
package Child;
@ISA = qw(Parent);

sub new {
   # some class inits go here
   return shift->SUPER::new;
}
__END__

###################
# File: caller.pl #
###################
use Parent;
$b = Parent->new;

The caller.pl script is called under mod_perl, but that's not particularly important (although the server crash was due to runaway Apache processes caused by my coding gaff above). Can you see why this code is likely to take a long, long, long time to run?

It's almost a while(1) loop.

The Parent defines a constructor called new(). The child overrides that that constructor, but still calls the parent constructor for some initialization. Today, I added a hack to the Parent class that required an object of the Child class to be constructed in the Parent's constructor. When caller.pl tried make a new Parent object, the Parent needed a new Child object, which calls the Parent's constructor, who in turn calls the Child's constructor, who in turn calls the Parent's constructor, who in turn...

Well, you get the picture.

The unsatisfactory (but effective) hack I came up with to break this cycle is in the Parent's new:

###################
# File: Parent.pm #
###################
package Parent;

sub new {
  my $proto = shift;
  my $class = (ref $proto || $proto);

  if (my $r = Apache->request
      && $class eq 'Parent') {
     require Child;
     $b = Child->new;
  } 

  return bless {}, $class;
}
__END__

If anyone (like say Damian Conway) would like to suggest a more polite way of doing this, I'm all ears. No, I can't move the Child code I need into Parent because that would be like grafting a second evil head onto my shoulders.

I share my errors so that you may avoid them in your lives. ;-)


confused

merlyn on 2002-11-08T02:04:47

I don't know why you have the $b in there. Does every parent object wrap a child object?

If you need a child object to create a parent, and can use a singleton, just lazily initialize it. The first pass will loop around a bit, but every pass after that, you're already done.

Re:confused

jjohn on 2002-11-08T05:57:38

I don't know why you have the $b in there. Does every parent object wrap a child object?

The parent merely needs to use (and then discard) the child object. To make things a little more concrete, the Parent class (not it's real name) is there to provide both a reasonable object constructor for children and AUTOLOAD object properties accessor methods. For public methods, this class has a series of methods that return DBI handles, so that DB credentials are centralized in one place.

Child classes provide convenient objects for complex data. For instance there is a child class called User that saves and stores data about a user to a mysql database. Because this is a web app, the User class is able to read an HTTP cookie (created with Apache::AuthCookie) that has the current user's credentials (ok, the cookie has a session ID which can be used to get at this info [Thanks Apache::Session!]).

Now why would I want an instance of User in this generic Parent class? Because sometimes I need to return different DBI handles based on the kind of User. The Parent has a method called companies_dbh() which returns a DBI handle to main database (whatever that means). Certain users will be looking at completely distinct data kept on a separate machine. Of course the data has the same structure as the "regular" data.

By using the User class, the Parent can smartly and magically deliver the correct DBI handle to existing scripts.

If this rambling explanation made any sense, you'll see that this is a fairly unique situation. I thought I'd post the code (strip of all it's complex context) because the essence of the bug is fairly simple. And yet it took me several minutes to locate the problem.

Re:confused

merlyn on 2002-11-08T12:17:28

Ahh... I get it. You've got the wrong model. If
Child->new
might not return an object of type Child, then you're asking the wrong class. You just need a
Parent->object_for_situation(@this)
method, which has a lazy initialization for creating a specific Parent handle to help decide which Child_nnn class to create.

There. That sorts it out. I'd be very confused if calling

Child->new
returned a Child2 instead of a Child. It doesn't make sense from an OO perspective.