Reglas para trabajar con matrices dinámicas y clases de colección personalizadas



Reglas para trabajar con matrices dinámicas y clases de colección personalizadas
Estas son las reglas a las que me adhiero cuando trabajo con matrices dinámicas. De hecho, esta es una guía para diseñar matrices, pero no quería ponerla en una guía para diseñar objetos, porque no todos los lenguajes orientados a objetos tienen matrices dinámicas. Los ejemplos están escritos en PHP porque es similar a Java (con el que ya puede estar familiarizado), pero con matrices dinámicas en lugar de las clases e interfaces de colección incorporadas.

Usar matrices como una lista


Todos los artículos deben ser del mismo tipo.


Si usa una matriz como una lista (una colección de valores en un cierto orden), entonces todos los valores deben ser del mismo tipo:

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

El estilo de anotación de tipo de lista común es: @var array<TypeOfElment> . Asegúrese de no agregar un tipo de índice, siempre debe ser int .

Es necesario ignorar el índice de cada elemento.


PHP creará automáticamente un nuevo índice para cada elemento de la lista (0, 1, 2, etc.). Sin embargo, no debe confiar en estos índices ni utilizarlos directamente. Los clientes solo pueden confiar en countable iterable y countable .

Por lo tanto, puede usar libremente foreach y count() , pero no use for recorrer los elementos de la lista:

 // 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, el bucle for puede no funcionar en absoluto si no hay índices en la lista, o si hay más índices que el número de elementos.

Use un filtro en lugar de eliminar elementos


Es posible que desee eliminar elementos por índice ( unset() ), pero en lugar de eliminarlo, es mejor crear una nueva lista sin elementos no deseados utilizando array_filter() .

Nuevamente, uno no debe confiar en los índices de elementos. Entonces, cuando use array_filter() no use el parámetro flag para filtrar elementos por índice, o incluso por elemento e índice.

 // 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 ); 

Usar matrices como matrices asociativas


Si las claves son relevantes y no son índices (0, 1, 2, etc.), utilice libremente matrices asociativas (una colección de la que puede extraer valores mediante sus claves únicas).

Todas las claves deben ser del mismo tipo.


La primera regla del uso de matrices asociativas: todas las claves deben ser del mismo tipo (la mayoría de las veces es una string ).

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

Todos los valores deben ser del mismo tipo.


Lo mismo se aplica a los valores: deben ser del mismo tipo.

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

Un estilo común para anotar un tipo es: @var array<TypeOfKy, TypeOfValue> .

Las matrices asociativas deben permanecer privadas


Las listas, debido a la simplicidad de sus características, se pueden transferir de forma segura de un objeto a otro. Cualquier cliente puede recorrer los elementos o contarlos, incluso si la lista está vacía. Es más difícil trabajar con el mapa, porque los clientes pueden confiar en claves que no coinciden con ningún valor. Esto significa que las matrices asociativas generalmente deben permanecer privadas con respecto a los objetos que las administran. En lugar de permitir que los clientes accedan directamente a mapas internos, permita que los captadores (y posiblemente los definidores) recuperen valores. Lanza excepciones si no hay valor para la clave solicitada. Sin embargo, si puede mantener el mapa y sus valores completamente privados, hágalo.

 // 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 { // ... } 

Utilice objetos como matrices asociativas con valores de varios tipos.


Si desea usar una matriz asociativa, pero almacenar valores de diferentes tipos en ella, use objetos. Defina una clase, agregue propiedades de tipo público o agregue un constructor y captadores. Dichos objetos incluyen objetos de configuración o comando:

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

Excepciones a la regla


Las bibliotecas y los marcos a veces requieren el uso de matrices de forma más dinámica. Entonces es imposible (e indeseable) seguir las reglas anteriores. Los ejemplos incluyen la matriz de datos que se almacena en una tabla de base de datos y la configuración del formulario en Symfony.

Clases de colección personalizadas


Las clases de colección personalizadas pueden ser una gran herramienta para trabajar con ArrayAccess y otras entidades, pero el código a menudo se vuelve confuso. Cualquiera que busque código por primera vez tendrá que consultar el manual de PHP, incluso si es un desarrollador experimentado. Además, tendrá que escribir más código para mantener (prueba, depuración, etc.). Entonces, en la mayoría de los casos, una simple matriz con las anotaciones de tipo correcta es suficiente.

¿Qué indica que necesita envolver la matriz en un objeto de colección personalizado?

  • Duplicación de lógica relacionada con una matriz.
  • Los clientes tienen que trabajar con demasiados detalles sobre el contenido de la matriz.

Use una clase de colección personalizada para evitar la lógica duplicada.


Si varios clientes que trabajan con la misma matriz realizan la misma tarea (por ejemplo, filtrar, comparar, reducir, contar), se pueden eliminar los duplicados utilizando la clase de colección personalizada. La transferencia de lógica duplicada a un método de clase de colección permite a cualquier cliente realizar la misma tarea simplemente llamando al método de colección:

 $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(); 

La ventaja de transformar una colección usando un método es que se nombra esta transformación. Puede agregar un nombre corto e informativo para llamar a array_filter() , que de lo contrario sería bastante difícil de encontrar.

Desvincula a los clientes con una clase de colección personalizada


Si un cliente realiza un ciclo a través de una matriz, toma parte de los datos de los elementos seleccionados y hace algo con ellos, este cliente se une estrechamente a todos los tipos correspondientes: matriz, elementos, valores recuperados, método selector, etc. que debido a un enlace tan profundo, será mucho más difícil para usted cambiar cualquier cosa relacionada con estos tipos sin romper el cliente. En este caso, también puede envolver la matriz en una clase de colección personalizada y dar la respuesta correcta, realizar los cálculos necesarios dentro y aflojar el enlace del cliente a la colección.

 $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; } } 

Algunas reglas para clases de colección personalizadas


Hazlos inmutables


Al realizar tales transformaciones, las referencias existentes a la instancia de colección no deberían verse afectadas. Por lo tanto, cualquier método que realice esta conversión debería devolver una nueva instancia de la clase, como vimos en el ejemplo anterior:

 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( /* ... */ ); } } 

Por supuesto, si convierte una matriz interna, puede convertirla a otro tipo de colección o una matriz simple. Como de costumbre, asegúrese de que se devuelva el tipo correcto.

Solo brinde el comportamiento que los clientes realmente necesitan


En lugar de expandir una clase desde una biblioteca con una colección universal, o implementar un filtro o mapa universal, así como reducir para cada clase de colección personalizada, implemente solo lo que realmente necesita. Si en algún momento deja de usar el método, bórrelo.

Use IteratorAggregate y ArrayIterator para iterar


Si está trabajando con PHP, en lugar de implementar todos los métodos de la interfaz Iterator (guardar punteros internos, etc.), implemente solo la interfaz ArrayIterator y deje que devuelva una instancia de ArrayIterator basada en la matriz interna:

 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) { // ... } 

Compromiso


Como está escribiendo más código para una clase de colección personalizada, debería ser más fácil para los clientes trabajar con esta colección (y no solo con una matriz). Si el código del cliente se vuelve más claro, si la colección proporciona un comportamiento útil, esto justifica el esfuerzo adicional de mantener una clase de colección personalizada. Pero dado que trabajar con matrices dinámicas es muy fácil (principalmente porque no necesita especificar los tipos utilizados), rara vez uso mis clases de colección. Sin embargo, algunos desarrolladores los usan activamente, por lo que definitivamente continuaré buscando posibles casos de uso.

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


All Articles