Одна из первых вещей, которые вы обычно хотите сделать при работе с устаревшим кодом – это написать все правильно тесты. Таким образом, вы можете позже внести изменения в код и убедиться, что вы не изменили или не нарушили существующее поведение.
Проблема обычно заключается в том, что устаревший код не очень способствует тестированию. Это может быть вызвано самыми разными причинами. Но проблема, которую я видел чаще всего заключается в том, что все так тесно связано, что вы действительно не можете просто проверить одну сущность. Потому что эта сущность зависит от другой, которая требует третью
Вы хотели банан, но то, что вы получили, было гориллой, держащей банан и все джунгли.
Джо Армстронг – создатель Эрланга
Возьмем для примера этот упрощенный фрагмент кода:
class LegacyClass
{
public function legacyMethod()
{
$dependency = new MyDependency();
// do something...
}
}
Я обычно не большой поклонник тяжелых тестов. Но, в случае устаревшего приложения, я действительно не хочу втягивать зависимость, которую я не могу контролировать. В некоторых случаях это может даже сделать тестирование почти невозможным из-за того, насколько тесно все связано. Что же MyDependency
делает? Он может расширить еще 15 классов. Для этого может потребоваться подключение к базе данных. Он может отправить электронное письмо, используя жестко закодированные учетные данные! Я не хочу, чтобы это происходило каждый раз, когда я провожу тест.
Создайте необязательный параметр конструктора
Одна вещь, которую вы можете сделать в этом случае, – это объявить зависимость как необязательную зависимость конструктора. Затем в вызове метода вы создаете экземпляр зависимости только в том случае, если она не была передана через конструктор.
class LegacyClass
{
/** @var null|MyDependency */
private $dep;
public function __construct(?MyDependency $dep = null)
{
$this->dep = $dep;
}
public function legacyMethod()
{
$dependency = $this->dep ?: new MyDependency();
// do something...
}
}
Таким образом, теперь вы можете передать заглушку/макет/подделку/что угодно в своем тесте, но в то же время не должны касаться какого-либо существующего кода. Если мы не передаем зависимость через конструктор, то метод просто работает так же, как и раньше.
/** @test */
public function it_does_something()
{
$dependencyMock = $this->createMock(MyDependency::class);
// Set up your mock here...
$classUnderTest = new LegacyClass($dependencyMock);
$result = $classUnderTest->legacyMethod();
// Assert against the result...
}
Инъекционные зависимости на основе каждого метода
Если по какой-то причине передача зависимостей через конструктор нецелесообразна, например MyDependency
требуются параметры, которые недоступны в момент создания экземпляра LegacyClass
, вы можете следовать тому же шаблону, чтобы вместо этого ввести зависимость в вызов метода.
class LegacyClass
{
public function legacyMethod(?MyDependency $dep = null)
{
$dep = $dep ?: new MyDependency();
// do something...
}
}
О боже, это еще хуже!
Если вы посмотрите на якобы лучший код выше и отшатнетесь в ужасе, позвольте мне процитировать один из моих любимых отрывков из книги “эффективная работа с устаревшим кодом” пера Micheal C. Feathers
Когда вы ломаете зависимости в устаревшем коде, вам часто приходится немного притупить свое эстетическое чувство. Некоторые зависимости ломаются чисто; другие в конечном итоге выглядят менее идеальными с точки зрения дизайна. Они подобны точкам разреза в хирургии: в вашем коде может остаться шрам после работы, но все, что под ним, может стать лучше. Если позже вы сможете покрыть код вокруг точки, где вы нарушили зависимости, вы также сможете залечить этот шрам.
Micheal C. Feathers Working Effectively with Legacy Code
Помните, что это не конец процесса рефакторинга, а только самый первый шаг. Для того, чтобы сделать любой вид рефакторинга, вам абсолютно необходимы тесты на месте, чтобы убедиться, что ваш рефакторинг ничего не сломал. Затем, когда вы позже захотите сделать правильную инъекцию зависимостей, этот класс уже будет готов к ней.
Счастливого рефакторинга!
Оригинал https://www.kai-sassnowski.com/post/nullable-constructor-parameters/
Перевод Максим Гречушников