Статьи

Практический рефакторинг PHP: сохранить весь объект

В сегодняшнем сценарии мы извлекаем некоторые поля или вычисленные значения из объекта, а затем вызываем метод где-то еще, передавая их в качестве параметров.

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

Почему один объект в качестве параметра?

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

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

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

Примечание о зависимостях

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

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

альтернативы

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

Существует также случай, когда весь объект вызывает метод, передавая себя сам. Лично мне не нравится передавать $ this вместо $ this-> field {1,2,3}, когда метод в A вызывается объектом B, передавая свои собственные значения.

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

меры

  1. Добавьте в метод новый параметр : весь объект. Используйте Добавить параметр.
  2. Определите параметры, которые нужно взять с объекта.
  3. Для каждого из них замените внутреннюю ссылку на код, который получает ее от всего объекта .
  4. Удалите параметр и перезапустите следующий. Используйте параметр «Удалить параметр», чтобы избежать взлома кода.

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

пример

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

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

<?php
class PreserveWholeObject extends PHPUnit_Framework_TestCase
{
    public function testTheSlotEvaluatesItsLength()
    {
        $today = new DateTime('2011-11-23');
        $slot = new MonthSpecificSlot();
        $this->assertTrue($slot->containsAWeek($today->format('m'), $today->format('d')));
    }
}

class MonthSpecificSlot
{
    public function containsAWeek($month, $day)
    {
        $reference = new DateTime('2011-' . $month . '-01');
        $daysInMonth = $reference->format('t');
        return $day + 6 <= $daysInMonth;
    }
}

Мы добавляем новый параметр: объект DateTime, из которого извлекаются день и месяц. Нам не нужно устранять зависимость от объекта путем введения интерфейса, потому что DateTime — это простой ValueObject, представленный языком, как, например, ArrayIterator.

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

<?php
class PreserveWholeObject extends PHPUnit_Framework_TestCase
{
    public function testTheSlotEvaluatesItsLength()
    {
        $today = new DateTime('2011-11-23');
        $slot = new MonthSpecificSlot();
        $this->assertTrue($slot->containsAWeek($today, $today->format('m'), $today->format('d')));
    }
}

class MonthSpecificSlot
{
    public function containsAWeek($startDate, $month, $day)
    {
        $reference = new DateTime('2011-' . $month . '-01');
        $daysInMonth = $reference->format('t');
        return $day + 6 <= $daysInMonth;
    }
}

Теперь мы извлекаем параметры изнутри объекта в методе. Тест все еще проходит.

class MonthSpecificSlot
{
    public function containsAWeek($startDate, $month, $day)
    {
        $month = $startDate->format('m');
        $day = $startDate->format('d');
        $reference = new DateTime('2011-' . $month . '-01');
        $daysInMonth = $reference->format('t');
        return $day + 6 <= $daysInMonth;
    }
}

Мы удаляем неиспользуемые параметры, действуя также на клиентском коде.

<?php
class PreserveWholeObject extends PHPUnit_Framework_TestCase
{
    public function testTheSlotEvaluatesItsLength()
    {
        $today = new DateTime('2011-11-23');
        $slot = new MonthSpecificSlot();
        $this->assertTrue($slot->containsAWeek($today));
    }
}

class MonthSpecificSlot
{
    public function containsAWeek($startDate)
    {
        $month = $startDate->format('m');
        $day = $startDate->format('d');
        $reference = new DateTime('2011-' . $month . '-01');
        $daysInMonth = $reference->format('t');
        return $day + 6 <= $daysInMonth;
    }
}

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

class MonthSpecificSlot
{
    public function containsAWeek($startDate)
    {
        $day = $startDate->format('d');
        $daysInMonth = $startDate->format('t');
        return $day + 6 <= $daysInMonth;
    }
}