[C++]——Explicación detallada del patrón singleton

Prefacio:

  • En este número, explicaré el conocimiento relevante sobre el patrón singleton, ¡un patrón de diseño común en C++! !

Tabla de contenido

(1) Seis principios de patrones de diseño

(2) Clasificación de patrones de diseño.

(3) modo singleton

1. Definición

 2. Método de implementación

1️⃣ Modo perezoso

 2️⃣ Modo hombre hambriento

(4) Implementación segura del modo diferido

Resumir


En primer lugar, lo que necesitamos saber es que los patrones de diseño son el resumen de la experiencia de desarrollo de código de sus predecesores y una serie de rutinas para resolver problemas específicos. No es un requisito gramatical, sino un conjunto de soluciones para mejorar la reutilización, mantenibilidad, legibilidad, solidez y seguridad del código.
 


(1) Seis principios de patrones de diseño

Principio de responsabilidad única

  • La responsabilidad de la clase debe ser única y un método solo debe hacer una cosa. La división de responsabilidades es clara y cada cambio se realiza en la unidad más pequeña de método o clase.
  • Sugerencias de uso : Dos funciones completamente diferentes no deben colocarse en la misma clase, y una clase debe ser un grupo de funciones y encapsulación de datos altamente correlacionadas.
  • Caso de uso : chat de red: comunicación de red y chat, debe dividirse en comunicación de red y chat
     

Principio abierto cerrado

  • Abierto para extensión, cerrado para modificación
  • Sugerencias de uso : Para cambios en entidades de software, es mejor utilizar extensiones en lugar de modificaciones.
  • Caso de uso : venta de horas extras: precio del producto --- no para modificar el precio original del producto, sino para agregar un precio promocional.

Principio de sustitución de Liskov

  • En términos sencillos, siempre que pueda aparecer la clase principal, también puede aparecer la subclase, y reemplazarla con una subclase no provocará ningún error ni excepción.
  • Al heredar una clase, asegúrese de reescribir todos los métodos de la clase principal. Preste especial atención a los métodos protegidos de la clase principal. Las subclases deben intentar no exponer sus propios métodos públicos a llamadas externas.
  • Sugerencias de uso : La clase secundaria debe implementar completamente los métodos de la clase principal y la clase secundaria puede tener su propia personalidad. Al anular o implementar métodos de la clase principal, los parámetros de entrada se pueden ampliar y la salida se puede reducir.
  • Caso de uso : clase de corredor: puede correr, subclase de corredor de larga distancia: puede correr y es bueno en carreras de larga distancia, subclase de velocista: puede correr y es bueno en carreras de corta distancia.

Principio de inversión de dependencia

  • Los módulos de alto nivel no deberían depender de los módulos de bajo nivel, ambos deberían depender de sus abstracciones. La lógica atómica indivisible es el patrón de bajo nivel, y el conjunto de la lógica atómica es el módulo de alto nivel.
  • Las dependencias entre módulos se producen a través de abstracciones (interfaces) y no existen dependencias directas entre clases concretas.
  • Sugerencias de uso : cada clase debe tener una clase abstracta en la medida de lo posible y ninguna clase debe derivarse de una clase concreta. Intente no anular los métodos de la clase base. Úselo junto con el principio de reemplazo de litio.
  • Caso de uso : clase de conductor de Mercedes-Benz: solo puede conducir Mercedes-Benz; clase de conductor: conduce cualquier automóvil que se le proporcione; persona que conduce el automóvil: conductor: depende de la abstracción

Ley de Deméter, también conocida como "Ley Menos Conocida"

  • Minimiza las interacciones entre objetos para reducir el acoplamiento entre clases. Un objeto debe tener un conocimiento mínimo sobre otros objetos.
  • Hay requisitos claros para un bajo acoplamiento de clases : comunicarse solo con amigos directos y también hay una distancia entre amigos. Lo que es tuyo es tuyo (si se coloca un método en esta clase, no aumenta la relación entre clases ni tiene un impacto negativo en esta clase, entonces colócalo en esta clase)
  • Caso de uso : el maestro le pide al líder de la clase que pase lista; el maestro le da una lista al líder de la clase, el líder de la clase completa el pase de lista y verifica, y devuelve el resultado; en lugar de que el líder de la clase pase lista, el maestro verifica la lista

Principio de segregación de interfaz

  • El cliente no debe depender de interfaces que no necesita y las dependencias entre clases deben establecerse en la interfaz más pequeña.
  • Sugerencias de uso : Mantenga el diseño de la interfaz lo más simple posible, pero no exponga interfaces que no tengan importancia práctica para el mundo exterior.
  • Caso de uso : Para cambiar la contraseña, no debe haber una interfaz para modificar la información del usuario, sino una única interfaz mínima de modificación de contraseña, y la operación de la base de datos no debe estar expuesta.

Para comprender los seis principios de diseño en su conjunto, se pueden resumir brevemente en una oración: use la abstracción para construir un marco y use la implementación para expandir los detalles. Específicamente, cada principio de diseño corresponde a una nota:

  • El principio de responsabilidad única nos dice que una clase de implementación debe tener una responsabilidad única;
  • El principio de sustitución nos dice que no destruyamos el sistema de herencia;
  • El principio de inversión de dependencia nos indica la programación orientada a interfaz;
  • El principio de aislamiento de interfaz nos dice que seamos simples al diseñar interfaces;
  • La Ley de Dimit nos dice que reduzcamos el acoplamiento;
  • El principio abierto-cerrado es la pauta general que nos dice que estemos abiertos a la expansión y cerrados a la modificación.

(2) Clasificación de patrones de diseño.

Los patrones de diseño se pueden clasificar según su propósito y cómo se utilizan. Las siguientes son clasificaciones de patrones de diseño comunes:

  1. Patrones de creación: estos patrones se centran en el proceso de creación de objetos y la forma en que se crean instancias. Los patrones creacionales comunes incluyen:

    • Patrón singleton
    • Patrón de fábrica
    • Patrón abstracto de fábrica
    • Patrón de constructor
    • Patrón de prototipo
  2. Patrones estructurales: estos patrones se centran en cómo se combinan y relacionan los objetos para formar estructuras más grandes. Los patrones estructurales comunes incluyen:

    • Patrón de adaptador
    • Patrón decorador
    • Patrón de proxy
    • Patrón de puente
    • Patrón compuesto
    • Patrón de fachada
    • Patrón de peso mosca
  3. Patrones de comportamiento: estos patrones se centran en la comunicación e interacción entre objetos para definir la distribución de responsabilidades y comportamientos entre objetos. Los patrones de comportamiento comunes incluyen:

    • Patrón de observador
    • Patrón de estrategia
    • Patrón de método de plantilla
    • Patrón de comando
    • Patrón iterador
    • Patrón de estado
    • Patrón de cadena de responsabilidad
    • Patrón mediador
    • Patrón de visitante
    • Patrón de recuerdo
    • Patrón de intérprete

Además de estas clasificaciones principales, en realidad existen: modo concurrente y modo de grupo de subprocesos .


(3) modo singleton

En este número, primero aprendemos el primer patrón en los patrones de diseño: el patrón singleton .

1. Definición

Una clase solo puede crear un objeto , es decir, modo singleton. Este patrón de diseño puede garantizar que solo haya una instancia de esta clase en el sistema y proporcionar un punto de acceso global para acceder a ella. Esta instancia es compartida por todos los módulos del programa. Por ejemplo, en un programa de servidor, la información de configuración del servidor se almacena en un archivo. Estos datos de configuración son leídos uniformemente por un objeto singleton, y luego otros objetos en el proceso de servicio los obtienen a través de este objeto singleton. Esta información de configuración simplifica Gestión de la configuración en entornos complejos.


 2. Método de implementación

Por lo general, hay dos modos de modo singleton, a saber, singleton de estilo perezoso y singleton de estilo hambriento . Los dos modos se implementan de la siguiente manera:
 

1️⃣ Modo perezoso
 

Si la construcción de un objeto singleton requiere mucho tiempo o muchos recursos , como cargar complementos, inicializar conexiones de red, leer archivos, etc., es posible que el objeto no se utilice cuando se El programa se está ejecutando, entonces debe hacerse al comienzo del programa. Simplemente inicializarlo hará que el programa se inicie muy lentamente . ¡Así que es mejor usar el modo diferido (carga diferida) en este caso! !

Hay dos métodos de diseño comunes para el modo diferido:

  • A. Puntero estático + inicializado cuando se usa.
  • B. Variables estáticas locales
     

(1) Implementación del modo diferido 1 : puntero estático + inicialización cuando se usa
 

template<typename T>
class Singleton
{
public:
	static T& getInstance()
	{
		if (!_value)
		{
			_value = new T();
		}
		return *_value;
	}

private:

	Singleton() 
	{}

	~Singleton()
	{}

	static T* _value;
};

template<typename T>
T* Singleton<T>::_value = NULL;

【explicar】

En un solo subproceso , este método de escritura se puede utilizar correctamente, pero no funciona en subprocesos múltiples y este método no es seguro para subprocesos.

  • a. Si el hilo A y el hilo B quieren acceder a la función getInstance, el hilo A ingresa a la función getInstance y detecta la condición if. Como es la primera vez que ingresa, el valor está vacío, se establece la condición if y el objeto La instancia está lista para ser creada.
  • b. Sin embargo, el programador del sistema operativo puede interrumpir el subproceso A y suspenderlo del modo de suspensión, y el control se entregará al subproceso B.
  • c. El subproceso B también llegó a la condición if y descubrió que el valor todavía era NULL porque el subproceso A había sido interrumpido antes de que tuviera tiempo de construirlo. En este momento, se supone que el hilo B completa la creación del objeto y regresa sin problemas.
  • d. Luego, el hilo A se despierta y continúa ejecutando new para crear el objeto nuevamente. De esta manera, los dos hilos construyen dos instancias de objeto, lo que destruye la unicidad.

Además, también existe el problema de la pérdida de memoria . Las cosas nuevas nunca se publican. La siguiente es una mejora al estilo del hombre hambriento.
 

template<typename T>
class Singleton
{
public:
	static T& getInstance()
	{
		if (!_value)
		{
			_value = new T();
		}
		return *_value;
	}

private:
    // 实现一个内嵌垃圾回收类
	class CGarbo
	{
	public:
		~CGarbo()
		{
			if (Singleton::_value)
				delete Singleton::_value;
		}
	};

	static CGarbo Garbo;

	Singleton()
	{};

	~Singleton()
	{};

	static T* _value;
};

template<typename T>
T* Singleton<T>::_value = nullptr;

【explicar】

  • Al final del programa, el sistema llamará al destructor del miembro estático Garbo de Singleton, que eliminará la única instancia del singleton.

El uso de este método para liberar un objeto singleton tiene las siguientes características:

  1. Definir clases anidadas propietarias dentro de clases singleton
  2. Defina miembros estáticos privados en la clase singleton específicamente para su lanzamiento;
  3. Utilice la función de destrucción de variables globales al final del programa para elegir el tiempo de lanzamiento final
     

【Aviso】

  1. Cabe señalar que este código no es seguro para subprocesos en un entorno de subprocesos múltiples;
  2. Si se utilizan varios subprocesos al mismo tiempo  getInstance(), se pueden crear varios objetos. Para implementar un modo singleton seguro para subprocesos, se deben utilizar mecanismos de sincronización apropiados, como el uso de bloqueos mutex o bloqueos de doble verificación ( esto se discutirá a continuación )

(2) Implementación del modo diferido 2 : variables estáticas locales
 

template<typename T>
class Singleton {
public:
    static T& getInstance() {
        static T instance;
        return instance;
    }

private:
    Singleton() {}
    ~Singleton() {}

    Singleton(const Singleton&);
    Singleton& operator=(const Singleton&);
};

【explicar】

  1. En el código anterior, las declaraciones del constructor de copia y del operador de asignación se colocan en la sección privada, pero no se proporcionan definiciones para ellos. De esta manera, cualquier intento de llamar a estas funciones fuera de la clase resultará en un error de vinculación. Este método logra el propósito de restringir la copia y garantizar la unicidad de los objetos únicos.
  2. Cuando se utiliza este patrón,  el objeto singleton de la clase  se obtiene  Singleton<YourClass>::getInstance() llamando  a una función.getInstance()YourClass
  3. Este método de implementación también es un método de implementación de modo diferido común: no requiere el uso de  delete palabras clave, pero restringe el comportamiento de copia privatizando el constructor de copia y el operador de asignación, logrando así la unicidad del singleton.


 2️⃣ Modo hombre hambriento

Se crea un objeto de instancia único cuando se inicia el programa . Debido a que se ha determinado el objeto singleton, es más adecuado para
entornos de subprocesos
múltiples . Los subprocesos múltiples no necesitan bloquear el objeto singleton para obtenerlo, lo que puede evitar efectivamente la competencia de recursos y mejorar el rendimiento.

(1) Implementación del patrón 1 del hombre hambriento : definir directamente objetos estáticos
 

template<typename T>
class Singleton 
{
private:
	static T _eton;
private:
	Singleton() {}
	~Singleton() {}

public:
	Singleton(const Singleton&) = delete;
	Singleton& operator=(const Singleton&) = delete;
	static T& getInstance()
	{
		return _eton;
	}
};

template<typename T>
T Singleton<T>::_eton;

ventaja:

  • Fácil de implementar y seguro para múltiples subprocesos.

defecto:

  • a. Si hay varios objetos singleton y estos objetos singleton dependen unos de otros, puede existir el riesgo de que el programa falle. Motivo : para el compilador, el orden de inicialización y el orden de destrucción de las variables miembro estáticas es un comportamiento indefinido;
  • b. Cree una instancia de la clase al comienzo del programa. Si el objeto Singleton es costoso de generar y rara vez se usa, este método es ligeramente peor que la clase singleton perezosa desde la perspectiva de la eficiencia en la utilización de recursos. Pero desde la perspectiva del tiempo de respuesta, es ligeramente mejor que la clase singleton perezosa.


Condiciones de Uso:

  • a. Cuando definitivamente no existan dependencias de construcción y destrucción.
  • B. Quiere evitar el consumo de rendimiento del bloqueo frecuente.


(2) Implementación del patrón hambriento 2 : puntero estático + implementación de nuevo espacio durante la inicialización fuera de clase
 

template<typename T>
class Singleton
{
private:
    static T* _eton;

    Singleton() {}
    ~Singleton() {}

public:
    Singleton(const Singleton&) = delete;
    Singleton& operator=(const Singleton&) = delete;

    static T& getInstance()
    {
        return *_eton;
    }
};

template<typename T>
T* Singleton<T>::_eton = new T();

【resumen】

  1. Cabe señalar que dado que el modo hambriento crea un objeto singleton cuando se inicia el programa, no puede lograr el efecto de carga diferida;
  2. Si no es necesario utilizar objetos singleton durante la ejecución del programa, se producirá un consumo innecesario de recursos;
  3. Por lo tanto, el patrón diferido suele usarse con más frecuencia y crea objetos únicos solo cuando es necesario.

(4) Implementación segura del modo diferido



class Singleton
{
public:
	static Singleton* GetInstance()
	{
		// 双检查加锁
		if (m_pInstance == nullptr) {
			m_mutex.lock();
			if (m_pInstance == nullptr)
			{
				m_pInstance = new Singleton;
			}
			m_mutex.unlock();
		}
		return m_pInstance;
	}

	static void DelInstance()
	{
		m_mutex.lock();
		if (m_pInstance)
		{
			delete m_pInstance;
			m_pInstance = nullptr;
		}
		m_mutex.unlock();
	}

	// 实现一个内嵌垃圾回收类
	class CGarbo
	{
	public:
		~CGarbo()
		{
			DelInstance();
		}
	};
	// 定义一个静态成员变量,程序结束时,系统会自动调用它的析构函数从而释放单例对象
	static CGarbo Garbo;

	// 一般全局都要使用单例对象,所以单例对象一般不需要显示释放
	// 有些特殊场景,想显示释放一下
	void Add(const string& str)
	{
		_vmtx.lock();

		_v.push_back(str);

		_vmtx.unlock();
	}

	void Print()
	{
		_vmtx.lock();

		for (auto& e : _v)
		{
			cout << e << endl;
		}
		cout << endl;

		_vmtx.unlock();
	}

	~Singleton()
	{
		// 持久化
		// 比如要求程序结束时,将数据写到文件,单例对象析构时持久化就比较好
	}
private:
	mutex _vmtx;
	vector<string> _v;

private:
	// 构造函数私有
	Singleton()
	{}

	// 防拷贝
	//Singleton(Singleton const&);
	//Singleton& operator = (Singleton const&);

	static mutex m_mutex;			//互斥锁
	static Singleton* m_pInstance; // 单例对象指针
};

Singleton* Singleton::m_pInstance = nullptr;
Singleton::CGarbo Garbo;
mutex Singleton::m_mutex;

int main()
{
	srand(time(0));

	int n = 3000;
	thread t1([n]() {
		for (size_t i = 0; i < n; ++i)
		{
			Singleton::GetInstance()->Add("t1线程:" + to_string(rand()));
		}
		});

	thread t2([n]() {
		for (size_t i = 0; i < n; ++i)
		{
			Singleton::GetInstance()->Add("t2线程:" + to_string(rand()));
		}
		});

	t1.join();
	t2.join();

	Singleton::GetInstance()->Print();

	return 0;
}


Resumir

En este punto termina la explicación del patrón singleton. A continuación, ¡repasemos y resumamos brevemente este artículo! ! !

El modo perezoso y el modo hambriento son implementaciones del modo singleton y ambos se utilizan para garantizar que una clase tenga solo una instancia y proporcione un punto de acceso global.

  • Modo diferido : se refiere a crear una instancia de objeto solo cuando se usa por primera vez. En términos de implementación específica, el modo diferido generalmente se juzga en el método getInstance () a través de la carga retrasada. Si la instancia aún no se ha creado, se creará y devolverá cuando sea necesario. La ventaja del modo diferido es que ahorra recursos y crea instancias solo cuando es necesario, pero la desventaja es que requiere mecanismos de sincronización adicionales en un entorno de subprocesos múltiples para garantizar la seguridad de los subprocesos.
  • Modo hambriento : se refiere a la creación de una instancia de objeto cuando se carga la clase. En términos de implementación específica, Hungry Pattern crea directamente un objeto de instancia en la variable miembro estática de la clase y devuelve la instancia en el método getInstance (). La ventaja del modo hambriento es que es fácil de implementar y no necesita considerar problemas de sincronización de subprocesos múltiples, pero la desventaja es que la instancia se crea cuando se inicia la aplicación, lo que puede desperdiciar algunos recursos.

En aplicaciones reales, sopese las ventajas y desventajas del modo perezoso y el modo hambriento según la situación específica y elija el método de implementación adecuado.

Lo anterior es el contenido completo de este artículo. ¡Gracias a todos por mirar y apoyar! ! !

Supongo que te gusta

Origin blog.csdn.net/m0_56069910/article/details/132724371
Recomendado
Clasificación