Статьи

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

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

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

Альтернативой является передача ссылки в качестве параметра метода. Фаулер подчеркивает, что это всего лишь несколько лучший вариант, и в следующих статьях этой серии будет больше статей, в которых мы рассмотрим более простые вызовы метода «Создание» .

Почему другой параметр?

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

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

Почему нет еще одного параметра?

Списки параметров имеют тенденцию становиться все длиннее и длиннее : PHPUnit_Framework_TestCase :: getMock () является примером популярного метода, принимающего 7 параметров (хотя большинство из них являются необязательными.) Параметр также вводит некоторые зависимости: объект становится зависимым от параметра, даже если на него ссылаются только в одном из его методов, а не в других его 100 строках кода.

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

Но давайте прекратим этот абстрактный разговор и углубимся в некоторый код!

меры

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

  1. Добавьте новый метод с новым параметром. Скопируйте в него код, подобный операции переименования , и используйте немного другое имя (см. Пример, чтобы узнать, почему).
  2. Измените старую копию метода так, чтобы он просто делегировал новую . Особенно, когда старый метод, вероятно, покрывает много вариантов использования, мы не хотим ломать их; как только два метода сработают, а дублирование устранено, мы можем реорганизовать вызовы по одному за раз.
  3. Если тесты пройдены, измените вызовы на старый метод и продолжайте сохранять пакет зеленым.
  4. После изменения всех вызовов удалите старую версию метода, так как она больше не должна быть доступна ни из производственного кода, ни из набора тестов.

пример

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

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

<?php
class AddParameter extends PHPUnit_Framework_TestCase
{
public function testProvidesNextInvoiceNumbersForTheCurrentDate()
{
$invoices = new Invoices(array(1 => '2011-10-01', 2 => '2011-11-01'));
$this->assertEquals(3, $invoices->getNextProgressiveNumber());
}

public function testNextInvoiceNumberIsAnExistingOneInCaseWeHaveToRenumerate()
{
$invoices = new Invoices(array(1 => '2011-10-01', 2 => '2011-11-10'));
$this->assertEquals(2, $invoices->getNextProgressiveNumber());
}
}

class Invoices
{
private $invoiceDates;

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

public function getNextProgressiveNumber()
{
$currentDate = date('Y-m-d');
foreach ($this->invoiceDates as $number => $date) {
if ($date > $currentDate) {
$nextNumber = $number;
break;
}
}
if (!isset($nextNumber)) {
$nextNumber = count($this->invoiceDates) + 1;
}
return $nextNumber;
}
}

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

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

class Invoices
{
private $invoiceDates;

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

public function getNextProgressiveNumber()
{
$currentDate = date('Y-m-d');
foreach ($this->invoiceDates as $number => $date) {
if ($date > $currentDate) {
$nextNumber = $number;
break;
}
}
if (!isset($nextNumber)) {
$nextNumber = count($this->invoiceDates) + 1;
}
return $nextNumber;
}

public function getProgressiveNumberForInsertion($currentDate)
{
foreach ($this->invoiceDates as $number => $date) {
if ($date > $currentDate) {
$nextNumber = $number;
break;
}
}
if (!isset($nextNumber)) {
$nextNumber = count($this->invoiceDates) + 1;
}
return $nextNumber;
}
}

Далее мы делегируем ответственность старого метода новому, принимающему параметр.

class Invoices
{
private $invoiceDates;

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

public function getNextProgressiveNumber()
{
$currentDate = date('Y-m-d');
return $this->getProgressiveNumberForInsertion($currentDate);
}

public function getProgressiveNumberForInsertion($currentDate)
{
foreach ($this->invoiceDates as $number => $date) {
if ($date > $currentDate) {
$nextNumber = $number;
break;
}
}
if (!isset($nextNumber)) {
$nextNumber = count($this->invoiceDates) + 1;
}
return $nextNumber;
}
}

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

<?php
class AddParameter extends PHPUnit_Framework_TestCase
{
    public function testProvidesNextInvoiceNumbersForTheCurrentDate()
    {
        $invoices = new Invoices(array(1 => '2011-10-01', 2 => '2011-11-01'));
        $this->assertEquals(3, $invoices->getProgressiveNumberForInsertion('2011-11-02'));
    }

    public function testNextInvoiceNumberIsAnExistingOneInCaseWeHaveToRenumerate()
    {
        $invoices = new Invoices(array(1 => '2011-10-01', 2 => '2011-11-10'));
        $this->assertEquals(2, $invoices->getProgressiveNumberForInsertion('2011-11-02'));
    }
}

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

class Invoices
{
private $invoiceDates;

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

public function getProgressiveNumberForInsertion($currentDate)
{
foreach ($this->invoiceDates as $number => $date) {
if ($date > $currentDate) {
$nextNumber = $number;
break;
}
}
if (!isset($nextNumber)) {
$nextNumber = count($this->invoiceDates) + 1;
}
return $nextNumber;
}
}