Статьи

Практический рефакторинг PHP: инкапсуляция Downcast (и упаковка)

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

Языки со статической типизацией иногда сталкиваются с проблемой понижения рейтинга: компилятор может гарантировать только базовый тип, а содержащийся в нем объект является экземпляром более богатого подтипа. Пример Java может быть коллекцией экземпляров Object, где некоторые из них на самом деле являются String: чтобы получить экземпляр String, чтобы иметь возможность вызывать string.startsWith («prefix_»), код, использующий коллекцию, нуждается в понижении:

String myString = (String) myObject;

Эта проблема была действительно распространена в старом Java-коде (до введения Generics ) и до сих пор в некоторых случаях.

Как насчет PHP?

Вам никогда не потребуется понижать объекты: переменные могут содержать обработчики объектов или даже скаляры без проверок во время компиляции. Приведение с помощью (ClassName) даже не поддерживается языком (в то время как приведение необъекта с помощью (object) даст вам stdClass.)

Однако понижение рейтинга означает продвижение переменной из более строгого интерфейса в более богатый: мы применим этот рефакторинг к скалярам и их OO-эквивалентам, Value Values.

Обратите внимание, что в книге Фаулера даункастинг применяется только к объектам: тот же экземпляр просто приобретает новый (больший) интерфейс. Так как это приведение отсутствует в PHP и тип совпадает с текущим содержимым переменной, мы поговорим о преобразованиях переменных в различные типы.

Скаляры

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

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

public function isASeatAvailable()
{
    return $this->numberOfSeats - 42; // 42 is a total of sold tickets
}
$result = (bool) $theater->isASeatAvailable();

Приведение (bool) будет распространяться по всему клиентскому коду, вызывающему $ object. Логика рефакторинга такая же, как и для приведений к объектам: их инкапсуляция в метод приводит к более чистому API и отсутствию дублирования приведения во всем клиентском коде.

Объекты значения

Другая форма приведения / преобразования, которая нам нужна в гибридных языках, таких как PHP, — это преобразование примитивного (скалярного или массивного) значения в объект (обычно Value Object).
Иногда это преобразование дублируется, как в случае примитивов приведения:

$topic = new Topic($object->getAllPosts());
$firstPage = new Topic($topic->getPart($offset = 0, $limit = 10));

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

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

/** @var Topic */ $topic = $object->getAllPosts();
/** @var Topic */ $firstPage = $topic->getPart(0, 10);
/** @var Topic */ $filteredTopic = $topic->getSelectedPostsForQuery("refactoring");

Docblocks

Блоки документов, применяемые к методам, также должны быть изменены, чтобы отражать в документации API то, что вы возвращаете (объект, Traversable, IteratorAggregate или MyIterator). Таким образом, этот рефакторинг влияет на них.

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

/**
 * @return Traversable  containing strings
 */
public function threadTitles() { ... } 

меры

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

  • новые операторы MyClass (…) часто выполняются по результату метода.
  • Объекты Value, имеющие много методов получения для своих полей или вычисляемых полей, часто могут возвращать другой Объект Value; ищите дублированную логику в клиентском коде.
  • В конечном итоге (наоборот) любой метод, возвращающий массив или скалярное значение, является подозрительным.

Второй шаг — перемещение актера вниз в метод. Это означает изменение как вызова, так и объектов, в то время как вы можете сделать один шаг за раз в случае скалярного приведения из-за идемпотентности операции (типа).

Наконец, не забудьте обновить докблок измененных методов соответственно.

пример

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

Этот объект моделирует номерной знак, используемый в автомобиле. Я только что реализовал самый простой случай продвижения планшета (он переполнится после 26 вызовов next ()), чтобы этот код был простым и понятным.

Мы видим, что next () является целью нашего рефакторинга.

<?php
class EncapsulateDowncast extends PHPUnit_Framework_TestCase
{
    public function test()
    {
        $plate = new Plate('AB123XY');
        $newPlate = new Plate($plate->next());
        $this->assertEquals(new Plate('AB123XZ'), $newPlate);
    }
}

class Plate
{
    private $value;

    public function __construct($value)
    {
        $this->value = $value;
    }

    /**
     * @return string
     */
    public function next()
    {
        // we're dealing just with the basic case
        $lastLetter = substr($this->value, -1);
        $lastLetter++;
        $nextValue = substr_replace($this->value, $lastLetter, -1);
        return $nextValue;
    }
}

За один шаг мы можем сохранить прохождение теста. Мы модифицируем то, что ожидается от клиентского кода, теперь экземпляр Plate:

<?php
class EncapsulateDowncast extends PHPUnit_Framework_TestCase
{
    public function test()
    {
        $plate = new Plate('AB123XY');
        $newPlate = $plate->next();
        $this->assertEquals(new Plate('AB123XZ'), $newPlate);
    }
}

Мы модифицируем docblock, в частности @return, и выполняем инстанцирование внутри метода.

class Plate
{
    private $value;

    public function __construct($value)
    {
        $this->value = $value;
    }

    /**
     * @return Plate
     */
    public function next()
    {
        // we're dealing just with the basic case
        $lastLetter = substr($this->value, -1);
        $lastLetter++;
        $nextValue = substr_replace($this->value, $lastLetter, -1);
        return new self($nextValue);
    }
}