J'ai plusieurs objections; si tu y trouves des réponses (ou autres invalidation du problème), je suis preneur Au besoin, poste les réponses sous forme d'article sur le blog, et lie-les ici (répondre à ces objections sur le blog le renforcera)
1) Verbosité
Le code devient effectivement très verbeux, ce qui compense le gain de souplesse de East (okay, c'est souple, mais bien plus complexe à maintenir).
2) Obfuscation (c'est clairement le plus problématique à mon sens)
Beaucoup de retours ne sont non plus explicites (on n'a pas de "return"), mais implicite. Le code a l'air parallélisable, mais en réalité, il ne l'est pas car si "return" n'apparait pas, la logique du code se repose quand même dessus. Exemple: if ($this->canDrinkAlcohol === true) attends implicitement que le résultat de $this->country->canUserDrinkAlcohol($this); ait eu lieu. Idem dans canUserDrinkAlcohol.
3) Plus de méthodes publiques
Le code passe de deux méthodes publiques (void User::drinkAlcohol() et bool Country::getMajority()) à 7 méthodes publiques. Je pense que le risque de cassure de contrat ("Si country change son type de retour pour la méthode getMajority, mon code est cassé.") est alors bien plus important.
4) Lourdeur des classes
D'instinct, vu qu'il faut ajouter des méthodes quand on ajoute des conditions (void User:exAskedByCountry(Country)), le code ne sera pas d'une excellente extensibilité (il faudra les créer et altérer l'endroit où on les appelle; avant on altérait seulement la condition if); mais cela peut probablement se régler par des patterns type "liste de trucs à appeler dans canUserDrinkAlcohol").
5) Combinatoire
Au vu des noms de méthodes de User (sexAskedByCountry(Country), oldAskedByCountry(User)), on se prive de la combinatoire (le truc qui fait qu'avec 5 objets, on a 20 combinaisons différentes possibles) de l'orienté objet, ce qui va certainement entrainer beaucoup de réécriture de code: combien de méthodes faut-il ajouter si la possibilité de boire de l'alcool est également influencée par les fédérations (genre Country=France, Federation=Europe), par les maladies éventuelles du User, ou par la qualité des boissons de cette année?
Pour ma part, c'est plutôt la façon de voir les getters/setters qui pose problème en OO. Apparemment, pour certains (dont toi), c'est un moyen d'accéder, de façon sécurisée/contrôlée, aux propriétés internes de l'objet ("J'ai longtemps pensé qu'un getter c'était bien. Je me disais que c'était mieux que de laisser la propriété publique, on contrôle vraiment la donnée qui est accédée depuis l'extérieur."). Je trouve cette approche mauvaise: rien, dans le "contrat" publique d'une classe ne doit indiquer quelque chose de privé (d'interne) à la classe.
Considérer que les getters/setters sont des accès contrôlés aux propriétés privées fait fuiter l'existence de ces propriétés vers l'extérieur de la classe.
Je considère plutôt les getters comme des moyens d'interroger l'objet sur une donnée. L'objet répond par une donnée (ou par un autre objet), qui peut tout à fait venir des propriétés internes de la classe, d'une base de données, d'une propriété statique, ou d'une valeur aléatoire.
De même, je vois les setters comme un moyen d'informer l'objet sur un évènement précis, plutôt qu'un moyen de définir une de ses propriétés internes (ce qui, je répète, ferait fuiter la structure interne de la classe vers l'extérieur).
Du coup, dans le cas qui nous préoccupe, l'approche classique ne me dérange pas (au sens où getMajority() renvoie seulement l'âge de majorité, et non pas la valeur d'un private $majority). Toutefois, si l'extensibilité est importante, je coderai plutôt un Adapter:
Après, PHP n'a pas de typage pour les retours des fonctions (donc, c'est sûr, le typage peut changer sans prévenir). Trois Deux solutions à cela:
• Considérer un typage de retour dans la documentation, mais qui dans le code n'est pas explicite
• Considérer que le typage du retour est quelconque, et faire les casts nécessaires (un peu lourd, mais le plus proche du langage)
• Passer un conteneur en paramètre de la méthode, qui recevra le retour de celle-ci (une fausse-solution, puisque le getter est en fait déporté sur une autre classe)
1) Verbosité
Le code devient effectivement très verbeux, ce qui compense le gain de souplesse de East (okay, c'est souple, mais bien plus complexe à maintenir).
2) Obfuscation (c'est clairement le plus problématique à mon sens)
Beaucoup de retours ne sont non plus explicites (on n'a pas de "return"), mais implicite. Le code a l'air parallélisable, mais en réalité, il ne l'est pas car si "return" n'apparait pas, la logique du code se repose quand même dessus. Exemple: if ($this->canDrinkAlcohol === true) attends implicitement que le résultat de $this->country->canUserDrinkAlcohol($this); ait eu lieu. Idem dans canUserDrinkAlcohol.
3) Plus de méthodes publiques
Le code passe de deux méthodes publiques (void User::drinkAlcohol() et bool Country::getMajority()) à 7 méthodes publiques. Je pense que le risque de cassure de contrat ("Si country change son type de retour pour la méthode getMajority, mon code est cassé.") est alors bien plus important.
4) Lourdeur des classes
D'instinct, vu qu'il faut ajouter des méthodes quand on ajoute des conditions (void User:exAskedByCountry(Country)), le code ne sera pas d'une excellente extensibilité (il faudra les créer et altérer l'endroit où on les appelle; avant on altérait seulement la condition if); mais cela peut probablement se régler par des patterns type "liste de trucs à appeler dans canUserDrinkAlcohol").
5) Combinatoire
Au vu des noms de méthodes de User (sexAskedByCountry(Country), oldAskedByCountry(User)), on se prive de la combinatoire (le truc qui fait qu'avec 5 objets, on a 20 combinaisons différentes possibles) de l'orienté objet, ce qui va certainement entrainer beaucoup de réécriture de code: combien de méthodes faut-il ajouter si la possibilité de boire de l'alcool est également influencée par les fédérations (genre Country=France, Federation=Europe), par les maladies éventuelles du User, ou par la qualité des boissons de cette année?
Pour ma part, c'est plutôt la façon de voir les getters/setters qui pose problème en OO. Apparemment, pour certains (dont toi), c'est un moyen d'accéder, de façon sécurisée/contrôlée, aux propriétés internes de l'objet ("J'ai longtemps pensé qu'un getter c'était bien. Je me disais que c'était mieux que de laisser la propriété publique, on contrôle vraiment la donnée qui est accédée depuis l'extérieur."). Je trouve cette approche mauvaise: rien, dans le "contrat" publique d'une classe ne doit indiquer quelque chose de privé (d'interne) à la classe.
Considérer que les getters/setters sont des accès contrôlés aux propriétés privées fait fuiter l'existence de ces propriétés vers l'extérieur de la classe.
Je considère plutôt les getters comme des moyens d'interroger l'objet sur une donnée. L'objet répond par une donnée (ou par un autre objet), qui peut tout à fait venir des propriétés internes de la classe, d'une base de données, d'une propriété statique, ou d'une valeur aléatoire.
De même, je vois les setters comme un moyen d'informer l'objet sur un évènement précis, plutôt qu'un moyen de définir une de ses propriétés internes (ce qui, je répète, ferait fuiter la structure interne de la classe vers l'extérieur).
Du coup, dans le cas qui nous préoccupe, l'approche classique ne me dérange pas (au sens où getMajority() renvoie seulement l'âge de majorité, et non pas la valeur d'un private $majority). Toutefois, si l'extensibilité est importante, je coderai plutôt un Adapter:
class User
{
private $alcoholDrinkAllower;
/// or "setAlcoholDrinkAllower", but if "set" bothers you, name it another way
public function useAlcoholDrinkAllower(IAllower $allower) {
if (mt_rand(0, 100) < 50)
$this->alcoholDrinkAllower = $allower;
else
echo "I just don't want to use that allower.";
}
public function drinkAlcohol() {
if ($this->alcoholDrinkAllower === null)
echo "I drink a beer (without any acknowledgement)";
else if ($this->alcoholDrinkAllower->isAllowed())
echo "I drink a beer";
}
/// Same: you can call it "getAge()" if you want to
public function askAge() {
return $this->age;
}
}
class UserDrinkAllower implements IAllower {
private $user;
private $country;
private $beerQuality = 8;
public function isAllowed() {
return ($user->askAge() >= $country->askMajority() && $beerQuality > 5);
}
}
class Country
{
/// It's somehow the same as a getter, it's just a matter of naming
public function askMajority() {
return (int)file_get_contents("country.majority.txt");
}
}
Après, PHP n'a pas de typage pour les retours des fonctions (donc, c'est sûr, le typage peut changer sans prévenir). Trois Deux solutions à cela:
• Considérer un typage de retour dans la documentation, mais qui dans le code n'est pas explicite
/**
* @return int Age
*/
public function getAge() { ... }
• Considérer que le typage du retour est quelconque, et faire les casts nécessaires (un peu lourd, mais le plus proche du langage)
public function isAllowed() {
$age = (int)$user->getAge();
// Not sure what happens if return is "null" or not castable
}
...
public function getAge() { ... }
• Passer un conteneur en paramètre de la méthode, qui recevra le retour de celle-ci (une fausse-solution, puisque le getter est en fait déporté sur une autre classe)
public function isAllowed() {
// Forces us to create an Integer class (typehinting is only allowed for classes)
$age = new Integer(0);
$user->getAge($age);
}
...
public function getAge(Integer &$ageContainer) {
$ageContainer->setValue($this->age);
// We can call it "set" if spec says "Integer assures that getValue will return what setValue was"
}