O problema do singleton

Março 2017

O Singleton é um dos padrões de design (design pattern) mais conhecidos e mais fáceis a serem implantados. Mas também, é um dos mais mal utilizados, pois ele é muitas vezes mas escolhido para desenvolver certas práticas não compatíveis. De um modo geral, com um Singleton, é bem fácil recuperar a instância a partir de qualquer ponto do aplicativo criando dependências transversais que logo se mostrarão difíceis de serem mantidas e desenvolvidas. Em outras palavras, com o singleton, você pode, como diz o ditado popular, "Pegar o bonde errado"!


Implementação habitual de um singleton

Na maioria das vezes, ele se baseia em dois pontos importantes para ser desenvolvido em PHP: uma alteração da visibilidade do fabricante para impedir o distanciamento fora da classe (+ proibição de clonagem) e a criação de um método estático para fornecer a instância:


 php><?php
class Singleton {
private static $instance = null;

private function __construct()
{
}

private function __clone()
{
}

public static function getInstance()
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
}

$singleton = Singleton::getInstance();

// Gera um erro fatal. Catchable avec PHP 7
// "Call to private Singleton::__construct() from invalid context"
$singleton = new Singleton();


O principal problema desta implementação é o uso de um método estático que permite que qualquer desenvolvedor recupere a instância, seja qual for o contexto. Por exemplo, uma exibição encarregada de mostrar uma lista de opções, que chama diretamente o banco de dados para executar uma consulta SQL e recuperar seu conteúdo. Não é que devamos fazer absolutamente o MVC, mas a separação das responsabilidades fornece um ótimo nível de manutenção. Se, o desenvolvedor estiver apressado, ele vai pegar o caminho mais curto, e com a primeira marcha engatada, ele se deixará envolver demais pela velocidade.

Que solução

Na verdade, se o problema for o método estático, é melhor optar por uma fábrica ("factory"), cujo papel é o de nos fazer voltar uma instância da classe e, se o chamarmos várias vezes, retornamos à instância já criada. Portanto, não temos mais uma única classe, mas duas. O problema desta solução é que o fabricante se torna público possibilitando criar outras instâncias da classe de singleton. Para evitar isso, desde o PHP 7, é possível criar uma classe anônima dentro da fábrica:


<?php
class Factory {
    private $instance = null;
    
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new class() {
                private $var;
                public function get() {
                    return $this->var;
                }
                public function set($value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);
$o2 = $factory->getInstance();

// Exibe "int(5)"
var_dump($o2->get());


O problema desta solução é que, não é possível fazer uma configuração típica (tipo de sugestão) porque a classe retornada é anônima "object(class@anonymous)". Este é um falso problema, já que a configuração típica deveria basear-se nas interfaces, e, neste caso, será proposta uma classe anônima que implemente uma interface:


<?php
interface MyInterface {
    public function get(): int;
    public function set(int $value);
}

class Factory {
    private $instance = null;
    
    public function getInstance(): MyInterface
    {
        if ($this->instance === null) {
            $this->instance = new class() implements MyInterface {
                private $var;
                public function get(): int {
                    return $this->var;
                }
                public function set(int $value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

function display(MyInterface $object)
{
    var_dump($object->get());
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);
$o2 = $factory->getInstance();

display($o2);


O problema agora é que é possível instanciar várias fábricas e, por conseguinte, ter várias instâncias do nosso singleton. Poderíamos declarar a propriedade "$ instance" estática mas, neste caso, voltamos ao problema da criação de uma instância da fábrica em qualquer lugar do código, para dispor do nosso singleton. Na realidade, o ideal é impedir que se instancie várias vezes a nossa classe e manter a nossa única instância na nossa fábrica, ou seja, uma Classe Musket (Musket Class). Porém, vamos encontrar um problema de classes anônimas, a classe é diferentes de uma instância para a outra e o uso de uma propriedade estática não identifica se esta é uma nova instância:


<?php

class Factory {
    private $instance = null;
    
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new class() {
                private static $instanciated = false;
                
                public function __construct()
                {
                    if (self::$instanciated) {
                        throw new RuntimeException('Could not instanciate class');
                    }
                    self::$instanciated = true;
                }
                
                private $var;
                
                public function get(): int {
                    return $this->var;
                }
                
                public function set(int $value) {
                    $this->var = $value;
                }
            };
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);

$factory2 = new Factory();
// Esperaríamos uma exceção aqui ... mas não 
$o2 = $factory->getInstance();
var_dump($o2->get());


Mas, como não podemos instanciar a nossa classe várias vezes, não somos mais obrigados a usar uma classe anônima, então, o código ficaria assim:


<?php
class Singleton {
    private static $instanciated = false;
    private $var;
    
    public function __construct()
    {
        if (self::$instanciated === true) {
            throw new RuntimeException('Could not instanciate class');
        }
        self::$instanciated = true;
    }

    // Proibição de Clonagem
    private function __clone()
    {
    }
    
    public function get(): int {
        return $this->var;
    }
    
    public function set(int $value) {
        $this->var = $value;
    }
}

class Factory {
    private $instance = null;
    
    public function getInstance()
    {
        if ($this->instance === null) {
            $this->instance = new Singleton();
        }
        return $this->instance;
    }
}

$factory = new Factory();
$o = $factory->getInstance();
$o->set(5);

$factory2 = new Factory();
// Gera uma exceção "Could not instanciate class"
$o2 = $factory2->getInstance();


Neste caso, não temos um singleton, mas uma Classe Musket (Musket Class). Aliás, podemos aplicar este mesmo princípio na própria fábrica.

Uma desvantagem da Classe Musket é a sua capacidade de teste, porque uma vez instanciada, não podemos mais fazê-lo, ou seja, não podemos testar vários casos. Este problema também existe com o singleton e não é raro adicionar um método estático "resetInstance ()". No caso da Classe Musket, também podemos adicionar um método do tipo "rearmar()" para ajudar a criar uma nova instância. Mas, "Qual seria o interesse de tudo isso?" O singleton é usado, na maior parte dos casos, pelo desempenho e pela sua simplicidade. Além disso, podemos ter um bom desenvolvimento e criar um código que não seja muito simples. Apesar de concordar com o desempenho e a simplicidade, não acredito no seu uso rigoroso. Se você estiver sozinho em seu projeto e não tiver nenhuma data de entrega a ser respeitada, aí sim, talvez seja possível. Em todos os outros casos, a minha experiência tem demonstrado que ele não aguenta muito tempo: Deadline is a bitch!

Veja também

Artigo original publicado por . Tradução feita por pintuda.
Este documento, intitulado 'O problema do singleton', está disponível sob a licença Creative Commons. Você pode copiar e/ou modificar o conteúdo desta página com base nas condições estipuladas pela licença. Não se esqueça de creditar o CCM (br.ccm.net) ao utilizar este artigo.