Tempo geek - a beleza dos padrões de projeto. Modo singleton (meio): Por que não recomendo usar o modo singleton? Quais são as alternativas?

Embora o singleton seja um padrão de design muito comumente usado, nós o usamos frequentemente no desenvolvimento real.No entanto, algumas pessoas pensam que o singleton é um antipadrão e não é recomendado. Portanto, hoje irei falar sobre essas questões em detalhes em resposta a esta declaração: Quais são os problemas com o padrão de projeto singleton? Por que isso é chamado de anti-padrão? Se você não usa um singleton, como representar a classe única globalmente? Quais são as soluções alternativas?

Quais são os problemas com singletons?

Na maioria dos casos, usamos singletons em nossos projetos, que são usados ​​para representar algumas classes globalmente exclusivas, como classes de informações de configuração, classes de pool de conexão e classes geradoras de ID. O modo singleton é simples de escrever e fácil de usar.No código, não precisamos criar um objeto, basta chamá-lo diretamente através de um método como IdGenerator.getInstance (). GetId (). No entanto, esse método de uso é um pouco semelhante ao código rígido, o que traz muitos problemas. A seguir, vamos dar uma olhada nos problemas específicos.

1. Singleton não é amigável para recursos OOP

Sabemos que as quatro principais características da OOP são encapsulamento, abstração, herança e polimorfismo. O padrão de design singleton não suporta abstração, herança e polimorfismo. Por que você diz isso? Ainda usamos o exemplo de IdGenerator para explicar.


public class Order {
    
    
  public void create(...) {
    
    
    //...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

public class User {
    
    
  public void create(...) {
    
    
    // ...
    long id = IdGenerator.getInstance().getId();
    //...
  }
}

O uso de IdGenerator viola o princípio de design baseado em interfaces ao invés de implementação, e também viola as características abstratas de OOP como entendidas em um sentido amplo. No futuro, esperamos adotar diferentes algoritmos de geração de ID para diferentes negócios. Por exemplo, o ID do pedido e o ID do usuário são gerados usando diferentes geradores de ID. Para lidar com essa mudança na demanda, precisamos modificar todos os locais onde a classe IdGenerator é usada, de modo que as mudanças no código sejam relativamente grandes.


public class Order {
    
    
  public void create(...) {
    
    
    //...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = OrderIdGenerator.getIntance().getId();
    //...
  }
}

public class User {
    
    
  public void create(...) {
    
    
    // ...
    long id = IdGenerator.getInstance().getId();
    // 需要将上面一行代码,替换为下面一行代码
    long id = UserIdGenerator.getIntance().getId();
  }
}

Além disso, os singletons não são amigáveis ​​à herança e ao polimorfismo. A razão pela qual uso a palavra "hostil" em vez de "completamente sem suporte" aqui é porque, teoricamente, uma classe singleton também pode ser herdada e o polimorfismo pode ser alcançado, mas a implementação será muito estranha. Isso leva a uma baixa legibilidade do código. Pessoas que não entendem a intenção do projeto acharão tal projeto inexplicável. Portanto, uma vez que você escolha projetar uma classe em uma classe singleton, isso significa desistir dos dois poderosos recursos orientados a objetos de herança e polimorfismo, o que equivale a perder a escalabilidade que pode lidar com mudanças futuras nos requisitos .

2. Singleton irá ocultar dependências entre classes

Sabemos que a legibilidade do código é muito importante. Ao ler o código, esperamos ver rapidamente as dependências entre as classes e descobrir de quais classes externas essa classe depende.

As dependências entre as classes declaradas por meio de construtores, passagem de parâmetros, etc. podem ser facilmente identificadas observando a definição da função. No entanto, a classe singleton não precisa ser criada explicitamente e não precisa depender da passagem de parâmetro e pode ser chamada diretamente na função. Se o código for mais complicado, essa relação de chamada ficará muito oculta. Ao ler o código, precisamos verificar cuidadosamente a implementação do código de cada função para saber em quais classes singleton essa classe depende.

3. Singleton não é amigável para escalabilidade de código

Sabemos que uma classe singleton pode ter apenas uma instância de objeto. Se um dia no futuro, precisarmos criar duas ou mais instâncias no código, precisaremos fazer mudanças relativamente grandes no código. Você pode dizer: haverá tal demanda? Visto que classes singleton são usadas para representar classes globais na maioria dos casos, por que duas ou mais instâncias são necessárias?

Na verdade, essa demanda não é incomum. Vamos usar o conjunto de conexões de banco de dados como um exemplo para explicar.

No estágio inicial do design do sistema, sentimos que deveria haver apenas um pool de conexão de banco de dados no sistema, para que pudéssemos controlar o consumo de recursos de conexão de banco de dados. Portanto, projetamos a classe de pool de conexão de banco de dados como uma classe singleton. Mas então descobrimos que algumas instruções SQL no sistema são executadas muito lentamente. Quando essas instruções SQL são executadas, elas ocupam os recursos de conexão do banco de dados por um longo tempo, fazendo com que outras solicitações SQL falhem na resposta. Para resolver esse problema, esperamos isolar o SQL lento de outro SQL para execução. Para atingir esse objetivo, podemos criar dois pools de conexão de banco de dados no sistema. Slow SQL tem um pool de conexão de banco de dados exclusivo e outros SQLs têm um pool de conexão de banco de dados exclusivo. Dessa forma, podemos evitar que SQL lento afete a execução de outros SQLs.

Se projetarmos o pool de conexão de banco de dados como uma classe singleton, obviamente não será capaz de se adaptar a tais mudanças nos requisitos, ou seja, a classe singleton afetará a escalabilidade e flexibilidade do código em alguns casos. Portanto, é melhor não projetar pools de recursos, como pools de conexão de banco de dados e pools de thread como singletons. Na verdade, alguns pools de conexão de banco de dados de software livre e pools de threads não são projetados como singletons.

4. Singleton não é amigável para testabilidade de código

O uso do modo singleton afetará a testabilidade do código. Se a classe singleton depende de um recurso externo relativamente pesado, como DB, quando escrevemos testes de unidade, esperamos substituí-lo por simulação. O uso embutido de classes singleton torna impossível implementar a substituição simulada.

Além disso, se a classe singleton contém variáveis ​​de membro (como a variável de membro id em IdGenerator), ela é realmente equivalente a um tipo de variável global, compartilhada por todos os códigos. Se esta variável global é uma variável global variável, ou seja, suas variáveis ​​de membro podem ser modificadas, então quando escrevemos testes de unidade, também precisamos prestar atenção a diferentes casos de teste, modificar a classe singleton O valor da mesma variável membro causa o problema de influência mútua dos resultados do teste.

5. Singleton não suporta construtor parametrizado

Singletons não oferecem suporte a construtores parametrizados. Por exemplo, quando criamos um objeto singleton de um pool de conexão, não podemos especificar o tamanho do pool de conexão por meio de parâmetros. Em resposta a este problema, vamos ver quais soluções estão disponíveis.

A primeira solução é: após criar a instância, chame a função init () para passar os parâmetros. Deve-se observar que, quando usamos essa classe singleton, devemos chamar o método init () antes de chamar o método getInstance (), caso contrário, o código lançará uma exceção. A implementação do código específico é a seguinte:


public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    
    
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public static Singleton getInstance() {
    
    
    if (instance == null) {
    
    
       throw new RuntimeException("Run init() first.");
    }
    return instance;
  }

  public synchronized static Singleton init(int paramA, int paramB) {
    
    
    if (instance != null){
    
    
       throw new RuntimeException("Singleton has been created!");
    }
    instance = new Singleton(paramA, paramB);
    return instance;
  }
}

Singleton.init(10, 50); // 先init,再使用
Singleton singleton = Singleton.getInstance();

A segunda solução é colocar os parâmetros no método getIntance (). A implementação do código específico é a seguinte:


public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton(int paramA, int paramB) {
    
    
    this.paramA = paramA;
    this.paramB = paramB;
  }

  public synchronized static Singleton getInstance(int paramA, int paramB) {
    
    
    if (instance == null) {
    
    
      instance = new Singleton(paramA, paramB);
    }
    return instance;
  }
}

Singleton singleton = Singleton.getInstance(10, 50);

Não sei se você notou que a implementação do código acima é um pouco problemática. Se executarmos o método getInstance () duas vezes como segue, os paramA e paramB obtidos de singleton1 e signleton2 são 10 e 50. Em outras palavras, o segundo parâmetro (20, 30) não funcionou e o processo de construção não deu um prompt, o que enganaria os usuários. Como resolver este problema? Deixe com seu próprio pensamento, você pode falar sobre suas idéias de solução na área de mensagem.


Singleton singleton1 = Singleton.getInstance(10, 50);
Singleton singleton2 = Singleton.getInstance(20, 30);

A terceira solução é colocar os parâmetros em outra variável global. A implementação do código específico é a seguinte. Config é uma variável global que armazena os valores de paramA e paramB. O valor interno pode ser definido por constantes estáticas como o código a seguir, ou pode ser carregado do arquivo de configuração. Na verdade, esse método é o mais recomendado.


public class Config {
    
    
  public static final int PARAM_A = 123;
  public static final int PARAM_B = 245;
}

public class Singleton {
    
    
  private static Singleton instance = null;
  private final int paramA;
  private final int paramB;

  private Singleton() {
    
    
    this.paramA = Config.PARAM_A;
    this.paramB = Config.PARAM_B;
  }

  public synchronized static Singleton getInstance() {
    
    
    if (instance == null) {
    
    
      instance = new Singleton();
    }
    return instance;
  }
}

Quais são as soluções alternativas?

Acabamos de mencionar muitos problemas com os solteiros. Você pode dizer que mesmo que haja tantos problemas com os solteiros, eu não preciso. Minha empresa tem um requisito para uma classe globalmente exclusiva. Se eu não usar um singleton, como posso garantir que os objetos dessa classe sejam globalmente exclusivos?

Para garantir a exclusividade global, além de usar singletons, também podemos usar métodos estáticos para alcançar. Esta também é uma ideia de implementação frequentemente usada no desenvolvimento de projetos. Por exemplo, o exemplo do gerador de incremento de ID exclusivo mencionado na lição anterior pode ser implementado com um método estático, que se parece com este:


// 静态方法实现方式
public class IdGenerator {
    
    
  private static AtomicLong id = new AtomicLong(0);
  
  public static long getId() {
    
     
    return id.incrementAndGet();
  }
}
// 使用举例
long id = IdGenerator.getId();

No entanto, a realização de métodos estáticos não resolve os problemas que mencionamos anteriormente. Na verdade, é mais inflexível do que singletons, por exemplo, não pode suportar o carregamento lento. Vamos ver se tem outro jeito. Na verdade, existe outra maneira de usar um singleton, além da forma como usávamos antes. O código específico é o seguinte:


// 1. 老的使用方式
public demofunction() {
    
    
  //...
  long id = IdGenerator.getInstance().getId();
  //...
}

// 2. 新的使用方式:依赖注入
public demofunction(IdGenerator idGenerator) {
    
    
  long id = idGenerator.getId();
}
// 外部调用demofunction()的时候,传入idGenerator
IdGenerator idGenerator = IdGenerator.getInsance();
demofunction(idGenerator);

Com base no novo uso, passamos o objeto gerado pelo singleton como parâmetro para a função (também pode ser passado para a variável membro da classe por meio do construtor), que pode resolver o problema de dependências ocultas entre as classes do singleton. No entanto, outros problemas com singletons, como recursos OOP hostis, escalabilidade e testabilidade, não podem ser resolvidos.

Portanto, se quisermos resolver completamente esses problemas, talvez tenhamos que encontrar outras maneiras de implementar classes globalmente exclusivas a partir da raiz. Na verdade, a exclusividade global dos objetos de classe pode ser garantida de muitas maneiras diferentes. Podemos aplicar a garantia por meio do modelo singleton ou por meio do modelo de fábrica, contêiner IOC (como o contêiner Spring IOC) para garantir isso, mas também por meio dos próprios programadores (certifique-se de não criar duas classes ao escrever o código Objeto). Isso é semelhante ao JVM responsável pela liberação de objetos de memória em Java, enquanto o programador é responsável por isso em C ++. O motivo é o mesmo.

Para a explicação detalhada do modelo de fábrica alternativo e do contêiner IOC, iremos explicá-lo nos capítulos seguintes.

Acho que você gosta

Origin blog.csdn.net/zhujiangtaotaise/article/details/110443138
Recomendado
Clasificación