Статьи

Практический рефакторинг PHP: метод параметризации

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

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

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

Остерегайтесь этого рефакторинга

Не следует параметризировать метод, когда извлекаемые параметры являются логическими или магическими значениями, которые вызывают необходимость в условных выражениях внутри метода. Идея этого рефакторинга состоит в том, чтобы объединить тела методов, а не помещать их в ветви if () или switch ().

Другой случай, когда я с подозрением отношусь к этому рефакторингу, — это добавленная ценность в семантике нескольких методов . Иногда isSilverClient () и isGoldClient () — это на самом деле две разные вещи, а не просто проверка для более чем M и N ордеров.

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

  • applyTax20Percent (), applyTax4Percent (), applyTax40Percent () очень похожи. Если должен быть один для каждого целого числа от 1 до 100, параметризуйте.
  • countPostsOfAdmins () и countPostsOfUsers () могут выполнять совершенно разные запросы. Не вводите параметры в countPostsOf ( / ** @var boolean / * $ idAdmin) автоматически.

В последнем случае я бы действительно параметризовал метод, если бы он был первым шагом к полиморфизму (countPosts (RankingCriteria $ userCategory)).

меры

  1. Создайте параметризованный метод, который объединяет несколько версий и имеет дополнительные параметры, чтобы различать, что делать. Параметры обычно встраиваются в имена исходных версий и становятся формальными аргументами нового метода (они могут принимать значения по умолчанию). Это не должно влиять на клиентский код на данный момент.
  2. Протестируйте метод самостоятельно . Надеемся, что тесты, которые вам нужно написать, также являются объединением тестовых примеров старых методов.
  3. Измените вызовы , один или несколько одновременно, чтобы настроить таргетинг на новый метод.
  4. Когда вы закончите замену, вы можете удалить старые версии, так как они больше не называются. Их тесты тоже могут пройти, если вы перенесли их на тесты нового метода.

пример

Мы начнем с простого объекта, моделирующего статью этого сайта. Иногда статьи становятся популярными или в верхнем списке (это вымышленные), и поэтому они должны быть выделены при отображении. Для этого в классе Article есть два метода, которые инкапсулируют поле $ views, но допускают тесты популярности:

<?php
class ParameterizeMethod extends PHPUnit_Framework_TestCase
{
    public function testTheArticleIsConsideredPopularAfter1000Views()
    {
        $article = new Article('PPR: Extract Method', 1000);
        $this->assertTrue($article->isPopular());
    }

    public function testTheArticleIsConsideredInTheTopRankAfter10000Views()
    {
        $article = new Article('How to be a worse programmer', 10000);
        $this->assertTrue($article->isTop());
    }
}

class Article
{
    private $title;
    private $views;

    public function __construct($title, $views)
    {
        $this->title = $title;
        $this->views = $views;
    }

    public function isPopular()
    {
        return $this->views >= 1000;
    }

    public function isTop()
    {
        return $this->views >= 10000;
    }
}

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

<?php
class ParameterizeMethod extends PHPUnit_Framework_TestCase
{
    public function testTheArticleIsConsideredPopularAfter1000Views()
    {
        $article = new Article('PPR: Extract Method', 1000);
        $this->assertTrue($article->isPopular());
    }

    public function testTheArticleIsConsideredInTheTopRankAfter10000Views()
    {
        $article = new Article('How to be a worse programmer', 10000);
        $this->assertTrue($article->isTop());
    }

    public function testPopularityIsDecidedByAViewsParameter()
    {
        $article = new Article('How to be a worse programmer', 10000);
        $this->assertTrue($article->isEnoughPopular(10000));
        $this->assertFalse($article->isEnoughPopular(10001));
    }
}

Сейчас мы реализуем единый метод. Реализация может быть скопирована другими версиями, но вместо константы необходимо ввести параметр.

class Article
{
    private $title;
    private $views;

    public function __construct($title, $views)
    {
        $this->title = $title;
        $this->views = $views;
    }

    public function isEnoughPopular($minimumViews)
    {
        return $this->views >= $minimumViews;
    }

    public function isPopular()
    {
        return $this->views >= 1000;
    }

    public function isTop()
    {
        return $this->views >= 10000;
    }
}

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

Я бы, вероятно, удалил эти юнит-тесты, так как новый код хорошо освещен. Теперь мы оставляем их на месте, чтобы показать, как обращаться с существующим клиентским кодом.

<?php
class ParameterizeMethod extends PHPUnit_Framework_TestCase
{
    public function testTheArticleIsConsideredPopularAfter1000Views()
    {
        $article = new Article('PPR: Extract Method', 1000);
        $this->assertTrue($article->isEnoughPopular(Article::POPULAR));
    }

    public function testTheArticleIsConsideredInTheTopRankAfter10000Views()
    {
        $article = new Article('How to be a worse programmer', 10000);
        $this->assertTrue($article->isEnoughPopular(Article::TOP));
    }

    public function testPopularityIsDecidedByAViewsParameter()
    {
        $article = new Article('How to be a worse programmer', 10000);
        $this->assertTrue($article->isEnoughPopular(10000));
        $this->assertFalse($article->isEnoughPopular(10001));
    }
}

class Article
{
    private $title;
    private $views;
    const POPULAR = 1000;
    const TOP = 10000;

    public function __construct($title, $views)
    {
        $this->title = $title;
        $this->views = $views;
    }

    public function isEnoughPopular($minimumViews)
    {
        return $this->views >= $minimumViews;
    }

    public function isPopular()
    {
        return $this->views >= 1000;
    }

    public function isTop()
    {
        return $this->views >= 10000;
    }
}

Finally, we eliminate the old versions, which are not called nor covered by tests anymore.

class Article
{
    private $title;
    private $views;
    const POPULAR = 1000;
    const TOP = 10000;

    public function __construct($title, $views)
    {
        $this->title = $title;
        $this->views = $views;
    }

    public function isEnoughPopular($minimumViews)
    {
        return $this->views >= $minimumViews;
    }
}