One of the most cogent criticisms of the book Design Patterns is that too many people read it as "here's a list of characteristics your software should exhibit" instead of "here's a catalog of common design elements many projects exhibit". Rather than spreading a common vocabulary, the patterns book became yet another set of buzzwords to use to spice up developer CVs.
The backlash should have been predictable; it's almost a law of physics by this point. For every grand unifying pronouncement about a shiny new way to improve software development, cue a grand group of people ready to disclaim it as unnecessary, overcomplicated, a warmed-over rediscovery of something at least thirty years old, or a silly way to sell books and consulting. In this case, the naysayers had a point. Suddenly, global variables weren't bad. They were instances of the Singleton pattern. Oh frabjous day.
(Fortunately in these enlightened times, we use inversion of control and dependency injection to make our singletons, and we control them with great swaths of barely-typed XML or JSON. We're so modern it hurts.)
... not that singletons are always bad.
Some concerns really are global. Your logging framework is probably a global concern. Your configuration file is probably a global concern. Here's a secret, though: these things probably oughtn't be mutable. The real concern is mutable global data. (A secondary concern is too much coupling to concrete instances, but that's a different article.)
Sometimes you can go a little too far the other direction, though.
I have a test suite that's too slow for my taste—about 2500 assertions which run in 70 seconds. I'd love to get that down to 20 seconds, but under a minute is definitely an improvement.
The project has a configuration file in config/settings.yaml and a
corresponding Project::Config
module which loads the settings and
offers an API to its contents. Other modules within the system access the
configuration file by loading the module and calling methods on it.
Because this configuration information is global to a process, the configuration module stores the data structure containing the configuration in a lexical variable global to the module:
package Project::Config;
my $config;
sub load_config
{
$config = load_config( 'config/settings.yaml' );
}
1;
Everything was well and good until I saw YAML taking up more than 10% of
the execution time of one of the test files. I traced it to the configuration
module... which loaded the configuration file anew from its
import()
method.
For every other module in the system which used
Project::Config
, it dutifully re-read the configuration off of the
disk.
The tests run a little faster now.
This was a silly little pessimization anyone could have made, but it illustrates two interesting points. First, it shows that whoever wrote this code (I don't know who and I didn't look, because it could have been anyone) clearly had singleton concerns in mind, and rightly so. This is process-global data and it deserves to be available everywhere. Sure, the loading was a pessimization, but that's fixed now and everything still works. Success.
I take more interest in a subtler question: how should other modules
within the system access this configuration data? The current access
pattern is use Project::Config
and call methods that way, but that
demonstrates the concrete coupling problem I alluded to earlier, and it
certainly exacerbated the multiple-loading problem I fixed. What if, instead,
something external to the system could somehow inject an already-instantiated
configuration object into the other entities, such that none of them had to
couple themselves to the concrete module-name-to-filepath-mapping that
eventually called Project::Name
's import()
and
reloaded the configuration file?
Yes, that probably would have hidden the pessimization from my traces, but would that have mattered? It would also have hidden the effects of that pessimization.
That's not the only goal of my development process, but it's a benefit, and that's something to consider.
Just one note - that injected configuration does not need to be an object. It can be just a scalar or two or even a hash. That does not matter - the point is that it is the module you use that should define it's configuration (and other parameters). This way you can reuse your modules in other projects without carrying with you the whole configuration management - or even you can publish it to CPAN!
chromatic, thanks for looking at this problem space. It seems like it would particularly easy to make details form a config file available to an application, but it's surprisingly hard to do it well-- making sure the config file is read a minimum number of times, and also allowing project variations that might override some values.
I was looking at this problem space last week. In particular I was looking at possible replacements for CGI::Application::Plugin::Config::Perl based on a Moose Role and a Singleton. Even with that narrow focus, it's surprising how many attempts are out there to pre-package a solution. Here's a sampling of what I found. There are many more projects on CPAN that include their own home-grown config file solutions:
* Config::Role -- Uses File::HomeDir and Config::Any.
* MooseX::SimpleConfig -- requires the configuration to be in YAML.
* MooseX::Configuration -- requires an INI-style config file.
* MooseX::ConfigFromFile -- An abstract class. A sub-class could read our Perl-based config file and return an object configured with.
* MooseX::App::Plugin::Config -- It's part of the MooseX::App framework, and not a standalone thing. It could read a Perl-based config file, using Config::Any among other things.
* MooseX::Singleton -- Helps with singleton storage generally
* Mojito::Role::Config -- a project-specific example
* Config::Tiny::Singleton -- uses INI-style config files
Here's what I keep with myself for a project:
https://gist.github.com/4088910
Feedback welcome. I don't consider the design "done" yet. Eventually I want to support overriding some values in the configuration file, and end up with multiple singletons.
For example, mostly I want to deal with same configuration details throughout the code for a large website. But on the related mobile site, a few details change, but most are the same.
Deep recursion on subroutine "Project::Config::load_config"