Dependency Injection

15 views
Skip to first unread message

Piers Cawley

unread,
Jul 6, 2005, 6:47:47 PM7/6/05
to perl6-l...@perl.org
So, I got to thinking about stuff. One of the more annoying things about
writing nicely decoupled objects and applications are those occasions where you
want an object to be able to create objects in another class. Say you've
provided a singleton interface to your logging system. The naive implementation
goes something like:

require Logger;

has $.logger;

method logger { $.logger //= Logger.new }

method whatever {
./logger.debug("About to do stuff");
.do_some_stuff;
./logger.debug("Did stuff");
...
}

But that's problematic because we've backed knowledge of the particular class
that handles logging into our class. The bad solution to this is to subclass
our class if we want a different Logger class (that conforms to the same
interface). A slightly better solution is to parametrize the logger class
name, probably with a class variable -- but doing that means you have to
remember to set the variable for every class you use. Or you could use a
global, but globals are bad...

It'd be really cool if you could essentially write the naive implementation
above and have your application/test/webserver harness decide which particular
class will be taken to be the concrete implementation. Something like this:

role Logger is Injected {
method debug {...}
method info {...}
...
}

Logger::DBI does Logger { ... }

Logger::Debugger does Logger { ... }

Then the harness that actually sets up the application would simply do

use Logger::DBI :dsn<...>, :user<...>, :password<....>

and Logger::DBI would install itself as the default Logger class.

The question is, how does one write Injected to make this work? Or what
features do we need to be able to write Injected? Thinking about this, it
shouldn't be too hard to implement 'Injected' in Perl 5:

sub Injected::AUTOLOAD {
no strict 'refs';
die "If something's already been instantiated you're screwed"
if ref($_[0]) or $Injected::injecting;
local $Injected::injecting = 1;
my $target_role = $_[0]
my @possibles = grep {/(.*)::$/ && $1->isa($target_role)} keys %::;
die "Too many possibles" if @possibles > 1;
if (@possibles) {
*{"$target_role\::} = *{$possibles[0]};
}
else {
my $default_package = $target_role->default_class;
eval "require $default_package" or die "Can't find a default package";
*{"$target_role\::"} = *{"$default_package\::"};
}
&{$target_role->can($AUTOLOAD)}(@_);
}

NB, that's completely untested code, but it, or something like it, should work.




Sam Vilain

unread,
Jul 6, 2005, 7:24:11 PM7/6/05
to Piers Cawley, perl6-l...@perl.org
Piers Cawley wrote:
> Then the harness that actually sets up the application would simply do
> use Logger::DBI :dsn<...>, :user<...>, :password<....>
> and Logger::DBI would install itself as the default Logger class.
> The question is, how does one write Injected to make this work? Or what
> features do we need to be able to write Injected? Thinking about this, it
> shouldn't be too hard to implement 'Injected' in Perl 5:

FWIW this is exactly what I would like to achieve with the Date objects;
allowing code that doesn't care about which Date representation they want
the ability to just say "any Date representation".

Sam.

Larry Wall

unread,
Jul 6, 2005, 10:04:56 PM7/6/05
to perl6-l...@perl.org
On Wed, Jul 06, 2005 at 11:47:47PM +0100, Piers Cawley wrote:
: Or you could use a global, but globals are bad...

Globals are bad only if you use them to hold non-global values.
In this case it seems as though you're just going through contortions
to hide the fact that you're trying to do something naturally
global. You might argue that a singleton object lets you hide the
implementation of the delegation from the world, but there's really not
much to implement if you just want to tell the world that whenever the
world says "LOGGER" the world is really delegating to "LOGGER::DBI".
Plus if you really need to reimplement you just vector the world
through "LOGGER::SELECTOR" or some such.

One nice thing about a variable is that you're guaranteed to
be able to temporize it:

temp $*LOGGER = pick_a_logger();

On the other hand, there's no reason a role can't play with a
global variable, and arbitrate the use of the global among all the
classes that want to delegate through the global. But a role is not
allowed to function as its own singleton object, because roles don't
define objects. On the gripping hand, the package holding the role's
namespace is a perfectly fine spot for such singular information.
Every named package/module/class/role/subtype is a kind of global
singleton hash of the symbol table persuasion.

But if globals are inherently bad, we should remove them from Perl 6.

Hmm, this raises the question of whether a role-private "class" variable
actually belongs in the class's or in the role's namespace:

role Baz {
our $:x;
}
FooBar does Baz;
BarFoo does Baz;

Does $:x end up being a "private" global in the Baz package or a global
shared between the FooBar and BarFoo packages? That kinda goes along
with the question of whether attributes in general belong more to the
role or the class into which the role is composed. It seems like all
those cases can be useful, so maybe we need a syntax to distinguish
role attributes from class attributes. To think of it another way,
which delarations are to be taken literally, and which generically?
And which way should be the default?

For this particular use, you want some semantics resembling:

role LOGGER {
role:our $:logger handles Any;
role:submethod BUILD ($logger) {
fail "Logger already set" if defined $:logger;
$:logger = $logger;
}
...

Not necessarily recommending that syntax, of course. Could just default
declarators to class semantics and force role declaratoins to be explicit:

role LOGGER {
our $:LOGGER::logger handles Any;
submethod LOGGER::BUILD ($logger) {
fail "Logger already set" if defined $:logger;
$:logger = $logger;
}
...

But that seems a bit klunky, especially given the way package names
and twigils interact. And don't even think about using $?ROLE there.
Plus we've never let you use "our" on a qualified name before.

This really seems to be somewhat orthogonal to everything else, so
maybe we need some orthogonal syntax:

role LOGGER {
mumble our $:logger handles Any;
mumble submethod BUILD ($logger) {
fail "Logger already set" if defined $:logger;
$:logger = $logger;
}
...

for some value of "mumble" that implies "non-generic". Need to think
about this some more. I *don't* think it's viable to make non-generic
the default unless we do it only for variables. But then you'd be forced
to write

our &BUILD ::= submethod ($logger) {...}

to get a non-generic BUILD submethod, and that's more than a bit ugly.

So I'm still mumbling...

Larry

Piers Cawley

unread,
Jul 8, 2005, 8:48:01 AM7/8/05
to perl6-l...@perl.org
Larry Wall <la...@wall.org> writes:

> On Wed, Jul 06, 2005 at 11:47:47PM +0100, Piers Cawley wrote:
> : Or you could use a global, but globals are bad...
>
> Globals are bad only if you use them to hold non-global values.
> In this case it seems as though you're just going through contortions
> to hide the fact that you're trying to do something naturally
> global. You might argue that a singleton object lets you hide the
> implementation of the delegation from the world, but there's really not
> much to implement if you just want to tell the world that whenever the
> world says "LOGGER" the world is really delegating to "LOGGER::DBI".
> Plus if you really need to reimplement you just vector the world
> through "LOGGER::SELECTOR" or some such.

The thing about the version where the language handles the vectoring through
Logger, or whatever, is that you can retrofit such indirection without having
to make sure all your code uses the global. For instance, in the Test::* world,
there's a huge number of test modules that all play nicely because they work
with Test::Harness, and that's fine until you reach the point where you want to
specialize Test::Harness itself (say you want to write a graphical test
runner). With the global variable approach, you have to alter everything that
uses Test::Harness, with an injection approach, you only have to make changes
to Test::Harness itself.

Hmm... thinking about this a little further, the interface I proposed can't be
as automagical as simply loading a class that injects into your injectable
class because said class could well inherit from another injecting class. You
would have to make it a two step process like:

use InjectableClass;

use InjectingClass;

InjectableClass.inject_with(InjectingClass);

What I'm after is another way to hide implementation details from client
classes and allow programmers to write natural code.

> One nice thing about a variable is that you're guaranteed to
> be able to temporize it:
>
> temp $*LOGGER = pick_a_logger();

I'm not sure that, in the particular case, that's all that useful. It'd be cool
if, say in a debugging situation, I could say that, from now on, all messages
to Logger from class Foo are actually dispatched to InstrumentedLogger, and
then to turn it off again, but I'm not sure how I'd do that with a global.

$Package::Name::OUR::*LOGGER = ::InstrumentedLogger

perhaps?

Reply all
Reply to author
Forward
0 new messages