Patrons de conception: Singleton
() translation by (you can also view the original English article)
Dans cet article vous apprendrez à implémenter le patron Singleton ainsi que pourquoi et quand utiliser ce patron dans votre application. Comme le nom "Singleton" le suppose, cette méthode vous permet de créer une et une seul instance d'une classe.
Regardons ce que Wikipedia peut nous dire de ce patron :
Le singleton est un patron de conception dont l'objet est de restreindre l'instanciation d'une classe à un seul objet. Il est utilisé lorsque l'on a besoin d'exactement un objet pour coordonner des opérations dans un système.
Comme mentionné dans la définition ci-dessus, lorsque l'on veut être sûr qu'un et un seul objet doit être créé pour une classe, alors on doit implémenter le patron Singleton pour cette classe.
Vous pouvez vous demander pourquoi nous devons implémenter cette casse qui nous autorise l'instanciation d'un seul objet. Je vous répondrais qu'il y a beaucoup de cas d'utilisation où l'on peut appliquer ce patron de conception. Cela inclus : les classes de configuration, de session, de base de données et tant d'autres.
Je prendrais l'exemple d'une classe de base de données pour cet article. Premièrement nous verrons quel peut être le problème si le patron singleton n'est pas implémenté sur cette classe.
Le problème
Imaginez une classe de connexion à la base de données très simple qui créé une connexion avec la base de données lorsqu'on créé un objet de cette classe.
1 |
class database { |
2 |
|
3 |
private $dbName = null, $dbHost = null, $dbPass = null, $dbUser = null; |
4 |
|
5 |
public function __construct($dbDetails = array()) { |
6 |
|
7 |
$this->dbName = $dbDetails['db_name']; |
8 |
$this->dbHost = $dbDetails['db_host']; |
9 |
$this->dbUser = $dbDetails['db_user']; |
10 |
$this->dbPass = $dbDetails['db_pass']; |
11 |
|
12 |
$this->dbh = new PDO('mysql:host='.$this->dbHost.';dbname='.$this->dbName, $this->dbUser, $this->dbPass); |
13 |
|
14 |
}
|
15 |
|
16 |
}
|
Dans le code ci-dessus, vous pouvez voir qu'on créé une connexion à la base de données à chaque fois que vous créez un objet de cette classe. Donc, si un développeur a créé un objet de cette classe à plusieurs endroits, imaginez le nombre de connexion (identiques) que cela créera avec le serveur de base de données.
Donc sans le savoir le développeur fait des erreurs qui auront un énorme impact sur la rapidité de la base de données et du serveur applicatif. Regardons la même chose en créant un objet différent de cette classe.
1 |
$dbDetails = array( |
2 |
'db_name' => 'designpatterns', |
3 |
'db_host' => 'localhost', |
4 |
'db_user' => 'root', |
5 |
'db_pass' => 'mysqldba' |
6 |
);
|
7 |
|
8 |
$db1 = new database($dbDetails); |
9 |
var_dump($db1); |
10 |
$db2 = new database($dbDetails); |
11 |
var_dump($db2); |
12 |
$db3 = new database($dbDetails); |
13 |
var_dump($db3); |
14 |
$db4 = new database($dbDetails); |
15 |
var_dump($db4); |
16 |
|
17 |
// Output
|
18 |
object(database)[1] |
19 |
private 'dbName' => string 'designpatterns' (length=14) |
20 |
private 'dbHost' => string 'localhost' (length=9) |
21 |
private 'dbPass' => string 'mysqldba' (length=8) |
22 |
private 'dbUser' => string 'root' (length=4) |
23 |
public 'dbh' => object(PDO)[2] |
24 |
object(database)[3] |
25 |
private 'dbName' => string 'designpatterns' (length=14) |
26 |
private 'dbHost' => string 'localhost' (length=9) |
27 |
private 'dbPass' => string 'mysqldba' (length=8) |
28 |
private 'dbUser' => string 'root' (length=4) |
29 |
public 'dbh' => object(PDO)[4] |
30 |
object(database)[5] |
31 |
private 'dbName' => string 'designpatterns' (length=14) |
32 |
private 'dbHost' => string 'localhost' (length=9) |
33 |
private 'dbPass' => string 'mysqldba' (length=8) |
34 |
private 'dbUser' => string 'root' (length=4) |
35 |
public 'dbh' => object(PDO)[6] |
36 |
object(database)[7] |
37 |
private 'dbName' => string 'designpatterns' (length=14) |
38 |
private 'dbHost' => string 'localhost' (length=9) |
39 |
private 'dbPass' => string 'mysqldba' (length=8) |
40 |
private 'dbUser' => string 'root' (length=4) |
41 |
public 'dbh' => object(PDO)[8] |
Si vous regardez la sortie du code ci-dessus, vous verrez que chaque objet à sa propre resource ID, donc tous les objets sont des références différentes, et par conséquent une allocation mémoire séparée est faite pour chaque connexion. Inconsciemment votre application va consommer de ressources dont elle n'a pas vraiment besoin.
La solution
Nous ne controlons pas comment les développeurs utilisent notre framework. Nous le contrôlons après la procédure de revue de code, mais pendant le développement nous ne pouvons pas les surveiller tout le temps.
Pour palier ce problème, nous devons rendre notre classe de base instanciable qu'une seul fois ; elle retournera l'instance créé si elle existe déjà. C'est le cas d'utilisation pour lequel nous avons décidé d'implémenter le patron Singleton pour nos classes de base.
En développant ce patron, nous nous concentrons sur le fait qu'il autorise la création d'un objet d'une classe une et une seule fois. Laissez moi vous montrer le code de cette classe et ensuite nous le détaillerons.
1 |
class database { |
2 |
|
3 |
private $dbName = null, $dbHost = null, $dbPass = null, $dbUser = null; |
4 |
private static $instance = null; |
5 |
|
6 |
private function __construct($dbDetails = array()) { |
7 |
|
8 |
// Please note that this is Private Constructor
|
9 |
|
10 |
$this->dbName = $dbDetails['db_name']; |
11 |
$this->dbHost = $dbDetails['db_host']; |
12 |
$this->dbUser = $dbDetails['db_user']; |
13 |
$this->dbPass = $dbDetails['db_pass']; |
14 |
|
15 |
// Your Code here to connect to database //
|
16 |
$this->dbh = new PDO('mysql:host='.$this->dbHost.';dbname='.$this->dbName, $this->dbUser, $this->dbPass); |
17 |
}
|
18 |
|
19 |
public static function connect($dbDetails = array()) { |
20 |
|
21 |
// Check if instance is already exists
|
22 |
if(self::$instance == null) { |
23 |
self::$instance = new database($dbDetails); |
24 |
}
|
25 |
|
26 |
return self::$instance; |
27 |
|
28 |
}
|
29 |
|
30 |
private function __clone() { |
31 |
// Stopping Clonning of Object
|
32 |
}
|
33 |
|
34 |
private function __wakeup() { |
35 |
// Stopping unserialize of object
|
36 |
}
|
37 |
|
38 |
}
|
Il y a quelques indications sur le fait que cette classe soit un Singleton. La première, un constructeur privé qui empêche l'instanciation via le mot clé new. Mais aussi un attribut statique qui contient la référence vers un objet déjà créé.
1 |
$dbDetails = array( |
2 |
'db_name' => 'designpatterns', |
3 |
'db_host' => 'localhost', |
4 |
'db_user' => 'root', |
5 |
'db_pass' => 'mysqldba' |
6 |
);
|
7 |
|
8 |
$db1 = database::connect($dbDetails); |
9 |
var_dump($db1); |
10 |
$db2 = database::connect($dbDetails); |
11 |
var_dump($db2); |
12 |
$db3 = database::connect($dbDetails); |
13 |
var_dump($db3); |
14 |
$db4 = database::connect($dbDetails); |
15 |
var_dump($db4); |
16 |
|
17 |
// Output
|
18 |
|
19 |
object(database)[1] |
20 |
private 'dbName' => string 'designpatterns' (length=14) |
21 |
private 'dbHost' => string 'localhost' (length=9) |
22 |
private 'dbPass' => string 'mysqldba' (length=8) |
23 |
private 'dbUser' => string 'root' (length=4) |
24 |
public 'dbh' => object(PDO)[2] |
25 |
object(database)[1] |
26 |
private 'dbName' => string 'designpatterns' (length=14) |
27 |
private 'dbHost' => string 'localhost' (length=9) |
28 |
private 'dbPass' => string 'mysqldba' (length=8) |
29 |
private 'dbUser' => string 'root' (length=4) |
30 |
public 'dbh' => object(PDO)[2] |
31 |
object(database)[1] |
32 |
private 'dbName' => string 'designpatterns' (length=14) |
33 |
private 'dbHost' => string 'localhost' (length=9) |
34 |
private 'dbPass' => string 'mysqldba' (length=8) |
35 |
private 'dbUser' => string 'root' (length=4) |
36 |
public 'dbh' => object(PDO)[2] |
37 |
object(database)[1] |
38 |
private 'dbName' => string 'designpatterns' (length=14) |
39 |
private 'dbHost' => string 'localhost' (length=9) |
40 |
private 'dbPass' => string 'mysqldba' (length=8) |
41 |
private 'dbUser' => string 'root' (length=4) |
42 |
public 'dbh' => object(PDO)[2] |
Si on compare la sortie des deux sections de code précédentes on voit, dans la sortie du patron Singleton, que la resource ID de l'objet est la même pour tous les objets. Mais ce n'est pas le cas lorsque l'on utilise pas le patron.
Singleton comme anti-patron
Ce patron de conception est aussi appelé un anti-patron pour plusieurs raisons que je vais détailler ci-dessous :
- Ces une violation du SRP car il maîtrise sa propre création et son cycle de vie.
- Cela ajoute un état global à votre application. Je dirais que l'état global est mauvais car n'importe quel code peut changer sa valeur. Lors du débogage il devient difficile de trouver quelle partie du code à modifié l'état de la variable globale.
- Singleton est souvent un mauvais patron lorsque vous utilisez les tests unitaires et c'est une mauvaise idée que de ne pas faire de tests unitaires.
Conclusion
J'ai fait mon possible pour expliquer le patron de conception Singleton qui est largement abordé sur internet. J'espère que vous aurez trouvé cet article utile. Nous avons abordé les deux aspects de ce patron qui sont en tant que patron et en tant qu'anti-patron.
Posez vos questions, commentaires et/ou suggestions ci-dessous et je vous répondrais le plus rapidement possible. Vous pouvez aussi me contacter sur Twitter @XpertDevelopers ou par email.