Page 1 sur 1

Singleton, association, et paramètres par défaut (sont sur u

Posté : 13 nov. 2008, 20:01
par Hywan
Hey :),

J'ai noté un comportement sympa de PHP, mais c'est tout à fait logique. Je voulais vous en faire part car je me suis cassé la tête un p'tit moment avant de trouver. Attention, c'est très spécifique mais ça m'arrive d'écrire ce genre de chose et je ne pense pas être le seul :).

Alors : on a 2 classes par exemple. La première classe A qui est un singleton. Donc on retrouve notre méthode statique getInstance, et notre constructeur est bien sûr privé. La seconde classe B est en association (agrégation et pas composition) avec A, c'est à dire que A peut utiliser B et inversement.

Voici ce que j'avais fait : on appelle A::getInstance, et A::getInstance va appeler A::__construct. Jusque là, rien de choquant. Dans A::__construct, j'appelais B::uneMethode, et dedans, j'appelais A::getInstance pour récupérer des paramètres sur A.

Grosso modo, on a le code suivant (j'ajoute des var_dump dans getInstance pour la suite du sujet) :
class A {
    
    private static $_instance = null;
    
    private function __construct ( $parameter = true ) {
        
        // …
        
        if(false === $parameter)
            B::uneMethode();
        // …
    }
    
    public static function getInstance ( $parameter = true ) {
        
        var_dump(null === self::$_instance);
        
        if(null === self::$_instance)
            self::$_instance = new self($parameter);
        
        var_dump(null === self::$_instance);
        
        return self::$_instance;
    }
    
    // …
}

class B {
    
    // je la mets statique pour aller plus vite, mais ça ne change rien.
    public static function uneMethode ( ) {
        
        // …
        A::getInstance();//->getParameters();
        // …
    }
}

A::getInstance(false);
On aura en sortie :

Code : Tout sélectionner

bool(true) bool(true) bool(false) bool(false)
Pourquoi ?

On demande l'instance de A. Donc l'instance est nulle au départ, donc true. On demande l'instance (new self($parameter);), donc on entre dans le constructeur. Ce dernier, va appeler B::uneMethode. Cette méthode va appeler A::getInstance(). On retombe donc sur le premier var_dump. Est-ce que l'instance existe ? Non toujours pas … enfin … en réalité si, elle existe, mais elle n'est pas assignée à la variable self::$_instance, donc le test retourne true. Donc on retourne encore une fois dans le constructeur (car new self est encore appelée), mais on n'appelle plus B::uneMethode car le paramètre par défaut est utilisé. (Notez ici que l'on pourrait plonger dans une récursivité assez méchante. Ce n'était pas mon cas, je constatais seulement le double appel de la méthode __construct). Donc on ne retourne pas dans B. On quitte le constructeur, on revient dans getInstance, on remonte, on assigne une première fois, on remonte, on assigne une seconde fois … et non ! Car la première assignation a été faite, donc on n'assigne pas la deuxième (à cause de notre test de nullité de l'instance). Donc on se retrouve avec une instance ayant les paramètres par défaut (si on enregistre les paramètres).

Mais alors comment lancer l'appel à B ? Il suffit de déplacer l'appel. On le supprime du constructeur et on le place dans A::getInstance de cette façon :
    public static function getInstance ( $parameter = true ) {

        var_dump(null === self::$_instance);

        if(null === self::$_instance) {

            self::$_instance = new self($parameter);

            if(false === $parameter)
                B::uneMethode();
        }

        var_dump(null === self::$_instance);

        return self::$_instance;
    }
C'est un cas particulier que j'ai simplifié, mais ça peut très facilement arriver. Soit on n'a pas de paramètre et on boucle (récursivité sur le new), soit on se retrouve avec une instance paramétrée par défaut.

Ça fait intervenir plusieurs phénomènes, mais c'est principalement une question de statisme et d'assignation. PHP va d'abord évaluer new self($parameter); avant de l'assigner à self::$_instance ce qui est très logique et normal. Je ne fais aucun reproche à PHP, qu'on soit d'accord.

Moralité, attention quand vous coder des choses un peu particulière a bien tester, mais avant tout : bien réfléchir au comportement du langage, à sa façon d'interpréter les choses, de comprendre les choses, et l'ordre d'évaluation (ce dont il est question ici).

J'ai mis du temps à trouver car je travaillais sur 3 fichiers différents … Donc penser à bien atomiser vos tests si c'est possible (dans mon cas — pas celui de l'exemple —, ça n'était pas possible par exemple).

Ah oui, pourquoi ne pas passer les paramètres à B::uneMethode en argument, et comme ça, B::uneMethode n'appelle pas A::getInstance ? C'est une solution qui évite tout ces problèmes, mais elle ne me convenait pas on peut utiliser directement B::uneMethode sans passer par A. Et donc, dans ce cas, on doit utiliser les paramètres par défaut. Voilà le pourquoi du comment :).

Fin de la séance mal de crâne ;-).

Posté : 13 nov. 2008, 21:47
par stopher
Sympa la séance ...
vite des aspirines ... et oui à 20:01 , apres une dure journée devant un écran à s'arracher les cheveux , le soir la vue n'est plus vraiment en face des trous ... :D

Intéressant , mais que veux tu dire par
atomiser vos tests si c'est possible
Apres , c'est vrai que c'est un cas particulié que tu présentes ... mais c'est toujours intéressant de connaître des cas de ce style , pour le jour ou l'on tombe sur un bug bien caché que provoquer des situations comme celle ci ...

Merci pour cette séance ... :-p

Posté : 14 nov. 2008, 00:27
par Hywan
Atomiser les tests signifient qu'il faut les rendre unitaires. On teste une fonction, selon un contexte. Ensuite, on la change de contexte plusieurs fois. Et enfin, on l'incorpore dans notre classe. Je te propose de regarder du côté des tests unitaires pour plus d'informations :).

En effet, je présente un cas particulier, mais il a un double sens. Le premier est de montrer un bug facilement faisable (on a vite fait d'écrire une chose du genre sans savoir comment la résoudre). La deuxième (et surtout la deuxième) est de montrer qu'il faut toujours réfléchir sur des notions d'ordre (ou de temps … hmm ?). Il y a un ordre d'exécution à ne pas oublier.

On prend vraiment conscience de l'ordre d'exécution et d'interprétation des programmes quand on fait de l'algorithmie par exemple, ou dans le cas énoncé ici. Déplacer un appel de fonction deux lignes plus bas, et les performances changent totalement. Un autre exemple que j'étudie en ce moment : récursivité simple ou terminale, les différences sont flagrantes, et pour pas grand chose souvent :).

Posté : 14 nov. 2008, 02:04
par AB
... En effet, je présente un cas particulier, mais il a un double sens. Le premier est de montrer un bug facilement faisable (on a vite fait d'écrire une chose du genre sans savoir comment la résoudre). La deuxième (et surtout la deuxième) est de montrer qu'il faut toujours réfléchir sur des notions d'ordre (ou de temps … hmm ?). Il y a un ordre d'exécution à ne pas oublier.

Déplacer un appel de fonction deux lignes plus bas, et les performances changent totalement.
Heu c'est évident mais ce que tu dis est du domaine général et n'a pas pas besoin d'un cas particulier pour être démontré. Et c'est vérifiable bien au delà du domaine informatique. Je peux toujours m'acharner sur le démarreur de ma voiture si le système d'allumage est défaillant :wink:

Cela dit, il est vrai qu'en manipulant des méthodes statiques, ce problème peut-être beaucoup plus délicat à détecter :)