Reputation: 1139
I have a series of modules that perform output functions for my scripts. Sometimes the module is called directly -- it is called View
-- and sometimes a child class that extends it is used instead (View::ChildName
). View declares our $THEMENAME = 'default';
when it is loaded, but the child declares its own specific $THEMENAME
when it loads.
Problem: When new()
is called on a child theme, it calls my $self = $class->next::method(%params);
(using mro
) to get some core things set by the parent class before extending it. One of the core bits is that the parent class sets $self->{'themeName'}
. However, if it simply calls $THEMENAME
, it gets the parent's setting: "default."
The only way I've reliably and successfully solved this is to turn off strict
temporarily and do this:
my $packName = ref $self;
{
no strict;
$self->{'themeName'} = ${${packName} . "::THEMENAME"};
}
This works, but in profiling the code, if objects are created frequently, this actually adds more overhead than I expected. I tried the alternative of always using the parent's package name, e.g. the child sets $View::THEMENAME
. This works, but only if the theme name is set within new
and not on the load of the module; if it is on load, there is erratic behavior if several different child objects (of different children) are created over the course of the script.
These options both seem less than ideal. Is there a good way to do this? The only thing I found was this old question and I think incorporating Moo
would probably add more overhead than I'm trying to avoid by getting rid of my current no strict
block. Has anything been added to more modern versions of Perl that might solve my issue?
The alternative is to dodge the issue all together and simply set $self->{'themeName'}
within each child object's new
method, although I'm trying to avoid that change since there's a fair number of legacy child classes that expect $THEMENAME
to exist.
Minimal reproducible example of View.pm:
use strict;
package View;
our $THEMENAME = 'default';
sub new {
my $class = shift;
my $params = shift;
my $self = { 'setting' => $params{'setting'} };
bless $self, $class;
$self->{'themeName'} = $THEMENAME;
return $self;
}
And of View/Child.pm
:
use strict;
use mro;
package View::Child;
use parent 'View';
our $THEMENAME = 'child';
sub new {
my $class = shift;
my $params = shift;
my $self = $class->next::method($params);
bless $self, $class;
say STDOUT $self->{'themeName'};
# Prints 'default' not 'child'.
return $self;
}
Now a script to call it:
use View::Child;
my $object = View::Child->new();
If you added the first code block to View.pm
, it gives the expected result instead, but seems to add about 9 ms to each call to new
-- more than the time it takes for it to handle everything else I have in the much longer full length new
method -- which adds up if the program runs many iterations:
use strict;
package View;
our $THEMENAME = 'default';
sub new {
my $class = shift;
my $params = shift;
my $self = { };
bless $self, $class;
my $packName = ref $self;
{
no strict;
$self->{'themeName'} = ${${packName} . "::THEMENAME"};
}
return $self;
}
Upvotes: 3
Views: 206
Reputation: 1139
My final solution follows what @plentyofcoffee and @ikegami outlined, but I wanted to see if I could come up with a way to set it automatically without each child module implementing it (keeping in mind legacy code). Assuming the child does want to set it, it passes $param{'themeName'} to the parent's constructor, which sets it to $self->{'themeName'}
. If themeName
is undefined, I came up with this regex in the parent class that extracts the name of the child as a fallback themeName
:
unless ($self->{'themeName'}) {
state $getThemeNameRegEx = qr#^SAFARI::(.*::)+(.*?)$#;
$class =~ /$getThemeNameRegEx/;
$self->{'themeName'} = $2 // "default";
}
This sets to default
if the name doesn't contain at least two levels below SAFARI
, e.g. SAFARI::View
is default
(the parent module is in use without a child) and SAFARI::View::mysite
is mysite
.
Upvotes: 1
Reputation: 386481
The concept of class properties is one you should forget (in Perl). It's fine for the module to have constants and possibly variables, but they shouldn't be considered part of the class.
I see four approaches you could take:
# View.pm
sub new {
my ($class, $params) = @_;
my $self = bless({}, $class);
$self->{ setting } = $params->{ setting };
$self->{ themeName } = 'default';
return $self;
}
sub themeName { $_[0]->{themeName} } # Optional
# Child/View.pm
sub new {
my ($class, $params) = @_;
my $self = $class->next::method($params);
$self->{ themeName } = 'child';
return $self;
}
# View.pm
sub new {
my ($class, $params) = @_;
my $self = bless({}, $class);
$self->{ setting } = $params->{ setting };
$self->{ themeName } = $params->{ themeName } // 'default';
return $self;
}
sub themeName { $_[0]->{themeName} } # Optional
# Child/View.pm
sub new {
my ($class, $params) = @_;
my $self = $class->next::method({ themeName => 'child', %$params });
return $self;
}
# View.pm
sub new {
my ($class, $params) = @_;
my $self = bless({}, $class);
$self->{ setting } = $params->{ setting };
return $self;
}
sub themeName { 'default' }
# Child/View.pm
# No need to override `new`.
sub themeName { 'child' }
# View.pm
sub new {
my ($class, $params) = @_;
my $self = bless({}, $class);
$self->{ setting } = $params->{ setting };
$self->{ themeName } = $class->defaultThemeName;
return $self;
}
sub defaultThemeName { 'default' }
sub themeName { $_[0]->{themeName} } # Optional
# Child/View.pm
# No need to override `new`.
sub defaultThemeName { 'child' }
Upvotes: 3
Reputation: 488
One potential solution would be to add THEMENAME as an overridable method.
View.pm:
use strict;
package View;
our $THEMENAME = 'default';
sub THEMENAME {return 'default'}
sub new {
my $class = shift;
my $params = shift;
my $self = { 'setting' => $params->{'setting'} };
bless $self, $class;
$self->{'themeName'} = $self->THEMENAME;
return $self;
}
View/Child.pm:
use strict;
use mro;
package View::Child;
use parent 'View';
our $THEMENAME = 'child';
sub THEMENAME {return 'child'}
sub new {
my $class = shift;
my $params = shift;
my $self = $class->next::method($params);
bless $self, $class;
say STDOUT $self->{'themeName'};
# Prints 'default' not 'child'.
#
return $self;
}
# perl -Mlib=. -MView::Child -e 'View::Child->new()'
child
Upvotes: 2