Règles de travail avec les tableaux dynamiques et les classes de collection personnalisées



Règles de travail avec les tableaux dynamiques et les classes de collection personnalisées
Voici les règles auxquelles j'adhère lorsque je travaille avec des tableaux dynamiques. En fait, c'est un guide pour concevoir des tableaux, mais je ne voulais pas le mettre dans un guide pour concevoir des objets, car tous les langages orientés objet n'ont pas de tableaux dynamiques. Les exemples sont écrits en PHP car il est similaire à Java (que vous connaissez peut-être déjà), mais avec des tableaux dynamiques au lieu des classes de collection et des interfaces intégrées.

Utilisation de tableaux comme liste


Tous les articles doivent être du même type.


Si vous utilisez un tableau comme liste (une collection de valeurs dans un certain ordre), toutes les valeurs doivent être du même type:

$goodList = [ 'a', 'b' ]; $badList = [ 'a', 1 ]; 

Le style d'annotation de type de liste courant est: @var array<TypeOfElment> . Assurez-vous de ne pas ajouter de type d'index, il doit toujours être int .

Besoin d'ignorer l'index de chaque élément


PHP créera automatiquement un nouvel index pour chaque élément de la liste (0, 1, 2, etc.). Cependant, vous ne devez ni compter sur ces index, ni les utiliser directement. Les clients ne peuvent compter que sur countable iterable et countable .

Vous pouvez donc utiliser librement foreach et count() , mais ne pas utiliser for pour parcourir les éléments de la liste:

 // Good loop: foreach ($list as $element) { } // Bad loop (exposes the index of each element): foreach ($list as $index => $element) { } // Also bad loop (the index of each element should not be used): for ($i = 0; $i < count($list); $i++) { } 

En PHP, la boucle for peut ne pas fonctionner du tout s'il n'y a pas d'index dans la liste, ou s'il y a plus d'index que le nombre d'éléments.

Utilisez un filtre au lieu de supprimer des éléments


Vous pouvez supprimer des éléments par index ( unset() ), mais au lieu de les supprimer, il est préférable de créer une nouvelle liste sans éléments indésirables à l'aide de array_filter() .

Encore une fois, il ne faut pas se fier aux indices des éléments. Ainsi, lorsque vous utilisez array_filter() n'utilisez pas le paramètre flag pour filtrer les éléments par index, ni même par élément et index.

 // Good filter: array_filter( $list, function (string $element): bool { return strlen($element) > 2; } ); // Bad filter (uses the index to filter elements as well) array_filter( $list, function (int $index): bool { return $index > 3; }, ARRAY_FILTER_USE_KEY ); // Bad filter (uses both the index and the element to filter elements) array_filter( $list, function (string $element, int $index): bool { return $index > 3 || $element === 'Include'; }, ARRAY_FILTER_USE_BOTH ); 

Utilisation de tableaux en tant que tableaux associatifs


Si les clés sont pertinentes et non des index (0, 1, 2, etc.), utilisez librement des tableaux associatifs (une collection à partir de laquelle vous pouvez extraire des valeurs par leurs clés uniques).

Toutes les clés doivent être du même type.


Première règle d'utilisation des tableaux associatifs: toutes les clés doivent être du même type (le plus souvent c'est une string ).

 $goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // Bad (uses different types of keys) $badMap = [ 'foo' => 'bar', 1 => 'baz' ]; 

Toutes les valeurs doivent être du même type.


Il en va de même pour les valeurs: elles doivent être du même type.

 $goodMap = [ 'foo' => 'bar', 'bar' => 'baz' ]; // Bad (uses different types of values) $badMap = [ 'foo' => 'bar', 'bar' => 1 ]; 

Un style commun pour annoter un type est: @var array<TypeOfKy, TypeOfValue> .

Les tableaux associatifs doivent rester privés


Les listes, en raison de la simplicité de leurs caractéristiques, peuvent être transférées en toute sécurité d'un objet à l'autre. Tout client peut parcourir les éléments ou les compter, même si la liste est vide. La carte est plus difficile à utiliser, car les clients peuvent compter sur des clés qui ne correspondent à aucune valeur. Cela signifie que les tableaux associatifs doivent généralement rester privés par rapport aux objets qui les gèrent. Au lieu de laisser les clients accéder directement aux mappes internes, laissez les getters (et éventuellement les setters) récupérer les valeurs. Lancez des exceptions s'il n'y a pas de valeur pour la clé demandée. Cependant, si vous pouvez garder la carte et ses valeurs complètement privées, faites-le.

 // Exposing a list is fine /** * @return array<User> */ public function allUsers(): array { // ... } // Exposing a map may be troublesome /** * @return array<string, User> */ public function usersById(): array { // ... } // Instead, offer a method to retrieve a value by its key /** * @throws UserNotFound */ public function userById(string $id): User { // ... } 

Utiliser des objets comme des tableaux associatifs avec des valeurs de plusieurs types


Si vous souhaitez utiliser un tableau associatif, mais y stocker des valeurs de différents types, utilisez des objets. Définissez une classe, ajoutez des propriétés de type public ou ajoutez un constructeur et des getters. Ces objets incluent des objets de configuration ou de commande:

 final class SillyRegisterUserCommand { public string $username; public string $plainTextPassword; public bool $wantsToReceiveSpam; public int $answerToIAmNotARobotQuestion; } 

Exceptions à la règle


Les bibliothèques et les frameworks nécessitent parfois l'utilisation de tableaux de manière plus dynamique. Il est alors impossible (et indésirable) de suivre les règles précédentes. Les exemples incluent le tableau de données stocké dans une table de base de données et la configuration de formulaire dans Symfony.

Classes de collecte personnalisées


Les classes de collection personnalisées peuvent être un excellent outil pour travailler avec Iterator , ArrayAccess et d'autres entités, mais je trouve que le code devient souvent déroutant. Quiconque regarde du code pour la première fois devra consulter le manuel PHP, même s'il est un développeur expérimenté. De plus, vous devrez écrire plus de code à maintenir (test, débogage, etc.). Donc, dans la plupart des cas, un simple tableau avec les annotations de type correct suffit.

Qu'est-ce qui indique que vous devez encapsuler le tableau dans un objet de collection personnalisé?

  • Duplication de la logique liée à un tableau.
  • Les clients doivent travailler avec trop de détails sur le contenu du tableau.

Utilisez une classe de collection personnalisée pour éviter la logique en double.


Si plusieurs clients travaillant avec le même tableau effectuent la même tâche (par exemple, filtrer, comparer, réduire, compter), les doublons peuvent être supprimés à l'aide de la classe de collecte personnalisée. Le transfert de la logique en double vers une méthode de classe de collection permet à n'importe quel client d'effectuer la même tâche simplement en appelant la méthode de collection:

 $names = [/* ... */]; // Found in several places: $shortNames = array_filter( $names, function (string $element): bool { return strlen($element) < 5; } ); // Turned into a custom collection class: use Assert\Assert; final class Names { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function shortNames(): self { return new self( array_filter( $this->names, function (string $element): bool { return strlen($element) < 5; } ) ); } } $names = new Names([/* ... */]); $shortNames = $names->shortNames(); 

L'avantage de transformer une collection à l'aide d'une méthode est que cette transformation est nommée. Vous pouvez ajouter un nom court et informatif pour appeler array_filter() , qui autrement serait assez difficile à trouver.

Dissociez les clients avec une classe de collecte personnalisée


Si un client parcourt un tableau, prend une partie des données des éléments sélectionnés et fait quelque chose avec eux, ce client est étroitement lié à tous les types correspondants: tableau, éléments, valeurs récupérées, méthode de sélection, etc. qu'en raison d'une telle liaison profonde, il sera beaucoup plus difficile pour vous de modifier quoi que ce soit lié à ces types sans casser le client. Dans ce cas, vous pouvez également envelopper le tableau dans une classe de collection personnalisée et donner la bonne réponse, en effectuant les calculs nécessaires à l'intérieur et en desserrant la liaison du client à la collection.

 $lines = []; $sum = 0; foreach ($lines as $line) { if ($line->isComment()) { continue; } $sum += $line->quantity(); } // Turned into a custom collection class: final class Lines { public function totalQuantity(): int { $sum = 0; foreach ($lines as $line) { if ($line->isComment()) { continue; } $sum += $line->quantity(); } return $sum; } } 

Quelques règles pour les classes de collection personnalisées


Rendez-les immuables


Lors de l'exécution de telles transformations, les références existantes à l'instance de collection ne doivent pas être affectées. Par conséquent, toute méthode qui effectue cette conversion doit renvoyer une nouvelle instance de la classe, comme nous l'avons vu dans l'exemple précédent:

 final class Names { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function shortNames(): self { return new self( /* ... */ ); } } 

Bien sûr, si vous convertissez un tableau interne, vous pouvez convertir un autre type de collection ou un tableau simple. Comme d'habitude, assurez-vous que le type correct est renvoyé.

Fournissez uniquement le comportement dont les clients ont vraiment besoin


Au lieu d'étendre une classe à partir d'une bibliothèque avec une collection universelle, ou d'implémenter un filtre ou une carte universel, ainsi que de réduire pour chaque classe de collection personnalisée, implémentez uniquement ce dont vous avez vraiment besoin. Si à un moment donné vous arrêtez d'utiliser la méthode, supprimez-la.

Utilisez IteratorAggregate et ArrayIterator pour itérer


Si vous travaillez avec PHP, au lieu d'implémenter toutes les méthodes de l'interface Iterator (enregistrement de pointeurs internes, etc.), implémentez uniquement l'interface IteratorAggregate et laissez-la retourner une instance ArrayIterator basée sur le tableau interne:

 final class Names implements IteratorAggregate { /** * @var array<string> */ private array $names; public function __construct(array $names) { Assert::that()->allIsString($names); $this->names = $names; } public function getIterator(): Iterator { return new ArrayIterator($this->names); } } $names = new Names([/* ... */]); foreach ($names as $name) { // ... } 

Compromis


Puisque vous écrivez plus de code pour une classe de collection personnalisée, il devrait être plus facile pour les clients de travailler avec cette collection (et pas seulement avec un tableau). Si le code client devient plus clair, si la collection fournit un comportement utile, cela justifie l'effort supplémentaire de maintenance d'une classe de collection personnalisée. Mais comme travailler avec des tableaux dynamiques est si facile (principalement parce que vous n'avez pas besoin de spécifier les types utilisés), j'utilise rarement mes classes de collection. Cependant, certains développeurs les utilisent activement, donc je continuerai certainement à rechercher des cas d'utilisation possibles.

Source: https://habr.com/ru/post/fr484336/


All Articles