[C++] —— Explicação detalhada do padrão singleton

Prefácio:

  • Nesta edição, explicarei o conhecimento relevante sobre o modo singleton, um padrão de design comum em C++! !

Índice

(1) Seis princípios de padrões de design

(2) Classificação de padrões de projeto

(3) Modo Singleton

1. Definição

 2. Método de implementação

1️⃣ Modo preguiçoso

 2️⃣ Modo Homem Faminto

(4) Implementação segura do modo lento

Resumir


Primeiramente, o que precisamos saber é que os padrões de projeto são o resumo da experiência de desenvolvimento de código pelos antecessores e uma série de rotinas para resolver problemas específicos. Não é um requisito gramatical, mas um conjunto de soluções para melhorar a reutilização, manutenção, legibilidade, robustez e segurança do código.
 


(1) Seis princípios de padrões de design

Princípio de Responsabilidade Única

  • A responsabilidade de uma classe deve ser única e um método deve fazer apenas uma coisa. A divisão de responsabilidades é clara e cada mudança se resume à menor unidade de método ou classe.
  • Sugestões de uso : Duas funções completamente diferentes não devem ser colocadas em uma classe. Uma classe deve ser um conjunto de funções e encapsulamento de dados altamente relacionados.
  • Caso de uso : Bate-papo na Internet: a comunicação e o bate-papo na Internet devem ser divididos em categorias de comunicação em rede e bate-papo
     

Princípio Aberto Fechado

  • Aberto para extensão, fechado para modificação
  • Sugestões de uso : Para fazer alterações em entidades de software, é melhor usar extensões em vez de modificações.
  • Caso de uso : Venda de horas extras: preço do produto --- não modificando o preço original do produto, mas adicionando um novo preço promocional.

Princípio da Substituição de Liskov

  • Em termos leigos, desde que a classe pai possa aparecer, a subclasse pode aparecer, e substituí-la por uma subclasse não causará erros ou exceções.
  • Ao herdar uma classe, certifique-se de reescrever todos os métodos da classe pai. Preste atenção especial aos métodos protegidos da classe pai. As subclasses devem tentar não expor seus próprios métodos públicos para chamadas externas.
  • Sugestões de uso : A classe filha deve implementar totalmente os métodos da classe pai, e a classe filha pode ter personalidade própria. Ao substituir ou implementar métodos da classe pai, os parâmetros de entrada podem ser ampliados e a saída pode ser reduzida.
  • Caso de uso : classe Runner - pode correr, subclasse corredor de longa distância - pode correr e é bom em corridas de longa distância, subclasse velocista - pode correr e é bom em corridas de curta distância.

Princípio de Inversão de Dependência

  • Módulos de alto nível não devem depender de módulos de baixo nível, ambos devem depender de suas abstrações. A lógica atômica indivisível é o padrão de baixo nível, e a montagem da lógica atômica é o módulo de alto nível.
  • As dependências entre módulos ocorrem através de abstrações (interfaces), e não existem dependências diretas entre classes concretas.
  • Sugestões de uso : Cada classe deve ter uma classe abstrata tanto quanto possível, e nenhuma classe deve ser derivada de uma classe concreta. Tente não substituir os métodos da classe base. Use-o em conjunto com o princípio da substituição do lítio.
  • Caso de uso : classe de motorista Mercedes-Benz – só pode dirigir Mercedes-Benz; classe de motorista – dirige qualquer carro que lhe for dado; pessoa que dirige o carro: motorista – depende da abstração

Lei de Deméter, também conhecida como “Lei Menos Conhecida”

  • Minimize as interações entre objetos para reduzir o acoplamento entre classes. Um objeto deve ter conhecimento mínimo sobre outros objetos.
  • Existem requisitos claros para baixo acoplamento de aulas : comunique-se apenas com amigos diretos e também haja distância entre amigos. O seu é seu (se um método for colocado nesta classe, ele não aumentará o relacionamento entre classes nem terá um impacto negativo nesta classe, então coloque-o nesta classe)
  • Caso de uso : o professor pede ao líder da turma para fazer a chamada - o professor dá ao líder da turma uma lista, o líder da turma completa a chamada e verifica e retorna o resultado, em vez de o líder da turma fazer a chamada, o professor verifica a lista

Princípio de segregação de interface

  • O cliente não deve depender de interfaces de que não necessita, e as dependências entre classes devem ser estabelecidas na menor interface.
  • Sugestões de uso : Mantenha o design da interface o mais simples possível, mas não exponha interfaces que não tenham significado prático para o mundo exterior.
  • Caso de uso : Para alterar a senha, não deve haver uma interface para modificar as informações do usuário, mas uma única interface mínima de modificação de senha, e a operação do banco de dados não deve ser exposta.

Para entender os seis princípios de design como um todo, eles podem ser brevemente resumidos em uma frase, usando abstração para construir a estrutura e usando para implementar os detalhes de extensão. Para cada princípio de design, há uma nota correspondente:

  • O princípio da responsabilidade única nos diz que uma classe de implementação deve ter uma responsabilidade única;
  • O princípio da substituição diz-nos para não destruirmos o sistema de herança;
  • O princípio de inversão de dependência nos diz para programação orientada a interface;
  • O princípio do isolamento de interface nos diz para sermos simples ao projetar interfaces;
  • A Lei de Dimit nos diz para reduzir o acoplamento;
  • O princípio de abertura e fechamento é o esboço geral, dizendo-nos para estarmos abertos para extensão e fechados para modificação.

(2) Classificação de padrões de projeto

Os padrões de projeto podem ser classificados com base em sua finalidade e como são usados. A seguir estão as classificações comuns de padrões de design:

  1. Padrões Criacionais: Esses padrões concentram-se no processo de criação de objetos e na forma como eles são instanciados. Padrões de criação comuns incluem:

    • Padrão Singleton
    • Padrão de fábrica
    • Padrão abstrato de fábrica
    • Padrão de Construtor
    • Padrão de protótipo
  2. Padrões Estruturais: Esses padrões se concentram em como os objetos são combinados e relacionados para formar estruturas maiores. Padrões estruturais comuns incluem:

    • Padrão de adaptador
    • Padrão Decorador
    • Padrão de proxy
    • Padrão de ponte
    • Padrão Composto
    • Padrão de fachada
    • Padrão peso mosca
  3. Padrões Comportamentais: Esses padrões concentram-se na comunicação e interação entre objetos para definir a distribuição de responsabilidades e comportamentos entre objetos. Padrões comportamentais comuns incluem:

    • Padrão Observador
    • Padrão de Estratégia
    • Padrão de método de modelo
    • Padrão de Comando
    • Padrão Iterador
    • Padrão de estado
    • Padrão de Cadeia de Responsabilidade
    • Padrão Mediador
    • Padrão de visitante
    • Padrão de lembrança
    • Padrão de intérprete

Além dessas classificações principais, existem: modo simultâneo e modo pool de threads .


(3) Modo Singleton

Nesta edição, aprenderemos primeiro o primeiro padrão do padrão de design - o padrão singleton .

1. Definição

Uma classe só pode criar um objeto , ou seja, modo singleton. Esse padrão de design pode garantir que haja apenas uma instância dessa classe no sistema e fornecer um ponto de acesso global para acessá-la. Essa instância é compartilhada por todos os módulos do programa. Por exemplo, em um programa de servidor, as informações de configuração do servidor são armazenadas em um arquivo. Esses dados de configuração são lidos uniformemente por um objeto singleton e, em seguida, outros objetos no processo de serviço os obtêm por meio desse objeto singleton. Essas informações de configuração simplificam gerenciamento de configuração em ambientes complexos.


 2. Método de implementação

Geralmente existem dois modos de modo singleton, ou seja, singleton de estilo preguiçoso e singleton de estilo faminto . Os métodos de implementação dos dois modos são os seguintes:
 

1️⃣ Modo preguiçoso
 

Se a construção de um objeto singleton consumir muito tempo ou consumir muitos recursos , como carregamento de plug-ins, inicialização de conexões de rede, leitura de arquivos, etc., e for possível que o objeto não seja usado quando o o programa está em execução, então isso deve ser feito no início do programa. Apenas inicializá-lo fará com que o programa inicie muito lentamente . Portanto é melhor usar o modo lento (carregamento lento) neste caso! !

Existem dois métodos de design comuns para o modo lento:

  • a. Ponteiro estático + inicializado quando usado
  • b. Variáveis ​​estáticas locais
     

(1) Implementação do modo lento 1 : ponteiro estático + inicialização quando usado
 

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】

Em um único thread , este método de escrita pode ser usado corretamente, mas não funciona em multi-threads. Este método não é seguro para threads.

  • a. Suponha que o thread A e o thread B, esses dois threads desejam acessar a função getInstance, o thread A entra na função getInstance e verifica a condição if, porque é a primeira vez que entra, o valor está vazio, a condição if é true e a instância do objeto está pronta para ser criada.
  • b. No entanto, o thread A pode ser interrompido pelo agendador do sistema operacional e suspenso do modo de suspensão, e o controle será transferido para o thread B.
  • c. O thread B também chegou à condição if e descobriu que o valor ainda era NULL porque o thread A foi interrompido antes de ter tempo de construí-lo. Neste momento, presume-se que o thread B conclua a criação do objeto e retorne sem problemas.
  • d. Depois disso, o thread A acorda e continua a executar novos para criar objetos novamente. Dessa forma, dois threads constroem duas instâncias de objetos, o que destrói a exclusividade

Além disso, há também o problema do vazamento de memória . As coisas novas nunca são lançadas. O que se segue é uma melhoria no estilo do homem faminto.
 

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】

  • Ao final do programa, o sistema chamará o destruidor do membro estático do Singleton, Garbo, que excluirá a única instância do singleton.

Usar este método para liberar um objeto singleton tem as seguintes características:

  1. Definindo uma classe aninhada proprietária dentro de uma classe singleton
  2. Defina membros estáticos privados na classe singleton especificamente para lançamento;
  3. Utilize o recurso de destruição de variáveis ​​globais no final do programa para escolher o horário de lançamento final
     

【Perceber】

  1. Deve-se observar que este código não é seguro para threads em um ambiente multithread;
  2. Se vários threads forem usados ​​ao mesmo tempo  getInstance(), vários objetos poderão ser criados. Para implementar um modo singleton seguro para threads, mecanismos de sincronização apropriados precisam ser usados, como o uso de bloqueios mutex ou bloqueios de verificação dupla ( isso será discutido abaixo ).

(2) Implementação do modo preguiçoso 2 : variáveis ​​estáticas locais
 

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. No código acima, as declarações do construtor de cópia e do operador de atribuição são colocadas na seção privada, mas nenhuma definição é fornecida para elas. Dessa forma, qualquer tentativa de chamar essas funções fora da classe resultará em erro de vinculação. Este método atinge o objetivo de restringir a cópia e garantir a exclusividade de objetos singleton.
  2. Ao usar esse padrão,  o objeto singleton da classe  é obtido  Singleton<YourClass>::getInstance() chamando  uma função.getInstance()YourClass
  3. Este método de implementação também é um método comum de implementação no modo preguiçoso, não precisa usar  delete palavras-chave, mas restringe o comportamento da cópia privatizando o construtor de cópia e o operador de atribuição, alcançando assim a exclusividade do singleton.


 2️⃣ Modo Homem Faminto

Um objeto de instância exclusivo é criado quando o programa é iniciado . Como o objeto singleton foi determinado, ele é mais adequado para
ambientes
multithread . Multithreads não precisam bloquear o objeto singleton para obtê-lo, o que pode efetivamente evitar a competição de recursos e melhorar o desempenho.

(1) Implementação do padrão 1 do Hungry Man : definir diretamente 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;

vantagem:

  • Simples de implementar e seguro para vários threads.

deficiência:

  • a. Se houver vários objetos singleton e esses objetos singleton dependerem uns dos outros, poderá haver risco de travamento do programa. Motivo : Para o compilador, a ordem de inicialização e a ordem de destruição das variáveis ​​​​de membros estáticos é um comportamento indefinido;
  • b. Crie uma instância da classe no início do programa. Se o objeto Singleton for caro para gerar e raramente usado, esse método é um pouco pior do que a classe singleton preguiçosa do ponto de vista da eficiência na utilização de recursos. Mas do ponto de vista do tempo de resposta, é um pouco melhor que a classe singleton preguiçosa.


Condições de Uso:

  • a. Quando não deve haver construção e destruição de dependências.
  • b. Deseja evitar o consumo de desempenho ao travar frequentemente


(2) Implementação de padrão faminto 2 : ponteiro estático + implementação de novo espaço durante inicialização fora da classe
 

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();

【resumo】

  1. Deve-se notar que, como o modo faminto cria um objeto singleton quando o programa é iniciado, ele não pode obter o efeito de carregamento lento;
  2. Se o objeto singleton não precisar necessariamente ser usado durante a execução do programa, ocorrerá um consumo desnecessário de recursos;
  3. Portanto, o padrão preguiçoso é geralmente mais usado, criando objetos singleton somente quando necessário.

(4) Implementação segura do modo lento



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

Neste ponto, a explicação do padrão singleton termina. A seguir, vamos revisar e resumir brevemente este artigo! ! !

O modo preguiçoso e o modo faminto são implementações do modo singleton. Ambos são usados ​​para garantir que uma classe tenha apenas uma instância e fornecer um ponto de acesso global.

  • Modo preguiçoso : refere-se à criação de uma instância de objeto somente quando ele é usado pela primeira vez. Em termos de implementação específica, o modo lento geralmente julga o método getInstance() por meio do carregamento lento.Se a instância não tiver sido criada, ela será criada e retornada quando necessário. A vantagem do modo preguiçoso é que ele economiza recursos e as instâncias são criadas apenas quando necessário, mas a desvantagem é que um mecanismo de sincronização adicional é necessário em um ambiente multithread para garantir a segurança do thread.
  • Modo Hungry : refere-se à criação de uma instância de objeto quando a classe é carregada. Em termos de implementação específica, o Hungry Pattern cria diretamente um objeto de instância na variável de membro estático da classe e retorna a instância no método getInstance(). A vantagem do modo faminto é que ele é simples de implementar e não precisa considerar a questão da sincronização multithread, mas a desvantagem é que uma instância será criada quando o aplicativo for iniciado, o que pode desperdiçar alguns recursos.

Em aplicações reais, avalie as vantagens e desvantagens do modo preguiçoso e do modo faminto de acordo com a situação específica e escolha o método de implementação apropriado.

O texto acima é todo o conteúdo deste artigo. Obrigado a todos por assistir e apoiar! ! !

Acho que você gosta

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