Specification: Administrative Component Selection
There exists a pattern in Twisted plugin-using applications where a user interface is presented (typically via a command line tool, but this is not the only possible case) for the selection of a specific plugin from a list of possibilities. Selection is often done by name -- that is, a short, unique string associated with each provider of the desired interface.
The drawbacks of this approach are manifold:
- Implementation of this selection is often duplicated by each user for each plugin interface.
- Each plugin interface is required to supply the name attribute.
- In order for users to make a meaningful selection, some further descriptive information must generally be provided (often in the form of a description attribute which references a string containing prose explaining the features of a particular provider).
- Users must repeat their selection each time they invoke a tool which uses plugins.
- Only objects found by the plugin system can be used, precluding more static configurations where it is desirable for a specific or otherwise well-known object to be used in place of any existing plugin.
The solution will need to provide a couple parts: an application interface for dealing with configuration and providing configuration mechanisms to the end-user.
There is a decision to be made about what kind of interface to expose to solve this problem. The general form of these solutions is to remove the responsibility of finding objects providing particular functionality and providing a user interface for choosing which of those objects to use.
This solution provides a simplified mechanism for application code to get particular kinds of objects which merely requires an Interface to be requested.
def getComponent(interface): """ Return the appropriate provider of the given interface. "appropriate" may be inferred based on available plugins, it may have been previously specified via some user configuration mechanism, it may be selected based on site- wide policy, or some other mechanism may be used. """
Someone might come up with a better name than getComponent, though.
For example, twistd might use this to set up logging like this:
def setUpLogging(self): logger = getComponent(ILogger) logger.start()
Some good things about this:
- As far as twistd is concerned, this is pretty simple (a ton simpler than the current code and a ton simpler than the code in the #638 branch).
- It's flexible: twistd doesn't need to change in order for new loggers to be used (that's the whole point, duh)
Some less good things:
- To a reader, this might not be entirely obvious. One needs to understand getComponent and find the system configuration in order to understand what's going on.
- Whoever wrote this had to make a conscious decision to use this system.
- The failure condition, where ILogger has not been specified, results in an entirely inscrutable traceback. Why does this code expect an ILogger to be available? What happens when one isn't? We've had this problem with Nevow's context object for years now. Personally, I think that this problem alone should be considered a deal-breaker for this interface. The "inverted" interface, below, leaves all the problems of location and configuration error reporting to the user, and they all occur before any application code is invoked, which is vastly superior. --GlyphLefkowitz
Framework-level ("inverted") interface
This solution inverts the APIs, so that instead of application-level code searching for objects and providing an interface to allow the user to select one, library code implements the selection process and passes one (or more, if multi-selection is allowed) in to the application code.
A completely inverted interface would be invisible. The application-code written to use it would look like this, though:
def setUpLogging(self, logger): logger.start()
There are several things which need to be addressed for this to work:
- someone has to figure out that logger is supposed to be an ILogger provider
- whoever is calling setUpLogging has to invoke the necessary machinery to find an ILogger and supply it
There are probably numerous ways to address each of these issues. Whatever solution is found for the second point probably resembles getComponent above quite closely. Some specifics for the first point should be explored. (TODO: explore some specifics)
The simplest implementation could call getPlugins and give back the first plugin -- not even real "configuration". A more useful implementation might look at environment variables or files or registry keys (or some other platform-appropriate configuration mechanism) to find existing configuration. A useful property of this interface is that each of these mechanisms can be implemented and deployed separately from the development of applications using the interface. This lets applications begin to benefit immediately and to benefit without change from improvements as they are made.
The kind of configuration which is possible has at least two aspects:
- Selecting a particular provider of an interface.
- Providing arguments to an implementor of an interface in order to get a provider.
The plugin system may be involved (particularly as a way of suggesting available providers through some user interface), but possible choices may not be limited to plugins. A simple mechanism might just store the FQPN of a provider. A more advanced mechanism might store the FQPN of an implementor, along with some arguments to pass to it. The UI for selecting these things might initially be a text editor (to edit a config file) or a shell (to set environment variables). Later it might be a more advanced tool which queries for plugins.
There is a strange issue that crops up given a combination of per-process configuration issues (such as command line arguments) and starting Twisted-using child processes. Given regular, persistent configuration, there is no problem: Twisted-using child processes can fetch configuration data via the same mechanisms that the parent process does. However, when per-process configuration is introduced, it may be expected that a getComponent (or whatever) call in the child process will honor settings affecting the parent process. There are a couple of general options here: either the getComponent calls in the child process can automatically honor those in the parent process (somehow), or the parent process could somehow send that configuration data explicitly to the child process. The main problem is that *some* mechanism for passing configuration state around must be provided.