Статьи

Практический рефакторинг PHP: интерфейс Extract

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

Extract Interface Рефакторинг создает интерфейс из существующего конкретного класса.

Зачем?

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

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

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

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

меры

  1. Создайте пустой интерфейс . Если вы можете думать только об имени, таком как Set и ISet, для класса и интерфейса, начните с сохранения хорошего имени для интерфейса: Set и TreeBasedSet лучше, чем в предыдущем случае.
  2. Объявите общие операции в интерфейсе с сигнатурами методов, идентичными оригинальным. Если клиент вызывает только некоторые операции, только это подмножество должно быть скопировано в интерфейс.
  3. Добавить реализует ключевые слова, чтобы связать существующие конкретные классы с интерфейсом.
  4. Упростите клиентский код, сделав его зависимым от интерфейса, где это возможно.

Примеров последнего шага несколько:

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

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

  • переименование интерфейса и конкретных классов для отражения роли (в интерфейсе) и особенностей реализации (в конкретных классах).
  • Добавьте или удалите методы в контракте, чтобы соответствовать желаниям звонящего.
  • Извлеките некоторые функциональные возможности в Decorator или Composite , которые являются множественной реализацией интерфейса.

пример

В исходном состоянии объект Money Presenter зависит от конкретного класса EuroLocale.

<?php
class ExtractInterface extends PHPUnit_Framework_TestCase
{
    public function testShouldDisplayAMoneyAmount()
    {
        $locale = new EuroLocale();
        $money = new Money("42");
        $this->assertEquals("42 €", $money->display($locale));
    }
}

class EuroLocale
{
    public function format($amount)
    {
        return $amount . ' €';
    }
}

class Money
{
    private $amount;

    /**
     * @param string $amount    to keep precision
     */
    public function __construct($amount)
    {
        $this->amount = $amount;
    }

    public function display(EuroLocale $locale)
    {
        return $locale->format($this->amount);
    }
}

Мы создаем интерфейс Locale, и извлекается только один публичный метод. Ничего больше не будет показано в этом примере и не будет отображено в интерфейсе.

interface Locale
{
    /**
     * @return string
     */
    public function format($amount);
}

Мы добавим инвентарь ключевого слова, и упростить зависимость типа намека на цель только Locale.

interface Locale
{
    /**
     * @return string
     */
    public function format($amount);
}

class EuroLocale implements Locale
{
    public function format($amount)
    {
        return $amount . ' €';
    }
}

class Money
{
    private $amount;

    /**
     * @param string $amount    to keep precision
     */
    public function __construct($amount)
    {
        $this->amount = $amount;
    }

    public function display(Locale $locale)
    {
        return $locale->format($this->amount);
    }
}

Обычно зависимость от конкретного класса, который может иметь дюжину различных методов, не позволяет нам проводить рефакторинг-тесты, вводящие Test Doubles. Это происходит потому, что мы не уверены, какие методы нам следует переопределить: какие из них вызываются в этом методе тестирования? А денежными объектами вообще?

Теперь, когда у нас есть интерфейс, мы явно определяем, что вызывается только format (), даже если в EuroLocale может быть много других. Таким образом, мы можем разбить тесты на две части: одна нацелена на деньги, а другая — на EuroLocale. Обратите внимание на порядок юнит-тестов: они полностью независимы, поэтому мы можем сначала протестировать (и, следовательно, разработать) деньги.

<?php
class ExtractInterface extends PHPUnit_Framework_TestCase
{
    public function testShouldFormatItsAmountBeforeDisplayingIt()
    {
        $locale = $this->getMock('Locale');
        $locale->expects($this->once())->method('format')->with("42")->will($this->returnValue('42 SIMBOL'));
        $money = new Money("42");
        $this->assertEquals("42 SIMBOL", $money->display($locale));
    }

    public function testShouldFormatAnAmountWithTheEuroSighn()
    {
        $locale = new EuroLocale();
        $this->assertEquals("42 €", $locale->format("42"));
    }
}

В реальном мире были бы десятки тестов с участием обоих объектов, с большим количеством установочного кода и оборудования. Интерфейсы позволяют нам ломать прямые зависимости и тестировать классы в реальной изоляции, подход, который лучше масштабируется для многих тестов. Например, в окончательной версии каждый тест для новых параметров форматирования (например, 42,00 или 10 000,00 EUR) должен только создать строку вместо объекта Money. Эквивалентно, любой тест для нового пользователя format () не должен заботиться о конкретных правилах форматирования.