DX : setter, wither and mutants
Tags: php, programming, architecture, dx
Published: 2023-04-04 13:20:01

(English version: https://dev.to/fredbouchery/dx-setter-wither-and-mutants-29e6)

Quand je conçois un code, je passe beaucoup de temps à réfléchir à sa DX (Developer eXperience), c'est d'ailleurs pour cela que j'apprécie les "test before", car cela me permet de réfléchir à la façon d'utiliser le code avant de l'implémenter.

Dernièrement, je concevais une classe de configuration pour une couche d'accès à une BDD et voici la façon dont j'avais initialement imaginé son usage : 

```PHP
<?php
$conf = (new ConnectionConfig())
    ->connection('main')
        ->driver(DriverInterface::DRIVER_MYSQL)
        ->primary()
            ->host('127.0.0.1', 3306)
            ->credentials('root', 'root')
        ->replica()
            ->host('127.0.0.1', 3307)
            ->credentials('readonly', 'readonly')
;
```

Au niveau implémentation, c'est une interface chaînée ([fluent interface](https://en.wikipedia.org/wiki/Fluent_interface)) assez simple : 

- `connection(string $name)` vient définir une propriété "`$currentConnectionName`"
- `primary()` ou `replica()` viennent définir une propriété `$currentReplicationServer`

Ensuite, quand on appelle `host()` ou `credentials()`, j'utilise les propriétés "`$currentXxxx`" pour stocker l'information.

Puis, j'ai voulu définir comment j'allais lire ces informations, et j'ai commencé à écrire ça : 
```PHP
<?php
[
    'login' => $login,
    'password' => $password
] = $conf
    ->connection('main')
    ->primary()
    ->getCredentials()
;
```

Premier constat : Pffff ... j'ai eu la flemme de définir un objet "ServerCredentials" pour y stocker le login et le pass, alors qu'un tableau associatif faisait bien l'affaire. Ne me regardez pas avec cette expression sévère, on l'a tous fait ... les jours de flemme, et comme le dirait [Krän](https://fr.wikipedia.org/wiki/Kr%C3%A4n) : "Y'a des jours, faut pas m'chercher ! Et y'a des jours tous les jours !". Mais c'est pas le débat aujourd'hui, voyons le problème suivant que je voudrais mettre en valeur.

Pour lire l'identité, j'ai utilisé un getter `getCredentials()` alors que pour la définir, j'ai utilisé la simple méthode `credentials($login, $pass)`, car je trouvais ça plus *cool* pour ma DX.

Simplement, quand on se place du point de vue de la personne qui va récupérer la définition, déjà, ce n'est pas habituel d'avoir un getter et une méthode d'assignation qui ne commence pas par "set", et en plus, cela peut être confusant quand l'IDE va autocompléter et proposer ces deux méthodes.

Bon, certes, il n'y a pas trop d'ambiguïté, car une méthode avec deux paramètres `$login` et `$pass`, il est probable qu'elle permette de les assigner. Mais, j'ai quand même préféré changer ça avec des setters.

Oui, car quand on conçoit une bonne DX, il faut éviter de trop changer les habitudes des développeuses et des développeurs ! Même si votre idée semble révolutionnaire, innovante, jamais faite auparavant, cela peut desservir votre DX drastiquement.

Me voilà donc à changer la définition : 
```PHP
<?php
$conf = (new ConnectionConfig())
    ->connection('main')
        ->setDriver(DriverInterface::DRIVER_MYSQL)
        ->primary()
            ->setHost('127.0.0.1', 3306)
            ->setCredentials('root', 'root')
        ->replica()
            ->setHost('127.0.0.1', 3307)
            ->setCredentials('readonly', 'readonly')
;
```

Et là, une nouvelle difficulté s'est présenté à moi : les mutants !

Regardez ce code:
```PHP
<?php
$serverIp = '127.0.0.1';
$login = 'root';
$password = 'root';

$conf = new ConnectionConfig();

$primary = $conf->connection('main')->primary();
$replica = $conf->connection('main')->replica();

// Define hosts
$primary->setHost($serverIp, 3306);
$replica->setHost($serverIp, 3307);

// Define credentials
$primary->setCredentials($login, $password);
$replica->setCredentials($login, $password);
```

Oui, on imagine une façon d'utiliser un code et la personne va le faire d'une toute autre manière, c'est son droit.

Seulement, dans le cas présent, il y a un bug assez gênant : le serveur primaire n'est jamais défini. Si vous ne voyez pas pourquoi, je vous laisse essayer de comprendre avant de lire l'explication qui va suivre.
.
.
.
.
.
.
.
.
.
Comme je l'ai indiqué au début, quand on appelle `primary()` ou `replica()`, on vient définir une propriété `$currentReplicationServer`, et comme j'utilise une interface "fluent", les variables `$primary` et `$replica` contiennent toutes les deux la même instance d'objet. Lors de l'appel à `->replica()` la propriété `$currentReplicationServer` va donc être modifiée pour indiquer que l'on configure le réplica, ce qui concerne également l'instance dans la variable `$primary` (je vous laisse relire si vous n'avez pas bien compris).

On tombe dans un grand classique en PHP (et d'autres langages) : la mutabilité des objets.

Pour résoudre cela, on utilise l'auto-clonage, on change la propriété et on retourne la nouvelle instance. On passe donc de : 
```PHP
<?php
public function primary(): self
{
    $this->currentReplicationConfiguration = 'primary';

    return $this;
}
```
À :
```PHP
<?php
public function primary(): self
{
    $self = clone $this;

    $self->currentReplicationConfiguration = 'primary';

    return $self;
}
```

Et on va appliquer cette technique sur l'ensemble des méthodes, sauf que ... ça ne fonctionne pas. Car dans le cas : 
```PHP
<?php
$conf = new ConnectionConfig();

$primary = $conf->connection('main')->primary();
$replica = $conf->connection('main')->replica();
```
Ce que je souhaite, c'est que $conf contienne tous les éléments de configuration, pour la connexion "main", avec le serveur primaire et le réplica, et au final, on a 3 instances de la classe de configuration avec des valeurs dissociées.

Et c'est bien tout le problème du design "*fluent*" couplé à un changement d'état (stateful vs stateless) : Si le comportement d'une méthode dépend de l'appel d'une autre, vous êtes dans le pétrin !

Il y a une possibilité pour résoudre partiellement ça, c'est en faisant comprendre au développeur, ou à la développeuse, que la méthode retourne une nouvelle instance : on va remplacer les "setters" par des "withers" :
```PHP
<?php
$conf = new ConnectionConfig();
$primary = $conf->withConnection('main')->withPrimary();
```
En utilisant le préfixe "`with`" cela indique que le retour sera une nouvelle instance de l'objet, donc `$conf` n'est pas la même instance que `$primary`. Le développeur ou la développeuse va donc devoir chaîner ses appels, sinon le résultat final ne contiendra pas ce qu'il souhaite.

En réalité, on n'a pas résolu la question par du code supplémentaire, mais juste par un simple renommage, car, oui, la conception d'un code, et d'une bonne DX, passe aussi par son nommage.

Le code final serait : 
```PHP
<?php
$conf = (new ConnectionConfig())
    ->withConnection('main')
        ->setDriver(DriverInterface::DRIVER_MYSQL)
        ->withPrimary()
            ->setHost('127.0.0.1', 3306)
            ->setCredentials('root', 'root')
        ->withReplica()
            ->setHost('127.0.0.1', 3307)
            ->setCredentials('readonly', 'readonly')
;
```

Et, si on revient à l'accès, cela donne : 
```PHP
<?php
[
    'login' => $login,
    'password' => $password
] = $conf
    ->withConnection('main')
    ->withPrimary()
    ->getCredentials()
;
```

Franchement, je trouve pas ça dingue comme DX, car dans mon petit cerveau, j'ai tendance à associer les "withers" à de la construction et non de l'accès.

C'est la quadrature du cercle, vous ne trouvez pas ?

En fait non, c'est tout simplement parce que j'ai fait fausse route depuis le début, et cela a commencé quand j'ai écrit que j'avais eu la flemme !

1. Ma classe aurait dû s'appeler "`ConnectionsConfiguration`" avec un "s" et un mot complet (Arrêtez les raccourcis, c'est pas 7 caractères de plus qui vont impacter votre productivité)
2. Il ne devrait y avoir qu'une seule méthode "`connection(string $name)`" qui retourne un objet "`ConnectionConfiguration`" (sans le "s" donc)
3. `ConnectionConfiguration` dispose de trois méthodes `setDriver()`, `primary()` et `replica()`, dont les deux dernières retournent des instances **mutables** de `ServerConfiguration`.
4. `ServerConfiguration` contient quatres méthodes `setHost()`, `setCredentials()`, `getHost()` et `getCredentials()` gérant des objets `ServerHost` et `ServerCredentials`

Au final, le code sera : 
```PHP
<?php
$conf = (new ConnectionsConfiguration())
    ->connection('main')
        ->setDriver(DriverInterface::DRIVER_MYSQL)
        ->primary()
            ->setHost('127.0.0.1', 3306)
            ->setCredentials('root', 'root')
        ->replica()
            ->setHost('127.0.0.1', 3307)
            ->setCredentials('readonly', 'readonly')
;
```
Sauf qu'avec ce code, je n'ai plus une interface fluent et ... j'ai une erreur fatale !
Si vous regardez attentivement, après le `setCredentials()`, j'appelle `replica()`, mais cette méthode n'existe pas, car `setCredentials()` ne retourne pas un `ConnectionConfiguration` mais un `ServerCredentials`. De même, la variable "`$conf`" ne contient pas un objet `ConnectionsConfiguration`. Bref, sans interface fluent, il faudrait écrire comme ceci :
```PHP
<?php
$conf = new ConnectionsConfiguration();
$conf->connection('main')
        ->setDriver(DriverInterface::DRIVER_MYSQL)
        ->primary()
            ->setHost('127.0.0.1', 3306)
            ->setCredentials('root', 'root')
;
$conf->connection('main')
        ->replica()
            ->setHost('127.0.0.1', 3307)
            ->setCredentials('readonly', 'readonly')
;
```
C'est tout de suite moins *joli* que les enchaînements d'appels d'une fluent, mais on aura beaucoup moins de risque de bug.

Pour l'accès à la configuration, cela donne :
```PHP
<?php
$credentials = $conf
   ->connection('main')
   ->primary()->
   ->getCredentials()
;
$login = $credentials->login;
$password = $credentials->password;
```

Les plus attentifs auront noté que mes classes sont mutables, et pour certains, c'est une hérésie, un scandale, une honte qui devrait me conduire au bûcher ! À ceux là, je répondrais :
Oui, mais cela générerait beaucoup plus de problèmes qu'il n'en résout et casserait ma DX. Alors, je vous renvois au mantra de Krän : "Y'a des jours, faut pas m'chercher ! Et y'a des jours tous les jours  !".

En résumé :
- Placez-vous du point de vue de l'utilisation et non de l'implémentation
- N'innovez pas trop, les gens ont des habitudes
- Ne soyez jamais flemmards
- Utilisez l'immutabilité avec parcimonie
- Attention aux interfaces fluents sur les objets stateful
- Lisez Krän, c'est une BD déjantée à souhait