Reputation: 13842
I'm trying to incorporate an old version of a module dynamically
We currently have a class that uses a module...very simplified for the question
use Modulev1;
our @ISA = qw( Modulev1 );
sub new {
my $class = shift or return
my $self = $class::SUPER::new( @_ ) or return;
$self->do_stuff();
$self;
}
We have an old module that has the same method names/variables, but different coding for a different version api on a remote server, that I'd like to 'plug in' as an option.
use Modulev1;
use Modulev2;
our @ISA = qw( Modulev1 );
sub new {
my $class = shift or return;
my %args = @_;
if( $args{ use_version_2 } ) {
@ISA = qw( Modulev2 );
}
my $self = $class::SUPER::new( \%args ) or return;
$self->do_stuff();
$self;
}
This seems to work in one way (gets results), but it feels like the wrong approach, and I get some warnings like 'Constant subroutine module::variable redefined at /opt/perl/lib/5.30.1/Exporter.pm' which makes sense, as it's trying define those constants twice in each version, and that feels wrong and prone to future bugs.
So there's 2 main questions
1) Is there any issue with reassigning @ISA, like I have.
2) Is there a better clean way to try and use 2 versions of the same module in code, rather than this ugly error prone hack ? It feels like I should somehow be keeping the modules more separate, so they can't try and do things like redefine constants.
Edit: I've also tried dynamically adding the 'use module' case, but that seems to have the same issue, and feels like it would be inefficient as this gets called a lot of times.
Edit: Just to clarify, the script may call the object multiple times within the same run, but use different versions.
Upvotes: 2
Views: 266
Reputation: 132758
Remember that @ISA
is a package (global) variable. You can't change it on a per-instance basis. Also remember that inheritance is designed for an object that is a more restricted notion of a more general idea, rather than a way to dispatch. Working against that is sure to bring tears.
Even if you could localize it, you could only ever have one object working at a time because the localization of a package variable is actually dynamic. The change exists everywhere in the program until the dynamic scope cleans up. You don't want those sorts of synchronization issues.
I think there are two cleaner ways to deal with this.
Give each module the same (or close) interface, and return their object directly. Your new
then is a "factory" that returns an object of a class appropriate to the arguments. If you can only have one of these active in the program at any time, then you should consider some way to make a singleton (or just make everything a class method).
Have an "adapter" class contain an object of the right versioned module and translate method calls to the versioned object that's part of the instance. When you run into a method that isn't in the versioned object, you have a way to complain about. This is useful for communicating API and interface changes in warning messages.
Upvotes: 3
Reputation: 9231
The main issue with changing @ISA
dynamically like this is that you are applying that change to the entire class, not just the one instance that was requested with the use_version_2
parameter. This may not matter if it's only expected that the object will only ever be used once in a program (i.e. it's a singleton) but it's not a good practice regardless. The other minor issue is that whenever @ISA
is changed, the method cache is reset (since method calls are likely to subsequently resolve to different subroutines).
Two alternative options I would consider would be to make a different subclass for each "version", or to leverage roles with Role::Tiny and With::Roles, which lets you dynamically apply a set of behavior to a specific object by essentially creating a subclass on the fly.
For example you could make "Modulev1" and "Modulev2" into two roles, and apply the one requested, this would also handle loading the module.
# first role module
package Modulev1;
use Role::Tiny;
# subroutines to be composed in
1;
# second role module
package Modulev2;
use Role::Tiny;
# subroutines to be composed in
1;
# user of roles
use With::Roles;
...
if ($args{use_version_2}) {
$self->with::roles('Modulev2');
} else {
$self->with::roles('Modulev1');
}
Or you could create two separate classes, and put the common code of the two versions into a role they would use statically using Role::Tiny::With (this is similar to how you would use an "interface" in some other languages, except that it can also supply implementations).
package ModuleCommon;
use Role::Tiny;
requires 'foo', 'bar'; # require these functions to be implemented by versioned class
sub baz { }
1;
package Modulev1;
use Role::Tiny::With;
sub foo { }
sub bar { }
with 'ModuleCommon';
1;
Then you would make the determination of which class to use externally (like with a wrapper function), rather than inside a constructor of either one.
Upvotes: 3