Posiblemente, el patrón de diseño más simple del GoF es
el Singleton
, que es una forma de asegurar una única
instancia de una clase. El siguiente programa muestra cómo implementar
un Singleton
en C++:
//: C10:SingletonPattern.cpp #include <iostream> using namespace std; class Singleton { static Singleton s; int i; Singleton(int x) : i(x) { } Singleton& operator=(Singleton&); // Disallowed Singleton(const Singleton&); // Disallowed public: static Singleton& instance() { return s; } int getValue() { return i; } void setValue(int x) { i = x; } }; Singleton Singleton::s(47); int main() { Singleton& s = Singleton::instance(); cout << s.getValue() << endl; Singleton& s2 = Singleton::instance(); s2.setValue(9); cout << s.getValue() << endl; } ///:~
Listado 9.3. C10/SingletonPattern.cpp
La clave para crear un Singleton es evitar que el programador cliente tenga control sobre el ciclo de vida del objeto. Para lograrlo, declare todos los constructores privados, y evite que el compilador genere implícitamente cualquier constructor. Fíjese que el FIXME: constructor de copia? y el operador de asignación (que intencionadamente carecen de implementación alguna, ya que nunca van a ser llamados) están declarados como privados, para evitar que se haga cualquier tipo de copia.
También debe decidir cómo va a crear el objeto. Aquí, se crea de forma estática, pero también puede esperar a que el programador cliente pida uno y crearlo bajo demanda. Esto se llama "inicialización vaga", y sólo tiene sentido si resulta caro crear el objeto y no siempre se necesita.
Si devuelve un puntero en lugar de una referencia, el usuario podría borrar el puntero sin darse cuenta, por lo que la implementación citada anteriormente es más segura (el destructor también podría declararse privado o protegido para solventar el problema). En cualquier caso, el objeto debería almacenarse de forma privada.
Usted da acceso a través de FIXME (funciones de miembros)
públicas. Aquí,
instance()
genera una referencia al objeto Singleton
. El resto
de la interfaz (getValue()
y setValue()
) es la interfaz regular
de la clase.
Fíjese en que no está restringido a crear un único objeto. Esta
técnica también soporta la creacion de un pool
limitado de
objetos. En este caso, sin embargo, puede enfrentarse al problema de
compartir objetos del pool
. Si esto supone un problema,
puede crear una solución que incluya un check-out
y
un check-in
de los objetos compartidos.
Cualquier miembro static
dentro de una clase es una
forma de Singleton se hará uno y sólo uno. En cierto modo, el
lenguaje da soporte directo a esta idea; se usa de forma
regular. Sin embargo, los objetos estáticos tienen un problema
(ya miembros o no): el orden de inicialización, tal y como se
describe en el volumen 1 de este libro. Si un
objeto static
depende de otro, es importante que los
objetos se inicializen en el orden correcto.
En el volumen 1, se mostró cómo controlar el orden de
inicialización definiendo un objeto estático dentro de una
función. Esto retrasa la inicialización del objeto hasta la primera
vez que se llama a la función. Si la función devuelve una referencia
al objeto estático, hace las veces de Singleton
a la vez que
elimina gran parte de la preocupación de la inicialización
estática. Por ejemplo, suponga que quiere crear un fichero
de log
en la primera llamada a una función que devuelve una
referencia a dicho fichero. Basta con este fichero de cabecera:
//: C10:LogFile.h #ifndef LOGFILE_H #define LOGFILE_H #include <fstream> std::ofstream& logfile(); #endif // LOGFILE_H ///:~
Listado 9.4. C10/LogFile.h
La implementación no debe FIXME: hacerse en la misma línea, porque eso significaría que la función entera, incluída la definición del objeto estático que contiene, podría ser duplicada en cualquier unidad de traducción donde se incluya, lo que viola la regla de única definición de C++. [137] Con toda seguridad, esto frustraría cualquier intento de controlar el orden de inicialización (pero potencialmente de una forma sutil y difícil de detectar). De forma que la implementación debe separarse:
//: C10:LogFile.cpp {O} #include "LogFile.h" std::ofstream& logfile() { static std::ofstream log("Logfile.log"); return log; } ///:~
Listado 9.5. C10/LogFile.cpp
Ahora el objeto log
no se inicializará hasta la
primera vez que se llame a logfile()
. Así que, si crea una
función:
//: C10:UseLog1.h #ifndef USELOG1_H #define USELOG1_H void f(); #endif // USELOG1_H ///:~
Listado 9.6. C10/UseLog1.h
que use logfile()
en su implementación:
//: C10:UseLog1.cpp {O} #include "UseLog1.h" #include "LogFile.h" void f() { logfile() << __FILE__ << std::endl; } ///:~
Listado 9.7. C10/UseLog1.cpp
y utiliza logfile()
otra vez en otro fichero:
//: C10:UseLog2.cpp //{L} LogFile UseLog1 #include "UseLog1.h" #include "LogFile.h" using namespace std; void g() { logfile() << __FILE__ << endl; } int main() { f(); g(); } ///:~
Listado 9.8. C10/UseLog2.cpp
el objecto log
no se crea hasta la primera
llamada a f()
.
Puede combinar fácilmente la creación de objetos estáticos
dentro de una función miembro con la clase
Singleton
. SingletonPattern.cpp puede modificarse para
usar esta aproximación:[138]
//: C10:SingletonPattern2.cpp // Meyers' Singleton. #include <iostream> using namespace std; class Singleton { int i; Singleton(int x) : i(x) { } void operator=(Singleton&); Singleton(const Singleton&); public: static Singleton& instance() { static Singleton s(47); return s; } int getValue() { return i; } void setValue(int x) { i = x; } }; int main() { Singleton& s = Singleton::instance(); cout << s.getValue() << endl; Singleton& s2 = Singleton::instance(); s2.setValue(9); cout << s.getValue() << endl; } ///:~
Listado 9.9. C10/SingletonPattern2.cpp
Se da un caso especialmente interesante cuando
dos Singletons
dependen mutuamente el uno del otro, de esta
forma:
//: C10:FunctionStaticSingleton.cpp class Singleton1 { Singleton1() {} public: static Singleton1& ref() { static Singleton1 single; return single; } }; class Singleton2 { Singleton1& s1; Singleton2(Singleton1& s) : s1(s) {} public: static Singleton2& ref() { static Singleton2 single(Singleton1::ref()); return single; } Singleton1& f() { return s1; } }; int main() { Singleton1& s1 = Singleton2::ref().f(); } ///:~
Listado 9.10. C10/FunctionStaticSingleton.cpp
Cuando se llama a Singleton2::ref()
, hace que se cree
su único objeto Singleton2
. En el proceso de esta creación,
se llama a Singleton1::ref()
, y esto hace que se cree su
objeto único
Singleton1
. Como esta técnica no se basa en el orden de linkado
ni el de carga, el programador tiene mucho mayor control sobre la inicialización,
lo que redunda en menos problemas.
Otra variación del Singleton
separa la unicidad de un
objeto de su implementación. Esto se logra usando el "Patrón Plantilla
Curiosamente Recursivo" mencionado en el Capítulo 5:
//: C10:CuriousSingleton.cpp // Separates a class from its Singleton-ness (almost). #include <iostream> using namespace std; template<class T> class Singleton { Singleton(const Singleton&); Singleton& operator=(const Singleton&); protected: Singleton() {} virtual ~Singleton() {} public: static T& instance() { static T theInstance; return theInstance; } }; // A sample class to be made into a Singleton class MyClass : public Singleton<MyClass> { int x; protected: friend class Singleton<MyClass>; MyClass() { x = 0; } public: void setValue(int n) { x = n; } int getValue() const { return x; } }; int main() { MyClass& m = MyClass::instance(); cout << m.getValue() << endl; m.setValue(1); cout << m.getValue() << endl; } ///:~
Listado 9.11. C10/CuriousSingleton.cpp
MyClass
se convierte en Singleton
:
1. Haciendo que su constructor sea private
o protected
.
2. Haciéndose amigo de Singleton<MyClass>
.
3. Derivando MyClass
desde Singleton<MyClass>
.
La auto-referencia del paso 3 podría sonar inversímil, pero
tal como se explicó en el Capítulo 5, funciona porque sólo hay una
dependencia estática sobre el argumento plantilla de la
plantilla Singleton
. En otras palabras, el código de la
clase Singleton<MyClass>
puede ser instanciado por el
compilador porque no depende del tamaño de MyClass
. Es
después, cuando se a Singleton<MyClass>::instance()
,
cuando se necesita el tamaño de
MyClass
, y para entonces MyClass
ya se ha compilado y su tamaño
se conoce.[139]
Es interesante lo intrincado que un patrón tan simple como el
Singleton
puede llegar a ser, y ni siquiera se han
tratado todavía asuntos de seguridad de hilos. Por último, el
patrón Singleton
debería usarse lo justo y necesario. Los
verdaderos objetos Singleton
rara vez aparecen, y la
última cosa para la que debe usarse un
Singleton
es para remplazar a una variable
global. [140]