Статьи

Практический рефакторинг PHP: заменить запись на класс данных


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

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

Структуры, подобные записям, являются эквивалентом структур C или хэшей Ruby . Этот рефакторинг является обобщением Replace Array with Object : в этом случае отправной точкой является не просто массив, который всегда имеет одинаковое количество и тип полей, но любая структура данных, однородная для него:

В некоторых языках (см. C) запись представляет собой особую структуру данных, которая не является объектом; в PHP это всегда массив или объект некоторого класса вендоров.

Даже ORM, основанные на Active Record , более продвинуты, чем этот вид использования: они обычно позволяют добавлять методы в классы модели, которые заполняются данными самим ORM. В этом рефакторинге речь идет о структуре данных, управляемой языком персистентного уровня, и код которой вы не можете изменить.

Зачем заменять структуру, похожую на запись?

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

Решения

Существуют разные способы устранить связь с подобной записи структурой.

Первый реорганизовать к подклассам , где структура данных становится активной записью. Вы расширяете класс vendor своим собственным: этот параметр доступен, только если структура определена как объект.

Кроме того, имя класса для создания экземпляра должно быть настраиваемым в механизме сохранения. Zend_Db_Table_Record поддерживает этот вид использования.

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

Третий и окончательный вариант заключается в использовании гидратации : данные копируются в вашей модели объектов и записи выбрасываются после факта. Doctrine 2 и Data Mappers в целом выбирают этот подход.

меры

Я опишу краткую процедуру рефакторинга для гидратации, поскольку это наиболее сложный подход, и он всегда применим. Другие вместо этого специфичны для конкретной структуры данных (например, с Zend_Db вы должны написать несколько подклассов и настроить некоторые защищенные поля, чтобы они содержали правильные имена классов.)

  1. Создайте новый класс , как одну из ваших моделей. Его состояние должно быть представлено одной строкой (или несколькими строками, объединенными в одну) в базе данных.
  2. Этот класс должен получить личное поле для каждого из полей записи , обычно с геттерами и сеттерами.
  3. Этот класс должен принимать в конструкторе или в методе Factory экземпляр записи , чтобы он мог создать новый экземпляр.

Если вы хотите отделить от постоянства, или вы хотите пойти двумя путями (также сохранить и не только визуализировать, так как это не просто модель представления), ищите Data Mapper, такой как Doctrine 2, который даже скрывает все записывать структуры от вас и менеджеров ассоциаций, где объекты составляют другие.

пример

В этом примере данные одного пользователя возвращаются в массиве. Я выбрал массив в качестве структуры записи, чтобы минимизировать внешние зависимости этого кода.

<?php
/**
 * The test case works as an example of client code, as always.
 */
class ReplaceRecordWithDataClass extends PHPUnit_Framework_TestCase
{
    public function testShowContainedData()
    {
        $users = new UsersTable();
        $giorgio = $users->find(42);
        $this->assertEquals('Giorgio', $giorgio['name']);
    }
}

/**
 * This is a Fake Table Data Gateway. The machinery for making it work with
 * a database will be distracting for our purposes, so they will be omitted.
 */
class UsersTable
{
    /**
     * @return mixed    the returned value can be a Zend_Db_Table_Row,
     *                  an Active Record, a stdClass, an associative array...
     *                  It should just represent a single entity.
     */
    public function find($id)
    {
        // execute a PDOStatement and fetch the data
        return array('id' => 42, 'name' => 'Giorgio');
    }
}

После рефакторинга у нас есть класс User, куда мы можем добавить все необходимые нам методы:

<?php
/**
 * The test case works as an example of client code, as always.
 */
class ReplaceRecordWithDataClass extends PHPUnit_Framework_TestCase
{
    public function testShowContainedData()
    {
        $users = new UsersTable();
        $giorgio = $users->find(42);
        $this->assertEquals('Giorgio', $giorgio->getName());
    }
}

/**
 * This is a Fake Table Data Gateway. The machinery for making it work with
 * a database will be distracting for our purposes, so they will be omitted.
 */
class UsersTable
{
    /**
     * @return mixed    the returned value can be a Zend_Db_Table_Row,
     *                  an Active Record, a stdClass, an associative array...
     *                  It should just represent a single entity.
     */
    public function find($id)
    {
        // execute a PDOStatement and fetch the data
        return User::fromRecord(array('id' => 42, 'name' => 'Giorgio'));
    }
}

class User
{
    private $id;
    private $name;

    public static function fromRecord(array $record)
    {
        $object = new self();
        $object->id = $record['id'];
        $object->name = $record['name'];
        return $object;
    }

    public function getName()
    {
        return $this->name;
    }
}