[RESOLU] Compréhension singleton

ViPHP
ViPHP | 2577 Messages

16 oct. 2012, 09:52

Bonjour,

J'ai un problème de compréhension sur les singleton. J'ai compris l'intérêt d'un singleton par rapport à une variable global au niveau de la protection de cette variable.

Par contre, j'ai du mal à comprendre pourquoi ne pas utiliser un objet static sans l'instancier. Le fait de travailler en static me semble suffisant pour être sur de ne pas multiplier cet objet.

ViPHP
xTG
ViPHP | 7331 Messages

16 oct. 2012, 11:35

Les deux se valent, c'est juste qu'on préfère avoir un objet à manipuler qu'un appel statique.
Bref c'est question de goût et de mode. ^^

De plus avec une class static tu ne pourras pas faire de multiton, d'où l'utilisation d'un objet instanciable pour le singleton pour garder en cohérence d'évolutivité.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 13231 Messages

16 oct. 2012, 12:14

Attention : singleton != variable globale

Un singleton permet de s'assurer qu'on n'instancie pas plusieurs fois un objet, pas qu'il est accessible de partout.
Par exemple, pour les classes de connexion au base de données, s'assurer qu'on ne la démarre pas à chaque fois qu'un objet en a besoin.

Commencer à assimiler ce singleton comme un variable globale (car il est vrai que c'est pratique a appeler), c'est juste ouvrir la porte à un code spaghetti avec pleins de dépendances.

Petit exemple pratique :
// Singleton de connexion à la base de données
class Database
{
    protected static $instance;

    protected function __construct() {}

    protected function __clone() {}

    public function getInstance()
    {
        if( is_null(static::$instance) )
        {
            static::$instance = new static();
        }

        return static::$instance;
    }
}
Pour moi, un singleton est mal utilisé quand je vois ce code :
class Foobar
{
    public function doStuff()
    {
        Database::getInstance()->query($query);
    }
}
Ici, le singleton n'est utiliser que pour accéder facilement à la connexion, et pose de gros soucis de couplage et de maintenance.

La bonne manière de l'utiliser est celle là :
class Foobar
{
    protected $conn;

    public function __construct(Database $conn)
    {
        $this->conn;
    }

    public function doStuff()
    {
        $query = 'Something interessting';
        $this->conn->query($query);
    }
}

$foobar = new Foobar(Database::getInstance());
Dans cet exemple, la classe Foobar gère sa connexion sans savoir d'où elle vient.
Et le singleton ne sert ici qu'à s'assurer que j'ai une seule connexion à la base de données d'ouverte.
Connaître son ignorance est la meilleure part de la connaissance
Pour un code lisible : n'hésitez pas à sauter des lignes et indenter

twitter - site perso - Github - Zend Certified Engineer

ViPHP
ViPHP | 2577 Messages

16 oct. 2012, 13:39

Lorsque j'utilisais une variable globale pour le connexion à la base de données, je l'initialisais systématiquement car j'en faisais toujours usage. L'utilisation de cette variable ne m'a jamais posé de problème dans la mesure ou je ne l'affectais jamais par la suite.
Maintenant, je suis passé à une classe static avec une variable privé pour la connexion et un fonctionnement qui doit s'apparenté à un adaptateur d'après ce que j'ai commencé à voir. Le fait de mélanger l'aspect singleton et l'aspect adaptateur n'est pas forcément une bonne chose. Je suis en train de creuser.

Après réflexion, l'utilité de l'objet instancié sur l'objet static est peut être le coté "variable" que l'on peut passer en paramètre (comme l'exemple de Zeus). J'ai encore du mal sur l'aspect "Database::getInstance()->query($query);" qui reste mon mode de pensée naturel avec plutôt "Database::query($query);".

La connexion est la première idée qui vient à l'esprit, mais dans le cas de "l'utilisateur connecté", c'est le même objet que l'utilisateur que l'on gère et on se retrouve surement avec un objet à instancier via un singleton.

Petit nouveau ! | 3 Messages

10 janv. 2013, 01:02

Bonsoir
Les deux se valent, c'est juste qu'on préfère avoir un objet à manipuler qu'un appel statique.
Bref c'est question de goût et de mode. ^^

De plus avec une class static tu ne pourras pas faire de multiton, d'où l'utilisation d'un objet instanciable pour le singleton pour garder en cohérence d'évolutivité.
tout à fait d'accord, le singleton n'est qu'une manière propre et standardisé d'écrire qu'une Class ne sera instanciée qu'une seul fois, tout dépend de l’envergure de ton projet mais pour un projet à long terme je te conseil d'utiliser le singleton afin de faciliter la compréhension du code au futures informaticiens.

Et oui le singleton est souvent utilisé pour la connexion, une gestion de session utilisateur, une log, etc...

Finalement le même code pourrait être écrit de façon lineaire, simplement une question de présentation et de standard.

Eléphant du PHP | 120 Messages

10 janv. 2013, 02:10

Il faut éviter les singletons autant que possible, comme par exemple les connexions à des bases de données. On se dit d'abord : chouette, ça évite de se reconnecter par erreur. Jusqu'au moment où on a besoin de se connecter à une autre base de données. Alors on commence à gérer un singleton qui peut être instancié deux fois et ça finit par devenir crade.

L'injection de dépendance est une approche bien plus propre de la gestion des instances de Helper ou d'adaptateur de quelque chose, car on ne peut jamais savoir si on n'aura pas besoin d'utiliser une autre instance de cette classe.

Et les exemples que tu donnes, Guiloj, sont de très bons exemples pour une mauvaise utilisation de singletons. Les logs, les connexions diverses et même la gestion de sessions (si c'est fait proprement, j'entends) n'ont pas besoin d'être limités de cette façon.

ViPHP
ViPHP | 928 Messages

10 janv. 2013, 02:21

Un gros +1 pour Perine ;)

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 13231 Messages

10 janv. 2013, 11:22

Le problème du singleton, c'est qu'on l'utilise trop souvent en se disant "ah ouais, c'est cool, je peux l'appeler de partout, facilement".
Et là, c'est le soucis.

Il n'est pas mal d'utiliser un singleton, mais il faut le faire pour les bonnes raisons, à savoir empêcher plusieurs instanciations d'une même classe.
J'abonde dans le sens de Perine, à savoir que les exemples de logs et de session sont un très bon contre-exemple, car ce qui est recherché, c'est de pouvoir faire un Log::getInstance->addTrace() de partout, alors que le seul but recherché, c'est de ne pas s'embêter à l'injecter.

Par contre, pour la base de données, c'est un bon exemple, car je ne veux pas ouvrir X connexions à ma base de données, et si j'ai plusieurs bases de données, il existe le pattern Multiton.
Mais là aussi, le singleton ne doit servir qu'à l'initialisation de la connexion, mais pas à l'accès dans les classes.

L'injection de dépendance est là pour ça et en plus, ça découple les objets.

PS : voilà longtemps que je n'avais participé à une discussion aussi intéressante ici ;)
Connaître son ignorance est la meilleure part de la connaissance
Pour un code lisible : n'hésitez pas à sauter des lignes et indenter

twitter - site perso - Github - Zend Certified Engineer

ViPHP
ViPHP | 2577 Messages

28 janv. 2013, 10:16

J'ai un peu de mal à suivre.
Par exemple, pour Log::getInstance->addTrace(), je suis plus tenté d'utiliser directement quelque chose comme trace::add() en ayant un object qui ne fait que des traces dans les log. J'ai du mal à imaginer ce qui pourrait changer dans une tel fonction.
Pour ce qui est de l'injection de dépendance, ca reste mystérieux. Je récupère des objets pour utiliser des fonctions, tant que la fonction existe, il n'y a pas d'impact sur le code extérieur à mon object. Que j'utilise une fonction pour récupérer l'objet externe ou une fonction pour envoyer l'objet, j'ai du mal à voir ce que ca change. Pour moi dans un cas, mon objet fait des getinstance() et dans l'autre il faut que je fasse des set après chaque création.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 13231 Messages

28 janv. 2013, 10:32

Par exemple, pour Log::getInstance->addTrace(), je suis plus tenté d'utiliser directement quelque chose comme trace::add() en ayant un object qui ne fait que des traces dans les log. J'ai du mal à imaginer ce qui pourrait changer dans une tel fonction.
Cette phrase montre que tu as mal compris l'intérêt du singleton.
Tu le conçois comme une manière aisée d'utiliser une classe, alors que ce n'est pas ça.

Pour reprendre ton exemple, pourquoi utiliser un singleton est utilise, c'est qu'utiliser une seule instance de ton logger va te permettre d'ouvrir le fichier une seule fois, de poser un verrou dessus, puis d'écrire. Tu n'auras pas à ouvrir le fichier en lecture plusieurs fois au cours de ton exécution, contrairement à un trace::log()
Pour ce qui est de l'injection de dépendance, ca reste mystérieux. Je récupère des objets pour utiliser des fonctions, tant que la fonction existe, il n'y a pas d'impact sur le code extérieur à mon object. Que j'utilise une fonction pour récupérer l'objet externe ou une fonction pour envoyer l'objet, j'ai du mal à voir ce que ca change. Pour moi dans un cas, mon objet fait des getinstance() et dans l'autre il faut que je fasse des set après chaque création.
La différence tiens en la maintenabilité de ton code. S'il est gavé de déclaration de classe externe, ce code n'est pas indépendant de cette classe externe.

Exemple :
<?php

class Foo {
  protected $logger;

  public function __construct()
  {
    $this->logger = new Logger(); // Si je veux déplacer la classe Foo, il faut que j'emporte la classe Logger
  }
}
<?php

class Foo {
  protected $logger;

  public function __construct()
  {
    $this->logger = new Logger(); // Si je veux déplacer la classe Foo, il faut que j'emporte la classe Logger.
    // De plus, si je veux remplacer ma classe Logger par autre chose, ça représente beaucoup de travail
  }

  public function setLogger(InterfaceLogger $logger)
  {
    $this->logger = $logger; // Ici, je n'ai que l'interface à emporter, et si je compte changer ma classe de Log, à partir du moment où elle implémente l'interface, c'est ok
  }
}
Est-ce plus clair maintenant ?
Connaître son ignorance est la meilleure part de la connaissance
Pour un code lisible : n'hésitez pas à sauter des lignes et indenter

twitter - site perso - Github - Zend Certified Engineer

ViPHP
ViPHP | 2577 Messages

28 janv. 2013, 15:42

<?php

class Foo {
  protected $logger;

  public function __construct()
  {
    $this->logger = log::getinstance(); // Je suis coincer, je récupère un objet de type fixé par l'objet log. Ca veux dire que j'ai un objet log par projet ou un objet foo par projet (OK, je comprends le problème)     
  }

  public function setLogger(InterfaceLogger $logger)
  {
    $this->logger = $logger; // J'avoue ne utiliser utiliser les interfaces en php... et en même temps regretter que php ne soit pas plus typé.
  }
}
Je crois que j'ai compris le problème que résout cette solution. Je n'y suis pas confronter car je ne garde pas le source de mes projets. Enfin je garde des sources que j'adapte à chaque fois. Une fois le projet terminé, j'oublie tout et si je dois intervenir à nouveau (jamais ?), on me rend les sources

Merci pour les explications. Je vais essayer de digérer ca et de le mettre en oeuvre. Je clos le sujet singleton et je vais passer à la suite.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 13231 Messages

28 janv. 2013, 16:40

Tu noteras au passage que mon second exemple est faux puisque j'ai omis de retirer un truc.

Voici l'exemple "propre" :
<?php

class Foo {
  protected $logger;

  public function __construct()
  {
  }

  public function setLogger(InterfaceLogger $logger)
  {
    $this->logger = $logger; // Ici, je n'ai que l'interface à emporter, et si je compte changer ma classe de Log, à partir du moment où elle implémente l'interface, c'est ok
  }
}
Connaître son ignorance est la meilleure part de la connaissance
Pour un code lisible : n'hésitez pas à sauter des lignes et indenter

twitter - site perso - Github - Zend Certified Engineer