The Curious Case of the Justifiable but Slow Singleton

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:

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.

Tags:

3 Comments

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: