While one of the main ideas behind Object Oriented programming is to be able to create multiple, and different instances of the same class, this is not always convenient.
If you have a class representing users of a system, we probably want to be able to create several instances, but in most applications there should be only one set of configuration at any given time.
The Singleton Pattern offers the theory behind the solution.
Let's start with a very simple example using the MooX::Singleton extension of Moo.
We have the MyConf.pm
file with the following content:
examples/singleton-moo/MyConf.pm
package MyConf;
use Moo;
with 'MooX::Singleton';
has file => (is => 'ro', required => 1);
1;
For now it only receives a filename, the name of the configuration file it is supposed to read, but to keep the example small we don't actually read the file.
Our "application" code looks like this:
examples/singleton-moo/application.pl
use strict;
use warnings;
use 5.010;
use MyConf;
my $c = MyConf->instance( file => 'conf.ini' );
say $c->file;
my $d = MyConf->instance( file => 'data.ini' );
say $d->file;
my $w = MyConf->instance();
say $w->file;
my $o = MyConf->new( file => 'other.ini' );
say $o->file;
my $z = MyConf->new( file => 'zorg.ini' );
say $z->file;
Instead of calling new
as the constructor, we must call the
instance
method. For the first call it behaves exactly
as the new
method would, but when we call it again it
disregards the parameters and returns the exact same object as earlier.
Calling new
does not have any special behavior. The output of
the above script will be:
conf.ini
conf.ini
conf.ini
other.ini
zorg.ini
Please note, the instance
method will silently ignore any
arguments passed in the second or any subsequent call.
This can cause surprises, so I'd recommend passing arguments only
in one place of the application and calling instance
without
any arguments in other places.
Why is this singleton interesting?
The above example is probably not showing why is using a singleton interesting.
What if we have an application implemented in 10 different modules, all of which need access to the configuration information?
One solution would be to create an instance of the MyConf
class in the main script
and then pass that object to the constructor of each class. Then each class would have
an attribute called conf
. This is doable, but it requires a lot of parameter passing.
Another solution is to have a global variable (e.g. our $conf = MyConf->new(...)
in the
main script and then access it via $main::conf
from every part of the code. This kind of
package global variable can work, but it does not look good. In every place we now have
to hard-code the fully qualified name of the variable. Soon we'll find a case (for example a test script)
that needs to work differently.
A third solution would be to create a MyConf
object in every class. Then we would probably need to
pass around the attributes of the constructor. In our example that is the name of the configuration file.
Even if we could solve that problem, this solution would mean we need to read the configuration information
in every class - and in every object of that class. This is both a run-time and a memory penalty.
A singleton class would allow us to create the instance in the main script using
my $conf = MyConf->instance( file => 'conf.ini' );
and then in every class we can just call
my $c = MyConf->instance;
and we know we get back the exact same object. Without touching
the configuration file, and without duplicating the configuration data in the memory.
Loading the content of the configuration file
Getting back to the actual example, it is not enough to have the name of the configuration file
passed to the MyConf
class. We would also like to load the content of the configuration file.
In this case we assume the configuration file is a simple INI file with sections and key-value pairs:
examples/singleton-moo-config/config.ini
[section A]
name=Foo
[section B]
name=Bar
We can load that using the Config::Tiny module
and we can do it in a BUILD
subroutine of Moo. This means the configuration file will be loaded
the first time we call instance
method where we are expected to pass the file => 'config.ini'
parameters when we create the object in the main script or setup subroutine.
Later calls to instance
won't call the BUILD
method any more and thus won't expect to have
a file => ...
parameter, nor will it even look at it.
examples/singleton-moo-config/MyConf.pm
package MyConf;
use Moo;
with 'MooX::Singleton';
use Config::Tiny;
has file => (is => 'ro', required => 1);
has conf => (is => 'rw');
sub BUILD {
my ($self) = @_;
print "BUILD called\n";
my $conf = Config::Tiny->read($self->file, 'utf8')
or die sprintf "Could not get configuration from '%s'", $self->file;
$self->conf($conf);
}
1;
In the main script we call:
MyConf->instance( file => 'conf.ini' );
then anywhere in our application we can have the following call to fetch the configuration values:
my $c = MyConf->instance();
my $a_value = $c->conf->{'section A'}{key};
See the main script:
examples/singleton-moo-config/application.pl
use strict;
use warnings;
use 5.010;
use MyConf;
use ModuleA;
use ModuleB;
sub setup {
MyConf->instance(file => 'config.ini');
}
setup();
ModuleA->new->do_something();
ModuleB->new->do_something_else();
and two modules used by that script. In ModuleA we call
instance
without passing any parameter:
examples/singleton-moo-config/ModuleA.pm
package ModuleA;
use 5.010;
use Moo;
sub do_something {
my ($self) = @_;
my $c = MyConf->instance();
my $a_name = $c->conf->{'section A'}{name};
say $a_name;
}
1;
In ModuleB we pass a new file => ...
parameter,
but it is disregarded by the code. The BUILD
function is not
even called:
examples/singleton-moo-config/ModuleB.pm
package ModuleB;
use 5.010;
use Moo;
sub do_something_else {
my ($self) = @_;
my $c = MyConf->instance(file => 'other.ini');
my $a_name = $c->conf->{'section A'}{name};
say $a_name;
}
1;
Running that script we get the following output:
$ perl application.pl
BUILD called
Foo
Foo