Статьи

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


В сегодняшнем (повторяющемся) сценарии метод изменяет состояние объекта и одновременно возвращает что-то.
В этом случае метод является гибридом между:

  1. запрос , который позволяет получить часть состояния объекта (мы не говорим о SQL здесь);
  2. модификатора , который может изменить наблюдаемое состояние.

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

Наблюдаемый что?

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

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

То же самое касается любого вида кэша : их внутреннее состояние не наблюдается, поэтому, если они изменяют его при первом вызове, это не проблема с точки зрения клиента. Для них естественно назначить поля $ this и вернуть что-то тем же методом.

Первое эмпирическое правило для выявления нарушения CQS рассматривает методы, которые:

  • изменить состояние, присваивая что-то $ this или перенаправляя вызовы, чтобы изменить состояние соавторов;
  • и иметь тип @return, отличный от void.

Тогда вы должны поставить под сомнение видимость изменений извне.

Принцип CQS

Принцип разделения командных запросов был формализован Бертраном Мейером, автором Открытого / Закрытого Принципа и языка Eiffel. Этот принцип позволяет вам дублировать вызовы к методу-получателю, не беспокоясь, или перемещать вызовы к другим объектам, зная, что они не могут причинить вреда.

Этот принцип отражен в другом месте: протокол HTTP отделяет GET как безопасный метод от POST, который может изменять состояние сервера. Таким образом, прокси-серверы могут кэшировать ответы на GET-запросы и всегда пересылать POST; и сканеры могут переходить по ссылкам, не заполняя свою прикладную базу мусором как побочный эффект.

Такое же преимущество присутствует в клиентском коде, который вызывает один из этих методов. Вы не хотите задаваться вопросом, изменит ли ваш getXXX () что-то; особенно при отладке или написании теста вы предполагаете, что можете вызывать его столько раз, сколько захотите, проверяя состояние, не влияя на наличие ошибки.

Известное исключение из этого принципа (цитируемое Грегом Янгом в одном из его выступлений по CQRS) — это очереди и метод их dequeue (). Однако это всего лишь исключение, а не правило.

меры

  1. Создайте открытый метод Query, который возвращает то, что возвращал оригинальный метод.
  2. Измените оригинальный метод : теперь он должен делегировать запросу, который является идемпотентным.
  3. Проверьте набор тестов .
  4. Замените вызовы исходного метода, разделив их в вызовах метода Command и нового метода Query.
  5. Исключите возвращаемые значения из исходного метода, который теперь вызывается, но без присвоения его результата (как void).

Я считаю эти шаги, перечисленные Фаулером, неполными в том случае, если мы хотим сначала извлечь Команду. Этот второй случай более уместен, если мы хотим устранить побочные эффекты из запроса.

пример

Этот объект домена представляет пользователя с вычисляемым полем, содержащим его полное имя. Ситуация такова, что __toString () вызывается где-то в приложении; Если вызов удален, неполный объект будет доступен для вызовов getXXX (), и вместо имени может быть напечатано значение NULL.

<?php
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase
{
    public function testTheDomainObjectProvidesACalculatedField()
    {
        $user = new User('Giorgio', 'Sironi');
        $this->assertEquals('User: Giorgio Sironi', $user->__toString());
    }
}

/**
 * @Entity
 */
class User
{
    /**
     * @Column
     */
    private $firstName;
    /**
     * @Column
     */
    private $lastName;
    /**
     * @Column
     */
    private $fullName;

    public function __construct($first, $last)
    {
        $this->firstName = $first;
        $this->lastName = $last;
    }

    public function getFirstName() { return $this->firstName; }
    public function getLastName() { return $this->lastName; }
    public function getFullName() { return $this->fullName; }

    public function __toString()
    {
        $this->fullName = $this->firstName . ' ' . $this->lastName;
        return 'User: ' . $this->fullName;
    }
}

Давайте напишем тест, который выявляет проблему CQS, будучи независимым от того, как вычисляется значение поля.

<?php
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase
{
    public function testTheDomainObjectProvidesACalculatedField()
    {
        $user = new User('Giorgio', 'Sironi');
        $this->assertEquals('User: Giorgio Sironi', $user->__toString());
    }

    public function testTheDomainObjectQueriesShouldNotModifyTheObservableStateOfTheObjectItself()
    {
        $user = new User('Giorgio', 'Sironi');
        $oldFullName = $user->getFullName();
        $user->__toString();
        $this->assertEquals($oldFullName, $user->getFullName());
    }
}

Мы извлекаем Команду, которая делегирует возвращаемую часть методу Query (который был очищен). На этот раз я предпочел бы извлечь команду вместо извлечения Query, потому что я хочу сохранить для нее имя __toString ().

Таким образом, мы должны также изменить вызовы сейчас.

<?php
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase
{
    public function testTheDomainObjectProvidesACalculatedField()
    {
        $user = new User('Giorgio', 'Sironi');
        $user->completeFields();
        $this->assertEquals('User: Giorgio Sironi', $user->__toString());
    }

    public function testTheDomainObjectQueriesShouldNotModifyTheObservableStateOfTheObjectItself()
    {
        $user = new User('Giorgio', 'Sironi');
        $user->completeFields();
        $oldFullName = $user->getFullName();
        $user->__toString();
        $this->assertEquals($oldFullName, $user->getFullName());
    }
}

/**
 * @Entity
 */
class User
{
    /**
     * @Column
     */
    private $firstName;
    /**
     * @Column
     */
    private $lastName;
    /**
     * @Column
     */
    private $fullName;

    public function __construct($first, $last)
    {
        $this->firstName = $first;
        $this->lastName = $last;
    }

    public function getFirstName() { return $this->firstName; }
    public function getLastName() { return $this->lastName; }
    public function getFullName() { return $this->fullName; }

    public function completeFields()
    {
        $this->fullName = $this->firstName . ' ' . $this->lastName;
        return $this->__toString();
    }

    public function __toString()
    {
        return 'User: ' . $this->fullName;
    }
}

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

<?php
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase
{
    public function testTheDomainObjectProvidesACalculatedField()
    {
        $user = new User('Giorgio', 'Sironi');
        $user->completeFields();
        $this->assertEquals('User: Giorgio Sironi', $user->__toString());
    }

    public function testTheDomainObjectQueriesShouldNotModifyTheObservableStateOfTheObjectItself()
    {
        $user = new User('Giorgio', 'Sironi');
        $user->completeFields();
        $oldFullName = $user->getFullName();
        $user->__toString();
        $this->assertEquals($oldFullName, $user->getFullName());
    }
}

/**
 * @Entity
 */
class User
{
    /**
     * @Column
     */
    private $firstName;
    /**
     * @Column
     */
    private $lastName;
    /**
     * @Column
     */
    private $fullName;

    public function __construct($first, $last)
    {
        $this->firstName = $first;
        $this->lastName = $last;
    }

    public function getFirstName() { return $this->firstName; }
    public function getLastName() { return $this->lastName; }
    public function getFullName() { return $this->fullName; }

    public function completeFields()
    {
        $this->fullName = $this->firstName . ' ' . $this->lastName;
    }

    public function __toString()
    {
        return 'User: ' . $this->fullName;
    }
}

Наконец, поскольку команда всегда вызывается после построения, я не могу избежать перемещения вызова внутри класса.

<?php
class SeparateQueryFromModifier extends PHPUnit_Framework_TestCase
{
    public function testTheDomainObjectProvidesACalculatedField()
    {
        $user = new User('Giorgio', 'Sironi');
        $this->assertEquals('User: Giorgio Sironi', $user->__toString());
    }

    public function testTheDomainObjectQueriesShouldNotModifyTheObservableStateOfTheObjectItself()
    {
        $user = new User('Giorgio', 'Sironi');
        $oldFullName = $user->getFullName();
        $user->__toString();
        $this->assertEquals($oldFullName, $user->getFullName());
    }
}

/**
 * @Entity
 */
class User
{
    /**
     * @Column
     */
    private $firstName;
    /**
     * @Column
     */
    private $lastName;
    /**
     * @Column
     */
    private $fullName;

    public function __construct($first, $last)
    {
        $this->firstName = $first;
        $this->lastName = $last;
        $this->completeFields();
    }

    public function getFirstName() { return $this->firstName; }
    public function getLastName() { return $this->lastName; }
    public function getFullName() { return $this->fullName; }

    private function completeFields()
    {
        $this->fullName = $this->firstName . ' ' . $this->lastName;
    }

    public function __toString()
    {
        return 'User: ' . $this->fullName;
    }
}