Se há alguma coisa que eu realmente odeio, essa coisa é uma variável global. Variáveis globais transformam programas em uma massa enrolada e desconexa como um espaguete sem a necessidade de um único comando goto.
O padrão Singleton nos ajuda a evitar variáveis globais e, ainda, inibe alguns erros de linkagem que variáveis globais podem provocar quando compilamos nossos programas. Entre uma variável global e um Singleton, eu sempre prefiro o Singleton, a não ser que seja impossível usá-lo.
Um singleton é apenas uma classe que pode ter um, e apenas um objeto como instância. Toda vez que tentamos acessar uma instância de um Singleton, acessamos sempre a mesma instância.
Uma implementação bastante interessante de Singleton pode ser vista abaixo:
class Singleton {
public:
static Singleton *access();
virtual int getValue();
virtual void setValue(const int &val);
protected:
Singleton() { this->value = 0; }
~Singleton() {}
protected:
int value;
};
Singleton *Singleton::access() {
static Singleton *single = NULL;
if(single == NULL) {
single = new Singleton();
}
return single;
}
int Singleton::getValue() {
return this->value;
}
void Singleton::setValue(const int &val) {
this->value = val;
}
Muito simples o padrão Singleton, não? E muito, muito útil. Qualquer variável global pode ser colocada dentro da classe Singleton, para evitar dados desorganizados espalhados pelo programa todo. Use o padrão Singleton para trazer forte organização ao seu código, a não ser que você goste de lidar com um monte de variáveis globais, uma vez que elas tornam a programação em um jogo de caça ao tesouro - mas, se você perder esse jogo, estará realmente derrotado (perdendo o seu próprio emprego, por exemplo).
Uma das características mais interessantes do Singleton é que tanto o seu Constructor quanto o seu Destructor são variáveis protegidas, dentro da classe. Elas não podem ser acessada fora da própria classe Singleton. Uma vez que um Singleton é instanciado, não faz sentido fornecer um Destructor para apagá-lo da memória. E o Singleton não causará vazamentos de memória, uma vez que ele não se multiplicará dentro do programa (ele é uma instância única, de toda forma), e Singletons, geralmente, são utilizados durante todo o período de execução do sistema. Eles serão desalocados automaticamente pelo sistema operacional quando o programa terminar.
Singletons, geralmente, são muito úteis em contextos de multi-threading. Nesse caso, as funções de acesso aos dados da classe precisam de algum tipo de sincronização, como pode ser visto no exemplo abaixo:
class SynchroSingleton {
public:
static SynchroSingleton *access();
virtual int getValue();
virtual void setValue(const int &val);
protected:
SynchroSingleton()
{ this->value = 0; }
~SynchroSingleton() {}
void lock();
void unlock();
protected:
int value;
/**
* Variables needed to lock
* and unlock functions:
*/
// ...
};
SynchroSingleton
*SynchroSingleton::access() {
static SynchroSingleton
*single = NULL;
if(single == NULL) {
single =
new SynchroSingleton();
}
return single;
}
int SynchroSingleton::getValue() {
return this->;value;
}
void
SynchroSingleton::setValue
(const int &val) {
this->value = val;
}
void SynchroSingleton::lock() {
/**
* Specific synchronization code,
* using Critical Section
* objects. This is platform
* specific and outside the scope
* of this tutorial. In
Linux or
* BSD you can use POSIX threads.
*
*/
}
void SynchroSingleton::unlock() {
/**
* Specific synchronization code,
* using Critical Section
* objects. This is platform
* specific and outside the scope
* of this tutorial. In Linux or
* BSD you can use POSIX threads.
*
*/
}
No exemplo acima, a atualização da variável value não precisa ser sincronizada porque, na maioria dos sistemas, uma atribuição simples a um inteiro é uma instrução atômica à CPU, mas, como o código acima é apenas um exemplo, ele mostra como usar a sincronização em uma instância de um Singleton. O pattern de Multiple-Read, Single Write, que será apresentando futuramente, também pode ser usado em conjunção com o Singleton.
O código a seguir mostra como chamar uma instância do Singleton:
Singleton.cpp:
#include <iostream>
using namespace std;
class Singleton {
public:
static Singleton *access();
virtual int getValue();
virtual void setValue
(const int &val);
protected:
Singleton() { this->value = 0; }
~Singleton() {}
protected:
int value;
};
Singleton *Singleton::access() {
static Singleton *single = NULL;
if(single == NULL) {
single = new Singleton();
}
return single;
}
int Singleton::getValue() {
return this->value;
}
void
Singleton::setValue(const int &val) {
this->value = val;
}
int main() {
cout << "Hello world!" << endl;
Singleton *s = Singleton::access();
cout << "Singleton address: "
<< (int) s
<< endl;
cout << "Let's load \"another\" "
<< singleton instance..."
<< endl;
Singleton *another_s =
Singleton::access();
cout << "New instance singleton "
<< "address: "
<< (int) another_s
<< endl;
cout << "Then: what's up, Doc?"
<< endl;
return 0;
}
Então, apenas compile o programa e rode:
g++ Singleton.cpp -o sing
$ ./sing
Hello world!
Singleton address: 1048912
Let's load "another" singleton instance...
New instance singleton address: 1048912
Then: what's up, Doc?
Como você pode ver, acima, ambas as chamadas a Singleton::access() retornam o mesmo endereço de memória, portanto o mesmo ponteiro para o mesmo objeto, uma vez que ele não pode ser destruído fora da própria classe Singleton.
No próximo artigo discutiremos a respeito do pattern Inversion of Control.
Créditos:
[1] Esse artigo foi publicado originalmente no meu blog em:
http://psfdeveloper.blogspot.com