Статьи

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

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

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

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

Почему косвенность по новой ()?

Есть много разных причин использовать фабричный метод.

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

Во-вторых, фабричный метод дает имя операции конструктора ; в классическом примере новый Area (0, 20, 100, 220) становится читаемым вызовом Area :: fromXYtoXY (0, 20, 100, 220). Объекты значения очень хорошо работают с этим шаблоном.

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

Статические фабричные методы, особенно для объектов-значений, являются одним из немногих случаев использования статического ключевого слова, которое я допускаю. Проблема с * static * методами заключается в том, что они не могут использовать соавторы: рассмотрим объект Factory, если метод Factory растет.

меры

  1. Создайте фабричный метод , делегируя его конструктору.
  2. Замените вызовы new () вызовами метода Factory, который просто делегирует конструктору.
  3. Измените область конструктора на частную, если она вам не нужна для тестирования.
  4. Переместите код, который относится только к Factory Method, за пределы конструктора. Если вы не используете конструктор повторно, он функционально эквивалентен, поэтому просто поместите его там, где он чувствует себя лучше (или где вы бы его искали).

Например, каждая ссылка на $ this должна оставаться в конструкторе; тяжелые операции над входными параметрами лучше размещаются в методе Factory, так что вы можете протестировать объект, быстро создав ему известную конфигурацию с помощью new ().

пример

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

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

  • даны два противоположных угла. Прямоугольник определяется однозначно.
  • Площадь строится вокруг центральной точки определенного размера (по умолчанию квадрат).
<?php
class ReplaceConstructorWithFactoryMethod extends PHPUnit_Framework_TestCase
{
    public function testAnAreaIsCreatedGivenTheTwoOppositeCorners()
    {
        $area = new Area(1, 11, 100, 210);
        $this->assertEquals(20000, $area->measure());
    }

    public function testAnAreaIsCreatedAroundAPoint()
    {
        $area = new Area(400, 500, 100);
        $this->assertEquals(10000, $area->measure());
    }
}

class Area
{
    private $first_corner_x;
    private $first_corner_y;
    private $second_corner_x;
    private $second_corner_y;

    public function __construct($first_x, $first_y, $second_x, $second_y = null)
    {
        if ($second_y === null) {
            $size = $second_x;
            $this->first_corner_x = $first_x - $size / 2 + 1;
            $this->first_corner_y = $first_y - $size / 2 + 1;
            $this->second_corner_x = $first_x + $size / 2;
            $this->second_corner_y = $first_y + $size / 2;
        } else {
            $this->first_corner_x = $first_x;
            $this->first_corner_y = $first_y;
            $this->second_corner_x = $second_x;
            $this->second_corner_y = $second_y;
        }
    }

    public function measure()
    {
        $width = $this->second_corner_x - $this->first_corner_x + 1;
        $height = $this->second_corner_y - $this->first_corner_y + 1;
        return $width * $height;
    }
}

Мы собираемся написать фабричный метод для первого варианта использования.

class Area
{
    /* ... */
    public static function fromXYtoXY($first_x, $first_y, $second_x, $second_y)
    {
        return new self($first_x, $first_y, $second_x, $second_y);
    }
}

А теперь второй вариант использования. Прямо сейчас эти методы просто вызывают new () и являются синтаксическим сахаром.

class Area
{
    /* ... */
    public static function fromXYtoXY($first_x, $first_y, $second_x, $second_y)
    {
        return new self($first_x, $first_y, $second_x, $second_y);
    }

    public static function fromCenterAndDimension($center_x, $center_y, $dimension)
    {
        return new self($center_x, $center_y, $dimension);
    }
}

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

<?php
class ReplaceConstructorWithFactoryMethod extends PHPUnit_Framework_TestCase
{
    public function testAnAreaIsCreatedGivenTheTwoOppositeCorners()
    {
        $area = Area::fromXYtoXY(1, 11, 100, 210);
        $this->assertEquals(20000, $area->measure());
    }

    public function testAnAreaIsCreatedAroundAPoint()
    {
        $area = Area::fromCenterAndDimension(400, 500, 100);
        $this->assertEquals(10000, $area->measure());
    }
}

class Area
{
    private $first_corner_x;
    private $first_corner_y;
    private $second_corner_x;
    private $second_corner_y;

    public function __construct($first_x, $first_y, $second_x, $second_y)
    {
        $this->first_corner_x = $first_x;
        $this->first_corner_y = $first_y;
        $this->second_corner_x = $second_x;
        $this->second_corner_y = $second_y;
    }

    public static function fromXYtoXY($first_x, $first_y, $second_x, $second_y)
    {
        return new self($first_x, $first_y, $second_x, $second_y);
    }

    public static function fromCenterAndDimension($center_x, $center_y, $dimension)
    {
        $first_x = $center_x - $dimension / 2 + 1;
        $first_y = $center_y - $dimension / 2 + 1;
        $second_x = $center_x + $dimension / 2;
        $second_y = $center_y + $dimension / 2;
        return new self($first_x, $first_y, $second_x, $second_y);
    }

    public function measure()
    {
        $width = $this->second_corner_x - $this->first_corner_x + 1;
        $height = $this->second_corner_y - $this->first_corner_y + 1;
        return $width * $height;
    }
}

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

class Area
{
    private $first_corner_x;
    private $first_corner_y;
    private $second_corner_x;
    private $second_corner_y;

    private function __construct($first_x, $first_y, $second_x, $second_y)
    {
        $this->first_corner_x = $first_x;
        $this->first_corner_y = $first_y;
        $this->second_corner_x = $second_x;
        $this->second_corner_y = $second_y;
    }
    /* ... */
}

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