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"!
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.
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!