NAME
Module::List::Pluggable - list or require sub-sets of modules
SYNOPSIS
use Module::List::Pluggable qw( list_modules_under import_modules );
# get a list of all modules installed under a given point
# in perl's module namespace
my @plugins = list_modules_under( "My::Project::Plugins" );
# require & import all modules in the tree
import_modules( "My::Project::Plugins::ViaExporter" );
# skip some of them
import_modules( "My::Project::Plugins::ViaExporter",
{ exceptions =>
'My::Project::Plugins::ViaExporter::ButNotThese' }
);
# just require them, don't do an "import"
import_modules( "My::Project::Plugins::ViaExporter",
{ import => 0 }
);
DESCRIPTION
This module provides some procedural routines to
(1) list a sub-set of modules installed in a particular place in perl's module namespace,
(2) require those modules and import their exported features into the current package.
Both of these functions are useful for implementing "plug-in" extension mechanisms.
Note: this module is named Module::List::Pluggable because it uses Module::List to do some things similar to Module::Pluggable.
EXPORT
None by default. The following are exported on request (":all" tag is available that brings in all of them):
- list_modules_under
-
Uses the "list_modules" feature of Module::List to get a list of all modules under the given root location in perl's module namespace.
Example: my @plugins = list_modules_under( "My::Project::Plugins" );
Note that if no location is supplied, this will list all installed modules: this can take some time.
- import_modules
-
Does a "require" and then an "import" of all of the modules in perl's module namespace at or under the given "root" location.
With Exporter based plugins, everything listed in @EXPORT becomes available in the calling namespace.
Inputs:
Returns: the number of successfully loaded modules.
- require_modules
-
Like "import_modules", this does a "require" (but no "import") on all of the modules in perl's module namespace at or under the given "root" location.
Inputs:
Returns: the number of successfully loaded plug-in modules.
reporting routines
- list_exports
-
Returns a list (aref) of all items that are exported from the modules under the object's plugin root.
- report_export_locations
-
Reports on all routines that are exported by the modules under the object's plug-in root, including the module where each routine is found.
Inputs:
- the location to begin scanning in module name space, e.g. "Mah::Modules::Plugins"
- an options hash reference, with options:
-
- exceptions
-
array reference of plug-in modules to be ignored.
Return:
A hash reference, keyed by the names of the exported routines with values that are array referencess listing all modules where that routine was found.
routines primarily for internal use
- check_plugin_exports
-
Looks for conflicts in the tree of plug-ins under the given plug-in root. Errors out if it finds multiple definitions of exported items of the same names.
The form of the error message is:
Multiple definitions of ___ from plugins: ___
Inputs:
- the location to begin scanning in module name space, e.g. "Mah::Modules::Plugins"
- an options hash reference, with options:
-
- exceptions
-
array reference of plug-in modules to be ignored.
Note: this routine also checks that each plug-in module is free of syntax errors.
- run_code_or_warn
-
Runs code passed in as a string (not a coderef), so that "barewords" can be created from variables.
Returns the value of the code expression.
Generates an error message string using an optional passed-in prefix, but with the the value from $@ appended.
As with carp, the error is reported as occuring in the calling context, but also includes the full error message with it's own location indicated. The error message is reported to STDERR, but excecution contines.
Inputs:
Example:
my $prefix = "problem with $module_name"; my $code = "require $module_name"; run_code_or_warn( $code, $prefix );
- run_code_or_die
-
Variant of run_code_or_warn that dies
Note: reports error in the calling context, much like "croak", but also includes the full error message with it's own location indicated.
DISCUSSION
A "plug-in" architecture is a way of allowing for the behavior of a system to be extended at a later date by the addition of new modules without any changes to the existing code.
Plug-in Extension Techniques (polymorphism vs. promiscuity)
There are essentially two styles of plug-ins:
- polymorphic plug-ins
-
With "polymorphic plug-ins" a particular module appropriate to a task is selected from the available set. In the "polymorphic" case, the same set of methods (often called the "interface") are defined in different ways, depending on the plug-in used.
- promiscuous plug-ins
-
With "promiscuous plug-ins", the entire set of plug-in modules is used at once. In the "promiscuous" case, each plug-in defines new methods.
When implementing "polymorhpic plug-ins", it's often convenient to get a list of available modules, and then choose one of them somehow (often by applying a naming convention). The "list_modules_under" routine here is helpful for this, though admittedly, it's frequently almost as easy to just require the expected module, and trap the error if the module doesn't exist.
For "promiscuous plug-ins", there are essentially two sub-types, object-oriented and proceedural. In the object-oriented case, a list of modules can be pushed directly into the @ISA array so that any methods implemented in the extension modules become available via the justly-feared but occasionally useful "multiple-inheritence" mechanism. In the proceedural case, you can use the "import_modules" routine provided here, which does something like a use-at-runtime on all of the plug-ins (it does a "require" of each module, and then an "import").
Obviously, in the object-oriented form, the routines in the extensions must be written as methods (e.g. each should begin with "my $self=shift;"). In the proceedural case, each module should use "Exporter", and to work with the "import_modules" routine supplied here, all features to-be-exported should be in the @EXPORT array of each plug-in module.
But note that these two approaches can be combined into a hybrid form: Exporter can be used to bring a collection of OOP methods into the current object's namespace.
These Exporter-based "promiscuous plug-ins" (whether OOP or proceedural) have an advantage over the multiple-inheritence approach: the damaged is limited that can be done by the addition of a new, perhaps carelessly written plug-in module.
The "import_modules" routine (by default) watches for name collisions in the routines imported from the plug-ins, and throws an error when they occur. In comparison the simple MI solution will silently use which ever plug-in method it sees first in the path; so if the sort order of your list of plug-in modules doesn't work as a precedence list, then you may be in trouble.
Using the hybrid approach (OOP methods brought into a common namespace by using "import_modules"), you need to watch out for the fact that these plug-in methods will inherit based on the @ISA of the class they're imported into: a "use base" in the package where the methods are defined will have no effect. If your plug-ins all need to use common code inherited from a particular module, then the parent needs to be in the inheritence chain of the class the plug-ins are imported into, not in the package in which they were originally written.
A restriction that all "promiscuous" OOP plug-in schemes share (to my knowledge) is that subclassing essentially doesn't work with them. Simply adding a subclass of a plug-in to the set is not enough to reliably override the original: the precedence between the two will be silently choosen based on some arbitrary criteria (typically accidents of sort order in the module names).
Even if a way could be found to solve that problem (e.g. an import mechanism that skips parents when a child exists) it wouldn't seem adviseable to use it: simply adding a new plug-in would have the potential to break existing code.
However, if you *really* feel the need to do something like this, the "exceptions" feature of "import_modules" could be used to manually suppress a parent plug-in, so that only the child plug-in will be imported. Similarly, if you realize that someone else's module is creating problems for you, the "exceptions" feature provides an alternate way to suppress it's use without uninstalling it.
MOTIVATION
The List::Filter project is an example of a use of "promiscuous plug-ins" to provide an extreme (perhaps "pathological") degree of extensibility.
list_modules_under
The wrapper routine "list_modules_under" seemed adviseable because of the very clunky interface of the "list_modules" routine provided by Module::List (see below). But then, at least Module::List actually works correctly, unlike Module::Find (which double-reports modules if found in two places in @INC). And Module::Pluggable is peculiarly limited in that it essentially assumes you'll have a hardcoded search location in module namespace.
Module::List peculiarities
The list_modules routine exported by Module::List has two "options" that you will almost always want enabled:
my $modules = list_modules("$root",
{ list_modules =>1, recurse => 1});
You need to tell list_modules that you really want it to list the modules (reminiscent of the "-print" option on the original unix "find" command). But recursion is off by default, the opposite of the "find" convention...
And the return value from "list_modules" is a hash reference (not a 'list' or an aref). What you actually want is the keys of this hash (the values are just undefs):
my @found = keys %{ $modules };
Another minor irritation is that the first argument (a place in module name space) is required to have a trailing "::" appended to it. However, it does understand that an empty string should be interpreted as the entire list of installed modules (note: it takes a long time to get this full list, as you might expect).
LIMITATIONS
When using "import_modules" (which brings in methods via Exporter):
o methods can inherit from the @ISA of their new context, but
not the package they came from.
o subclassing an existing plug-in to create a new one should
almost always be avoided: the precedence of child over
parent can't be easily guaranteed, and adding a new plug-in
can break existing code.
SEE ALSO
Module::List Module::Pluggable Module::Find
TODO
Add a "recurse" option to both routines: default to recurse, but allow them to work on a single directory level.
AUTHOR
Joseph Brenner, <doom@>
COPYRIGHT AND LICENSE
Copyright (C) 2007 by Joseph Brenner
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself, either Perl version 5.8.7 or, at your option, any later version of Perl 5 you may have available.