В сегодняшнем сценарии в методе отсутствует некоторая информация для выполнения своей обязанности. Обычно он вынужден искать эти данные в какой-либо форме глобального состояния (например, синглтоне) или просто избегает поддержки какого-либо варианта использования из-за недостающей части, в которой он нуждается.
В первом случае (во втором случае недопустимо отсутствующая функциональность) возникает проблема с поиском информации; внешние зависимости устанавливаются из этого класса в другие части системы. Вы не сможете повторно использовать или протестировать текущий класс, не имея готового Zend_Controller_Front или не установив некоторые переменные $ _SESSION.
Альтернативой является передача ссылки в качестве параметра метода. Фаулер подчеркивает, что это всего лишь несколько лучший вариант, и в следующих статьях этой серии будет больше статей, в которых мы рассмотрим более простые вызовы метода «Создание» .
Почему другой параметр?
Вам необходимо каким-то образом предоставить объекту эту зависимость: в противном случае объект не может работать или должен скопировать некоторый код из другого класса, что приводит к дублированию.
Если зависимость нужна только в этом конкретном методе, хранить ее в закрытом поле имеет меньше смысла . Эквивалентно, если один и тот же объект работает со многими различными экземплярами этой зависимости, поле имеет еще меньший смысл.
Почему нет еще одного параметра?
Списки параметров имеют тенденцию становиться все длиннее и длиннее : PHPUnit_Framework_TestCase :: getMock () является примером популярного метода, принимающего 7 параметров (хотя большинство из них являются необязательными.) Параметр также вводит некоторые зависимости: объект становится зависимым от параметра, даже если на него ссылаются только в одном из его методов, а не в других его 100 строках кода.
Если объект не может удовлетворить вызов метода без дополнительных параметров, это также может быть случай, когда эта ответственность не возлагается на него . Иногда ответственность можно разделить, а бизнес-логику разделить на существующий параметр или соавтор и текущий объект. В этом случае доступ к новому параметру является задачей, переданной на аутсорсинг другому объекту, который может уже иметь его в поле или в стеке.
Но давайте прекратим этот абстрактный разговор и углубимся в некоторый код!
меры
В качестве предварительного условия проверьте наличие подклассов и суперклассов: если вы хотите поддерживать полиморфное поведение, все копии метода в иерархии должны поддерживать общую подпись. Вам придется добавить параметр к ним тоже.
- Добавьте новый метод с новым параметром. Скопируйте в него код, подобный операции переименования , и используйте немного другое имя (см. Пример, чтобы узнать, почему).
- Измените старую копию метода так, чтобы он просто делегировал новую . Особенно, когда старый метод, вероятно, покрывает много вариантов использования, мы не хотим ломать их; как только два метода сработают, а дублирование устранено, мы можем реорганизовать вызовы по одному за раз.
- Если тесты пройдены, измените вызовы на старый метод и продолжайте сохранять пакет зеленым.
- После изменения всех вызовов удалите старую версию метода, так как она больше не должна быть доступна ни из производственного кода, ни из набора тестов.
пример
В исходном состоянии мы описываем фискальную систему, аналогичную итальянской, где все счета должны иметь прогрессивные номера, назначенные в соответствии с их датой.
Однако в некоторых случаях счет может быть вставлен в середину списка, поскольку нет необходимости регистрировать счета в компьютерной системе в их фактическом хронологическом порядке.
<?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;
}
}