Wednesday, August 24, 2011

CPAN, decoupling and Dependency Injection

Consider the code:

sub fetch
{
my ($self, $uri) = @_;
my $ua = LWP::UserAgent->new;
my $resp = $ua->get( $uri );

...
}


Yes - this is taken from a post by chromatic.

Now imagine that this is code from a CPAN module you installed and that some security concerns require you to replace LWP::UserAgent with LWPx::ParanoidAgent there. Bad luck - you'll probably need to subclass it, override that whole fetch method and pray that it will not change too much with every new release of the original module.

This is really why I am drumming this Dependency Injection drum over and over again - code that uses it is more reusable, more universal:

use Moose;
has 'ua', is => 'ro', default => sub { LWP::UserAgent->new };

sub fetch
{
my ($self, $uri) = @_;
my $ua = $self->ua;
my $resp = $ua->get( $uri );

...
}


Now you would not have any problem with providing a LWPx::ParanoidAgent object for the fetch method to use.

By the way, with classical DI you'd move that LWP::UserAgent->new completely out from the class, here it stays as a 'default' that can be overridden from outside if you need. The problem with classical DI is that you need to have a place where to move that initialization code - here it is sidestepped for the 'normal' usage and you need to worry about it only in the cases where you really need to. Java probably does not have this 'default' mechanism.

9 comments:

pwes said...

Java probably does not have this 'default' mechanism.

Just as Perl has Moose, Java has its own libraries for providing attributes syntax, e.g. through annotations.

I agree that simplicity of your example is a great vritue, but there is also a big drawback here: you don't specify what you expect from 'ua' object. Does it have to be of LWP::UserAgent class? Or maybe only 'get($uri)' method must be provided by alternative implementation? For that I prefer to add one more interface (a role?) used as a 'ua', wrapping around implementation (and unused e.g. LWP::UserAgent interface) details not important for usage scenario. More code, but with relaxed interfaces.

Ido Perlmuter said...

This is actually something I'm doing quite often, though I've never even realized it provides such an advantage. Thanks.

LeoNerd said...

I tend to go one stage further:

use constant UA_CLASS => "LWP::UserAgent";
sub new_ua { shift->UA_CLASS->new( @_ ) }

...

sub thingy
{
...
$self->new_ua( args => here );
}

That way, a subclass that just wants to use a subclass of LWP::UserAgent with the same constructor args needs only to provide a new UA_CLASS method.

nikos said...

Can Roles be used instead,for injecting the dependency to the class, like Ruby uses mixins with open classes?

zby said...

@nikos - this is about the need to create the dependencies outside of the class code. I don't see how roles would help with that.

nikos said...

Please check :
Ruby and dependency injection in a dynamic world
Can't the same be achieved with roles in Perl?

zby said...

I don't like reopening classes in client code and changing their definitions there that they do there. I think it is confusing - and as they admit it is also global. In Perl you can do that of course - not with roles because roles are consumed in the class definition not in the client code but maybe with dynamic traits or simply by monkey patching.

nikos said...

ok thanks,that makes it clear

Hernan Lopes said...

--------------------------------- README:

Here is my example on how to use different engines.
Save each file and execute: # Modify the motor, use Ferrari or Fusca.. it could be LWP or mechanize or whatever.
Fusca is the old beatle car.

$ startcars.pl

-------------------------------- startcars.pl:

use Carro;

my $c = Carro->new( motor => 'Ferrari' );
$c->acelerar;
warn 'Marca do carro: '. $c->marca;


----------------------------- Carro.pm
package Carro;
use Moose;
use Ferrari;
use Fusca;

has engine => (
is => 'rw',
isa => 'Any',
builder => '_build_engine',
);


has motor => (
is => 'rw',
isa => 'Str',
default => 'Fusca',
);

sub _build_engine {
my ( $self ) = @_;
$self->engine( $self->motor->new );
}

sub acelerar {
my ( $self ) = @_;
$self->engine->acelerar();
}
sub marca {
my ( $self ) = @_;
$self->engine->marca();
}

1;

----------------------------- Ferrari.pm

package Ferrari;
use Moose;

has top_speed => (
is => 'ro',
isa => 'Int',
default => 300,
);


sub acelerar {
my ( $self ) = @_;
for ( my $i = 0; $i <= $self->top_speed; $i = $i + 25 ) {
print $i,"\n";
}
}

has marca => (
is => 'ro',
isa => 'Str',
default => 'Ferrari v1.0',
);

1;


-------------------------------- Fusca.pm

package Fusca;
use Moose;

has top_speed => (
is => 'ro',
isa => 'Int',
default => 100,
);


sub acelerar {
my ( $self ) = @_;
for ( my $i = 0; $i <= $self->top_speed; $i = $i + 25 ) {
print $i,"\n";
}
}

has marca => (
is => 'ro',
isa => 'Str',
default => 'Fusca v1.0',
);

1;


### Hernan Lopes