Избегайте AOP: Array-Oriented Programming

Источник: «Avoid the AOP: Array-Oriented Programming»
Массивы — удобный способ организации и передачи данных в PHP-приложениях, но стоит ли это делать? В этой статье я расскажу о минусах злоупотребления использованием ассоциативных массивов и покажу простую альтернативу этому.

Что такое ассоциативные массивы

Ассоциативные Массивы, также известные как Словари, в PHP представляют собой тип массива, использующего пары ключ-значение для организации и хранения данных в структурированном формате. Эти массивы позволяют эффективно управлять данными с помощью ключей вместо обычных значений индексов, таких как 0,1,2 и т.д., обеспечивая более интуитивно понятные, читаемые человеком элементы.

Каждый ключ ассоциативного массива в PHP уникален и может ссылаться только на одно значение. Этим ключам можно присвоить любое имя, если они являются строкой или целым числом, что обеспечивает гибкий метод доступа к данным и манипулирования ими.

Например, в качестве ключей можно назначить имена сотрудников, а в качестве значений — связанную с ними информацию. Ассоциативные массивы значительно упрощают процесс работы с данными в PHP.

Преимущества использования ассоциативных массивов

Ассоциативные Массивы в PHP обладают рядом преимуществ. Во-первых, они обладают огромной гибкостью, поскольку позволяют использовать строки для указания индексов, а не ограничиваться только числовыми значениями. Это делает код более читабельным и доступным, поскольку разработчики могут использовать в качестве ключей осмысленные имена или описания.

Таким образом, они позволяют упростить поиск и управление данными, поскольку доступ к значениям осуществляется непосредственно через определённые ключи, что исключает необходимость итерации по элементам. Это обеспечивает более быстрый и эффективный поиск данных, особенно при работе с большими массивами данных.

Кроме того, PHP предоставляет множество встроенных функций, позволяющих эффективно работать с ассоциативными массивами, что ускоряет процесс сортировки, объединения, разделения и фильтрации массивов.

AOP — Array-Oriented Programming

Вы, вероятно, уже слышали об ООП (объектно-ориентированном программировании), но как насчёт AOP (Array-Oriented Programming)? Это не настоящая концепция программирования, но она довольно часто встречается во многих PHP-приложениях. Я называю AOP, когда в коде повсеместно используются ассоциативные массивы.

Рассмотрим пример, показывающий, что использование ассоциативных массивов может нанести вред вашей кодовой базе, когда она начинает расти или растёт команда, работающая над ней.

Если вы уже работали с каким-либо PHP-фреймворком, то наверняка знаете, что обычно данные из HTTP-запроса возвращаются в виде массива, когда нам нужно их проверить/использовать. В приведённом ниже примере показано, как мы можем получить данные из запроса и передать их в сервисный класс, который будет использовать данные запроса для получения списка товаров и их фильтрации с помощью Laravel.

// ProductController.php

final class ProductController extends Controller
{
public function __construct(private ProductService $service)
{
}

public function index(ProductsListRequest $request): JsonResponse
{
return response()->json($this->service->productsList($request->validated()));
}
}

// ProductService.php

final class ProductService
{
public function productsList(array $filters): Collection
{
$query = Product::query()
->select(['id', 'name', 'price']);

if (isset($filters['search'])) {
$query->where('name', 'LIKE', "%{$filters['search']}%");
}

if (isset($filters['min_price'])) {
$query->where('price', '>=', $filters['min_price']);
}

if (isset($filters['max_price'])) {
$query->where('price', '<=', $filters['max_price']);
}
}
}

На первый взгляд, в приведённом коде нет ничего плохого, и на самом деле это не так, если команда небольшая, работает по стандартному сценарию и кодовая база не огромна. Но даже в этом случае DX не самый лучший вариант, поскольку, если кому-то понадобится изменить логику метода productList, разработчику придётся также проверить класс ProductListRequest и убедиться, что он обновляется с учётом всех изменений, происходящих с фильтрами, передаваемыми UI.

Основная проблема здесь и во всех других местах, где мы используем Ассоциативные Массивы, одна и та же: у вас нет никакой информации о данных, которые хранит массив. Это может быть что угодно, и, что ещё хуже, каждый элемент массива может быть чем-то совершенно другим! У данных вообще нет контекста.

Представьте себе, что вы работаете над кодовой базой с более чем 100 конечными точками и с 10 разработчиками, использующими один и тот же подход, как показано выше. Это может стать очень сложной задачей. Я говорю так потому, что уже сталкивался с этим в одном из проектов, над которым работал. Поддержка старого кода и привлечение новых разработчиков может превратиться в кошмар.

Кроме этого примера, ещё одним удобным применением Ассоциативных Массивов является возврат метода, как в примере ниже:

public function getUserContactInfo(int $userId): array
{
$user = User::query()->firstOrFail($userId);

// ЛОГИКА ЗДЕСЬ

return [
'street' => 'Foo',
'number' => 1,
'zip' => '123456',
'phone' => '123456789',
];
}

Как в первом примере, сначала в приведённом методе нет ничего плохого, но представьте, что через год кто-то заходит в приведённый выше код и удаляет свойство phone. В зависимости от того, как это сделано, и от размера команды, другие разработчики могут подумать, что свойство phone все ещё существует, и вызвать проблемы или ошибки. Тесты могут это покрыть? Конечно, но это не идеальный вариант DX для вашей команды.

Другие недостатки

Ещё один момент, о котором следует помнить, и который является основным при работе с Ассоциативными Массивами, — это использование памяти. Они могут занимать достаточно много памяти, особенно при работе с большими массивами данных, поскольку помимо значений в них хранится дополнительная информация (ключи). Соответственно, это может негативно сказаться на производительности в приложениях с неоптимальным кодом.

Кроме того, массивы в PHP не являются строго типизированными, а значит, вы можете непреднамеренно хранить в одном массиве значения разных типов, что приведёт к потенциальным ошибкам или неэффективному использованию памяти.

Понимание кодовой базы

Для меня основной проблемой злоупотребления Ассоциативными Массивами является то, что это делает вашу кодовую базу запутанной. Это всегда требует дополнительной работы от того, кто работает с этой частью кода, чтобы понять, какой тип данных хранится в массиве.

Как же решить эту проблему? Очень просто: начать использовать Data Transfer Objects (DTOs). DTO могут представлять собой простой класс, используемый для отображения и типизации данных, как показано в примере ниже:

final readonly class ProductFilters(
public ?string $search = null,
public ?float $min_price = null,
public ?float $max_price = null,
) {}

С помощью этого DTO мы можем обновить показанный ранее код:

final class ProductService
{
public function productsList(ProductFilters $filters): Collection
{
$query = Product::query()
->select(['id', 'name', 'price']);

if (! blank($filters->search)) {
$query->where('name', 'LIKE', "%{$filters->search}%");
}

if (! blank($filters->min_price)) {
$query->where('price', '>=', $filters->min_price);
}

if (! blank($filters->max_price)) {
$query->where('price', '<=', $filters->max_price);
}
}
}

Сам код изменился незначительно, но контекст данных в этом коде стал совершенно другим. Теперь у вас есть объект ProductFilters, представляющий собой DTO, в который обёрнуты нужные вам фильтры и задан их контекст. Если кому-то понадобится что-то обновить в этом методе: исправить ошибку или расширить его новыми фильтрами, то это будет довольно простой и понятной задачей, поскольку у вас есть список всех возможных фильтров и вы точно знаете, какие данные хранит каждый фильтр.

То же самое мы можем сделать и для второго примера с другим очень простым DTO:

final readonly class UserContactInfo(
public string $street,
public int $number,
public ?string $zip,
public ?string $phone,
) {}

Затем мы можем обновить getUserContactInfo:

public function getUserContactInfo(int $userId): UserContactInfo
{
$user = User::query()->firstOrFail($userId);

// ЛОГИКА ЗДЕСЬ

return new UserContactInfo(
street: 'Foo',
number: 1,
zip: '123456',
phone: '123456789',
);
}

Опять, сам код практически тот же, но теперь данные обёрнуты в DTO, что придаёт им контекст и обеспечивает гораздо лучший DX для командной работы!

Заключение

Действительно, PHP является образцовым языком, известным своей универсальностью и надёжностью, позволяющей с относительной лёгкостью создавать сложные веб-приложения. Хотя полезность Ассоциативных Массивов нельзя игнорировать, поскольку они обеспечивают гибкий способ доступа к данным и манипулирования ими, необходимо осознанно использовать их, понимая их потенциальные недостатки, о которых я рассказал в статье.

Однако каждый инструмент в программировании имеет своё место и назначение. Ассоциативные Массивы не являются исключением, и их мощные возможности, безусловно, могут перевесить упомянутые недостатки в правильном контексте. В конце концов, выбор в пользу их использования во многом зависит от конкретных требований конкретного проекта, а также от тщательного анализа компромиссов. Разумное использование возможностей языка всегда будет играть ключевую роль в создании эффективных и мощных PHP-приложений.

Надеюсь, что Вам понравилась эта статья, и если да, то не забудьте поделиться этой статьёй со своими друзьями!!! До встречи!

Дополнительные ресурсы

Дополнительные материалы

Предыдущая Статья

Итерация файлов и каталогов в PHP

Следующая Статья

Композиция вместо Наследования в PHP