Beefy Boxes and Bandwidth Generously Provided by pair Networks
There's more than one way to do things

Modification of @ISA at run time

by jkeenan1 (Deacon)
on Sep 01, 2005 at 03:45 UTC ( #488288=perlquestion: print w/replies, xml ) Need Help??
jkeenan1 has asked for the wisdom of the Perl Monks concerning the following question:

In Chapter 16: Class Hierarchies, subsection Inheritance (bottom p. 360) of Perl Best Practices, TheDamian recommends defining "a class's hierarchy declaratively at compile time, using the standard use base pragma. He argues that

"this approach discourages messing about with class hierarchies at run time, by reassigning @ISA. The temptation to modify @ISA at run time is usually a sign that your class might be better implemented as a factory, a fašade, or with some other meta-object technique. (Emphasis added.)

Atypically, Damian doesn't provide an example of the code that would implement one of those other techniques. Not being experienced in design patterns, I'm somewhat at sea here. So I'm wondering if someone could provide a simple example of, say, the alternative to something like this:

BEGIN { use vars qw ( $VERSION @ISA ); $VERSION = '0.01'; require Solar::Data; require Lunar::Data; push @ISA, qw( Solar::Data Lunar::Data ); };

And later, at run time ...

if ($sun_rises_in_west) { require Astrological::Data; unshift @ISA, Astrological::Data; }

... called because, under certain unusual conditions, I want to make sure that the inheritance mechanism finds an Astrological::Data method (say, get_sunrise_time) before it finds Solar::Data::get_sunrise_time().

What would the alternative approach look like in code? Why, particularly in a simple case, would it be superior?

Thank you very much.

Jim Keenan

Replies are listed 'Best First'.
Re: Modification of @ISA at run time
by TheDamian (Priest) on Sep 01, 2005 at 06:50 UTC
    First some definitions:
    A class that generates instances of other classes, choosing which class to build on the basis of run-time information.
    A class that acts as a front-end for one or more other classes, delegating the actual work to those classes as appropriate.

    In your particular example, what you want is a factory: a class that generates objects according to where the sun rises. For example:

    # Rational helper class... package Astro::Data::Scientific; use base Solar::Data; use base Lunar::Data; # <define methods here> # "Differently rational" helper class... package Astro::Data::Mythological; use base Astrological::Data; # Pre-empt normal methods use base Astro::Data::Scientific; # <overload any methods here that the # heavens dictate should behave differently> # Factory class... package Astro::Data; # Constructor returns instances of helper classes, # depending on run-time astro[nl]o[mg]ical conditions sub new { shift; # off class name if ($sun_rises_in_west) { return Astro::Data::Mythological->new(@_); } else { return Astro::Data::Scientific->new(@_); } } # <no methods of its own, since it's never actually instantiated>
      Thanks, Damian and all the others who took the time to reply.

      I think I was mostly thrown by the unfamiliar nomenclature. It turns out that I've already used the factory concept (in projects other than the current one) without knowing the fancy name. So I shouldn't have too much trouble implementing one now.

      Jim Keenan

      Here is a cleaned-up version of Damian's example, along with a test.

      I hope this example is (a) correct and (b) useful to the next person who wonders about this.

      Jim Keenan

Re: Modification of @ISA at run time
by nothingmuch (Priest) on Sep 01, 2005 at 07:04 UTC
    My solution is always to deglobalize.

    If $sun_rises_in_west is a global condition, encapsulate it.

    Sometimes you want to calculate things for the parallel universe you don't live in. What is your user going to do then?

    To emulate global behavior, just make your default instantiation take into account the fact that the default $sun_rises_in_west_flag is x and not y, and save it as instance data.

    To dispatch based on instance data, the most useful technique is delegation - encapsulate your instance data in an object, and have your object as that object to polymorphically get_sunrise_time based on the kind of object it is.

    Playing with multiple inheritence implies that your object hierarchy is not only a definition, it also encapsulates state.

    While this feels very cool and meta-meta-meta-fun, it's usually not very good for:

    • testing
    • stability
    • readability
    • maintainability

    Update: blazar requrested that I added an example, based on TheDamian's reply..

    ## this starts like damian's example: # Rational helper class... package Astro::Data::Scientific; use base Solar::Data; use base Lunar::Data; # <define methods here> # "Differently rational" helper class... package Astro::Data::Mythological; use base Astrological::Data; # Pre-empt normal methods use base Astro::Data::Scientific; # <overload any methods here that the # heavens dictate should behave differently> # Delegating class package Astro::Data; sub new { my $class = shift; my $sun_rises_in_west = @_ ? shift : $default_sun_rises_in_west; my $delegate = ("Astro::Data::" . ($sun_rises_in_west ? "Mythologi +cal" : "Scientific"))->new; bless { delegate => $delegate, }, $class; } # define like this or use Class::Accessor sub sunrise_delegate { $_[0]{delegate}; } # define like this or use Class::Delegation sub get_sunrise_time { my $self = shift; $self->sunrise_delegate->get_sunrise_time(@_); }
    The difference between this and the factory approach is not very evident at this scale. This has some disadvantages, namely in being a harder to set up, and less efficient, but it scales better when you have several variadic sets of methods... For example, if $sun_rises_in_west and $number_of_moons > 1, and so on, it might be easier to make a delegate for each, instead of combining each possibility into it's own class.

    Update 2: I should mention what I really like about delegates - they allow you to keep things better separated.

    Instead of your entire set of capabilities (methods) being mushed into one object, you get a higher degree of separation. Whether this boundry is later blurred for convenience as far as code using the delegating object is concerned is irrelevant - the implementation of the delegating object stays cleaner.

    This is useful when you have conceptual objects that perform many roles. For example, I have a system where data can be used as resources in a resource allocator.

    Instead of making the data, which already has the mess involved with persistence and what not added in, inherit yet another class (the one for a resource), i simply make a new resource class, that is a thin wrapper around this object. This object then becomes a delegate of the resource class.

    This has a conceptual simiarity to the MVC model. The model part shows you what the is structured like. The control decides what to do with the data, andn the view, through the controller, displays the model. The delegating object is a bit like a controller, and the delegate is a bit like a model in this sense.

    zz zZ Z Z #!perl

      Apropos of the recent discussion "How to differentiate hash reference and object reference?", how do you recommend handling isa when using delegation like this? Do you also define an isa that passes the isa call through to the delegate?

      sub isa { my ($self, $class) = @_; return $self->{delegate}->isa($class); }

      If so, it's another reason why people should avoid this:

      if ( UNIVERSAL::isa( $obj, 'Astro::Data::Mythological' ) ) { # whatever }


      Code written by xdg and posted on PerlMonks is public domain. It is provided as is with no warranties, express or implied, of any kind. Posted code may not have been tested. Use of posted code is at your own risk.

        Well, the reimplementation of isa is probably the way to go, but normally I've found that the greater the all-knowingness of an object, the less I need to introspect it.

        In a sense the behavior is transparent, instead of using a delegate there could have been an if/else on the member data in the single classes get_sunrise_time

        My point is that when designing in this direction, usually you want to pretend that the object is the same, and the delegate is just an implementation detail. In situations when the role is truely different you don't want to instantiate from the same class anyway, you have have two top level classes, one for west-rising suns, and the other for east rising ones.

        In fact, that is exactly what the delegation model is - it's two independant classes, completely unbound to each other, but interchangable, and an object which can reuse them to give you an alternative with a single point of entry.

        Good examples of this kind of reuse can be dug up by querying Any... These are modules which usually wrap around several modules which do the same role, but have different features. The wrapper modules hide the details of the difference from the user, letting the user give the generic module more kinds of input, without caring which bunch of code really does the job at the end.

        To wit, after saying

        my $a = Archive::Any->new($archive_file);
        I don't care if what I got was reblessed into a different class, or wheter it has a delegate, or whether it uses MMD inside. In fact, it probably isn't Archive::Tar at all, in any sense, since it doesn't provide it's full interface.

        When ->isa checks are used with respect to the delegate for the prupose of type checking (what kind of archive format do you encapsulate, mr Archive::Any instance?), then it's perhaps a design problem, because not caring is the reason we wrapped it in the first place.

        zz zZ Z Z #!perl
Re: Modification of @ISA at run time
by pg (Canon) on Sep 01, 2005 at 04:51 UTC

    In this case, what you really should do is not to mess with the inheritance at the run time, but to have a class called SolarSystemDataFactory.

    This class should have a method that returns you an instance of the right class based on your input. For example, it takes a parameter saying whether the sun rises on the east on west, and base on that, it either returns you an object of Solar::Data or Nemesis::Data.

Re: Modification of @ISA at run time
by gargle (Chaplain) on Sep 01, 2005 at 06:14 UTC


    your last example look a lot like a goto!

    In fact, it works a bit like the goto considered harmfull. It all comes down to code reuse. If your object/class modifies, under certain unusual conditions, it's inheritance tree and thus its behaviour, it becomes very difficult to inherit from such class. You'd have to know in advance all possible scenarios of behaviour of your object/class before reuse. Checking wheter or not your object is of a certain type becomes very difficult because its type depends on an internal 'if'.

    It's better (easier for maintenance, easier to comprehend, easier for reuse) to fix your inheritance first, at compile time maybe, or by convention by never changing @ISA.

    if ( 1 ) { $postman->ring() for (1..2); }
Re: Modification of @ISA at run time
by mkirank (Chaplain) on Sep 01, 2005 at 06:04 UTC
    Greetings Jim :-)
    I guess one reason might be the example given below
    Suppose your whole application has a few lakhs lines of code and If some body has to maintain this , It will be easier for them if they can figure out by looking at the class (by looking at use base), If there are such runtime heirarchies It would be very difficult for them to figure out the heirarchies
    If they have to create a new class which uses your class as a base class, they will have to know the Inner workings of your class .. -Kiran

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://488288]
Approved by Tanktalus
Front-paged by Tanktalus
and all is quiet...

How do I use this? | Other CB clients
Other Users?
Others wandering the Monastery: (8)
As of 2018-05-22 10:34 GMT
Find Nodes?
    Voting Booth?