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.

        
        


       

Reply via email to