php arrays everywhere

Меня давно мучает мысль об одной проблеме — тотально злоупотребление массивами в PHP. Возможно корень проблемы в процедурном наследии PHP или в том, что PHP привлекает большое количество неопытных разработчиков, не знаю. Но дело в том, что очень многие используют массивы там, где должны использоваться объекты!

Взглянем на типичный пример ужасающего, на мой взгляд кода, злоупотребляющего массивами. Для примера, предположим, мы извлекаем данные из БД и работаем с ними посредством массива. Вот такие примеры я вижу ежедневно:

$ponds = array(
	array(
		"name" => "Breakspear",
		"size" => "large",
		"pegs" => "23",
		"amenities" => array(
			"toilets" => false,
			"shop"    => true,
		),
		"fishBreeds" => array(
			array(
				"name"    => "Bream",
				"stocked" => "2013-10-04 12:16:47",
				"number" => "100",
				"record"  => "5.4lbs",
			),
			array(
				"name"    => "Perch",
				"stocked" => "2012-02-02 05:23:32",
				"number" => "50",
				"record"  => "1.2lbs",
			),
			array(
				"name"    => "Common Carp",
				"stocked" => "2011-01-23 14:42:59",
				"number" => "10",
				"record"  => "15.4lbs",
			),
		),
	),
);

Мы видим огромный многомерный массив, хранящий информацию о конкретном рыболовном пруду. Здесь описан только один пруд, но представьте, что было бы, будь там описана сотня прудов? Что в итоге? Мы имеем набор данных, хранящийся в массиве, но не имеющий никакого связанного поведения. Когда нам нужно будет работать с этими данными, нам придется создавать сложный код, полный вложенных циклов. Например, как я могу получить общее количество рыб в пруду? Мне придется пройтись по всему массиву и сложить все количество рыб. Для малоопытного разработчика это не показалось бы чем-то плохим, так бы он и сделал, но мне больше по душе пришелся бы такой подход:

$ponds->getNamed("Breakspear")->getTotalStocked();

Намного меньше кода для получения того же результата. Конечно, полного перебора данных не избежать, но функциональность красиво инкапсулирована. Собственно, этот пример и раскрывает основную проблему — не стоит избегать преимуществ ООП. Примерно так должен выглядеть код из первого примера:

$ponds = new PondCollection();
 
$pond = new Pond("Breakspear");
$pond->addStockData(new StockData("Bream", 100, "2013-10-04 12:16:47"));
$pond->addStockData(new StockData("Perch", 50, "2012-02-02 05:23:32"));
$pond->addStockData(new StockData("Common Carp", 10, "2011-01-23 14:42:59"));
$pond->addAmenity(new ShopAmenity());
 
$ponds->add($pond);

Излишняя сложность


Использование огромных массивов означают не только невозможность добавления связанного поведения, но и обязывает создавать сложные механизмы доступа к данным. Часто это реализовывается как многофункциональные классы со здоровыми методами, использующими вложенные циклы. Вроде этого
foreach ($ponds as $pond)
{
    foreach ($pond["fishBreeds"] as $breed)
    {
        $stockedFish += $breed["number"];
    }
}

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

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

Ты делаешь это неверно


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

Работа с наборами данных


Выбросьте из головы, что вам нужно использовать массивы для работы с набором данных. «Набор данных» (или коллекция) не означает использование массивов! Неопытные разработчики поступают так, потому что не слышали про итераторы. Их использование позволяет вам работать с коллекциями, реализуя конкретное поведение. На первый взгляд сложно, но на деле все просто. Вот класс, реализующий итератор

class PondsCollection implements IteratorAggregate
{
    private $collection = array();
 
    public function getIterator()
    {
        return new ArrayIterator($this->collection);
    }
 
    public function add($pond)
    {
        $this->collection[] = $pond;
    }
}

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

$ponds = new PondsCollection();
$ponds->add(...);
 
foreach ($ponds as $pond)
{
    ...
}

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

Подробнее в документации:

Комментарии (131)


  1. VolCh
    27.03.2016 15:57

    Использование массивов само по себе ни хорошо, ни плохо. Плохо (в большинстве случаев) в настоящее время использовать в PHP массивы в качестве сишных структур, в качестве коллекций семантически разнотипных значений, особенно с ключами представляющими собой человеческие имена типа 'first_name' или 'contracts' — для этого есть объекты. А вот использовать массивы как коллекции однотипных значений с ключами в виде числовых индексов или идентификаторов объектов — хорошо.


  1. firegurafiku
    27.03.2016 22:40

    А чем описанное в статье использование PHP-массивов принципиально отличается от JSON, который сейчас, вроде как, везде принято любить? По-моему, это абсолютно то же самое, этот код можно даже вынести в отдельный файл (с расширением .phpon?) или хранить в БД, «декодируя» вызовом функции eval. Поэтому, думаю, определённая легитимная область применения у этого подхода есть, частично совпадающая с областью применения JSON/YAML/XML.


    1. lnroma
      27.03.2016 23:30

      eval зло, взломал базу->дописал shell->зашёл на сервер-> и понеслась


      1. firegurafiku
        27.03.2016 23:40

        Само собой, зло — потому и кавычки. Но концептуально «PHP Object Notation» (или «PHP Array Notation»?) ничем не хуже JSON, кроме отсутствия аналога функции json_decode.


    1. Fesor
      27.03.2016 23:51

      плохо — анемичная модель, плохо — отсутствие инкапсуляции и глобальное состояние. А все остальное — детали.


      1. firegurafiku
        28.03.2016 00:50

        «Анемичная» модель в виде массива в качестве сущности бизнес-логики, наверное, совсем не айс, но в качестве DTO — почему нет? DTO не особенно нужна инкапсуляция, а хранить массив таких объектов в глобальной переменной никто не заставляет.
        Кстати, в Python есть такая вещь, как collections.namedtuple, которая очень подходит для сущностей сложнее кортежа, но недостаточно сложных для написания отдельного рукотворного класса. Уверен, что в насквозь динамическом PHP можно сделать аналог (или он уже существует).


        1. Fesor
          28.03.2016 01:08

          но в качестве DTO — почему нет?

          Для DTO массивы — ок, во всяком случае в простых случаях, коих большинство. Но это никоем образом не относится к тому, о чем так печется Матиас (автор статьи, тут перевод если что) когда говорит обо всем этом — модель предметной области.
          но недостаточно сложных для написания отдельного рукотворного класса.

          Обычно все же эту штуку используют в качестве базового класса. Ну то есть для большинства случаев объявление нашей структуры сводится к одной вполне себе читабельной строчки, но если что — можно поведение добавлять и т.д. В PHP к сожалению таких механизмов нет.
          Уверен, что в насквозь динамическом PHP можно сделать аналог (или он уже существует).

          PHP то насквозь динамический, но не настолько к сожалению. Тут поможет только кодогенерация в рантайме. Всему виной различия в объектной модели php и python, у php оно более статично, в силу исторических причин и потому что слизано у java/.net.
          Во всяком случае я искал в свое время и не нашел, и понял что объявить простые структурки и сгенерировать конструктор как-то выходит быстрее и проще.
          p.s. вообще ооочень много вопросов и споров в этой статье свелись бы на нет, если бы автор перевода предоставил информацию о том, кто такой Матиас, с каким проектами он работает и т.д. Ибо заметки в его личном блоге обычно читают люди, кто вкурсе, что он обычно вещает о том как правильно проектировать предметную область, прославляет DDD и тд. А что у вас между контроллерами и моделью ходит в качестве DTO — массивчики динамические, статические структурки или тупые объекты без поведения — это уже вам решать.


          1. alexkunin
            28.03.2016 05:24

            Можно не генерировать код, а все сделать через магию (get, set, __call и т.д.), но либо ваша IDE сойдет с ума, либо придется писать для нее плагин, который будет как-то угадывать поля-методы и выводить их типы. Например, с Ларавель так и получилось — без плагина для PhpStorm само почти ничего не подхватится.

            Так что да, генерация проще и надежней с точки зрения встраивания в IDE, хотя и выглядит немножко прошлым веком. Не хватает некоего мета-программирования, только не каши, как в JS, а чего-то более стройного.


  1. rotor
    28.03.2016 01:08
    -1

    Эх поздно я вашу заметку увидел. Не дает мне Хабр ее плюсануть уже.
    Солидарен на 100%. Тоже всегда вызывает недоумение.
    Самое интересное, что ассоциативные контейнеры есть ведь во многих языках. Но только в php придумали использовать их вместо объектов. И не просто придумали, это стало просто эпидемией какой-то. Пандемия.


    1. Fesor
      28.03.2016 01:20

      да ладно вам, в javascript сообществе с этим похуже как-то пока. Но ребята растут, развиваются. А у PHP-ников пока отсутствует понятие "состояни" и "побочные эффекты".


    1. VolCh
      31.03.2016 00:33

      Придумали в то время, когда ещё объектов не было. И натолкнули на мысль авторы языка прежде всего, предоставляя окружение и запрос в виде суперглобальных ассоциативных массивов. Хочешь написать hello $name — используй массив.


  1. babylon
    31.03.2016 19:09

    Есть json структура, есть схема и есть набор бинарных буферов или мемори таблицы- больше ничего не надо! Если надо собрали вJSON объект из массивов ссылок на мемори таблицы. Если надо разобрали объект на массивы из ссылок на мемори таблицы или буферы в соответствии со схемами. Всё! Остальное извращение. Не имеет значения PHP,JS, JAVA,C++.


    1. Fesor
      31.03.2016 19:30
      +1

      А поведение где? Как вы будете спасаться от сайд эффектов? Из вашего описания получается какая-то процедурщина.
      Вообще есть хорошее правило — не стоит быть столь категоричными. Для каждой ситуации свой подход.