(The Moose/Moo answer is, of course, "Roles". This question is about the general case where you want to combine two modules that are both subclasses of the same parent, assuming no Moose/Moo.)
Let's take a slightly contrived example: the modules LWP::UserAgent::Determined and LWP::RobotUA are both subclasses of LWP::UserAgent and extend it in different ways. What should I do if I want to create an object that combines the methods from both? It will, at its core, still be a LWP::UserAgent object, and the other two modules don't clash with each other, so it should be easy, right?
As far as I can tell, the correct thing to do is to create a new package which declares both of the other two as parents – use parent qw(LWP::RobotUA LWP::UserAgent::Determined) – and then create objects from that. And, indeed, if you do that, you get an object that contains the methods from both, as well as from the base class LWP::UserAgent, and almost everything works as you'd expect.
But not quite. Both LWP::UserAgent::Determined and LWP::RobotUA have default values for certain attributes that are set when the object is created if no other value is given. When combining the two, LWP::RobotUA's defaults get set, but not LWP::UserAgent::Determined's. So something must be wrong.
Here's some test code:
#!/usr/bin/env perl
use strict;
use warnings;
use 5.016;
use LWP::RobotUA;
use LWP::UserAgent::Determined;
package MyUA;
use parent qw(LWP::RobotUA LWP::UserAgent::Determined);
for my $module (qw(LWP::RobotUA LWP::UserAgent::Determined MyUA)) {
say '# ', $module, ' #';
my $ua = $module->new(
'agent' => 'Test-UA',
'from' => 'example@example.com',
);
my $req = HTTP::Request->new(GET => 'https://www.bbc.co.uk/emp/network_status.txt');
my $response = $ua->request($req);
unless ($module eq 'LWP::UserAgent::Determined') {
say 'Use sleep? : ', $ua->use_sleep() // 'not defined!';
say 'Allowed OK? : ', $ua->rules->allowed('https://www.bbc.co.uk/') // 'not defined!';
say 'Sites with rules: ', (defined $ua->rules()->{loc}) ? join(', ', (sort keys %{$ua->rules()->{loc}})) : 'not defined!';
}
unless ($module eq 'LWP::RobotUA') {
print 'Timings: ';
if (defined $ua->timing()) {
say $ua->timing();
}
else {
print 'Timing defaults not set! ';
$ua->timing('1,5,10,20,60,240');
say '...but the method works: ', $ua->timing();
}
say 'Retry codes: ', (defined $ua->codes_to_determinate()) ? join(', ', (sort keys %{$ua->codes_to_determinate()})) : 'not defined!';
}
say '#'x60;
}
This outputs:
# LWP::RobotUA #
Use sleep? : 1
Allowed OK? : 1
Sites with rules: www.bbc.co.uk:443
############################################################
# LWP::UserAgent::Determined #
Timings: 1,3,15
Retry codes: 408, 500, 502, 503, 504
############################################################
# MyUA #
Use sleep? : 1
Allowed OK? : 1
Sites with rules: www.bbc.co.uk:443
Timings: Timing defaults not set! ...but the method works: 1,5,10,20,60,240
Retry codes: not defined!
############################################################
Here you can see that the methods for both modules work, but default values are not set for LWP::UserAgent::Determined's timing() or codes_to_determinate() methods when combined with LWP::RobotUA, while LWP::RobotUA's use_sleep() method is created with its default value of 1. Setting values manually works fine, however, and otherwise the combined object works as expected.
So, in summary: what's the correct way of handling this case, where you want to combine two modules that subclass a common third? Is this, in fact, correct, but I just chose an unfortunate example and LWP::UserAgent::Determined isn't well-behaved in how it sets it's defaults?