Статьи

Практический рефакторинг PHP: преобразование процедурного дизайна в объекты

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

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

Что значит объектно-ориентированный?

В 2011 году больше нет оснований для написания процедурного кода в веб-приложении:

  • все библиотеки и фреймворки, которые стоит включить, являются объектно-ориентированными, даже часть кода PHP (SPL, но наиболее важно PDO и даже DateTime).
  • Все остальные успешные языки в веб-пространстве либо объектно-ориентированные, либо функциональные, либо оба.
  • Литература по разработке программного обеспечения основана на объектах и ​​их схемах.

Однако использование классов и расширенных ключевых слов недостаточно для создания объектно-ориентированного дизайна; целые книги написаны на эту тему.

Этот рефакторинг пытается решить типичный случай процедурного проектирования, включенного в объектную модель:

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

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

Принцип « Говори, а не спрашивай» обобщает то, что мы хотели бы сделать, в нескольких словах:


Процедурный кодекс получает информацию, затем принимает решения.
Объектно-ориентированный код говорит объектам что-то делать. — Алек Шарп

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

меры

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

  1. Вставьте процедурный код в один класс . Этот шаг позволяет нам извлекать код по строкам, отличным от исходных, в остальной части рефакторинга: например, процедурный код часто делится на временные шаги, в то время как объекты могут вместо этого разделять разные части доступных данных.
  2. Извлечь методы на процедурный класс . Смотрите следующие шаги для подсказок о том, что извлечь.
  3. Методы, которые имеют один из немых объектов в качестве аргумента, могут быть перемещены на сам объект , исключив его как параметр, но сохранив оставшиеся. Метод Move должен освободить класс-гигант от любых несвязанных с ним обязанностей.

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

пример

Один из моих популярных примеров — это расчет счетов-фактур: вычисление полей, таких как общая цена и налоги, из серии информации.

В этой процедурной схеме у нас есть один счет и несколько строк, смоделированных с помощью Primitive Obsession (в виде массивов).

<?php
class ConvertProceduralDesignToObjects extends PHPUnit_Framework_TestCase
{
    public function testPricesAreSummedAfterAPercentageBasedTaxIsApplied()
    {
        $invoice = new Invoice(array(
            array(1000, 4),
            array(1000, 20),
            array(2000, 20),
        ));
        $this->assertEquals(4640, $invoice->total());
    }
}

class Invoice
{
    private $rows;

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

    public function total()
    {
        $total = 0;
        foreach ($this->rows as $row) {
            $rowTotal = $row[0] 
                      + $row[0] * $row[1] / 100;
            $total += $rowTotal;
        }
        return $total;
    }
}

Мы представили класс Row, но теперь дизайн еще хуже: он добавляет несколько строк кода (новый класс) без новой сущности, что дает нам что-то взамен. У объекта Row нет никаких обязанностей, и мы просто должны писать геттеры, а иногда и сеттеры. По крайней мере, мы записываем части нашей модели для документации (присваивая названия чистой цене и номерам налоговых ставок), но мы не уверены, что эта модель является самой универсальной.

<?php
class ConvertProceduralDesignToObjects extends PHPUnit_Framework_TestCase
{
    public function testPricesAreSummedAfterAPercentageBasedTaxIsApplied()
    {
        $invoice = new Invoice(array(
            new Row(1000, 4),
            new Row(1000, 20),
            new Row(2000, 20),
        ));
        $this->assertEquals(4640, $invoice->total());
    }
}

class Invoice
{
    private $rows;

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

    public function total()
    {
        $total = 0;
        foreach ($this->rows as $row) {
            $rowTotal = $row->getNetPrice() 
                      + $row->getTaxRate() * $row->getNetPrice() / 100;
            $total += $rowTotal;
        }
        return $total;
    }
}

class Row
{
    public function __construct($netPrice, $taxRate)
    {
        $this->netPrice = $netPrice;
        $this->taxRate = $taxRate;
    }

    public function getNetPrice()
    {
        return $this->netPrice;
    }

    public function getTaxRate()
    {
        return $this->taxRate;
    }
}

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

class Invoice
{
    private $rows;

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

    public function total()
    {
        $total = 0;
        foreach ($this->rows as $row) {
            $total += $this->rowTotal($row);
        }
        return $total;
    }

    public function rowTotal($row)
    {
        return $row->getNetPrice() 
             + $row->getTaxRate() * $row->getNetPrice() / 100;
    }
}

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

Фактически, поскольку тест все еще проходит, мы можем заметить, что этот метод имеет объект Row в своих аргументах, поэтому он может быть перемещен на Row теперь, когда его логика была четко изолирована:

  • Ссылки $ this-> field должны стать дополнительными параметрами метода перед его перемещением.
  • Другие параметры должны просто оставаться формальными параметрами.
  • Вызовы $ this-> anotherMethod () будет сложнее обработать, поскольку у вас есть возможность перемещать anothetMethod () в классе Row или извлечь интерфейс, содержащий anotherMethod (), и передать $ this.

При перемещении кода мы меняем ссылки на $ row на $ this и проверяем, что область видимости метода public. Мы также переименовываем метод в total () вместо rowTotal ().

{
    private $rows;

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

    public function total()
    {
        $total = 0;
        foreach ($this->rows as $row) {
            $total += $row->total();
        }
        return $total;
    }
}

class Row
{
    public function __construct($netPrice, $taxRate)
    {
        $this->netPrice = $netPrice;
        $this->taxRate = $taxRate;
    }

    public function getNetPrice()
    {
        return $this->netPrice;
    }

    public function getTaxRate()
    {
        return $this->taxRate;
    }

    public function total()
    {
        return $this->getNetPrice() 
             + $this->getTaxRate() * $this->getNetPrice() / 100;
    }
}

Наконец, мы включаем геттеры, так как они не используются извне класса Row. Они будут введены снова в будущем, если в этом будет реальная необходимость: как правило, мы избегаем выставлять любое состояние из строки, в котором нет необходимости.

class Row
{
    public function __construct($netPrice, $taxRate)
    {
        $this->netPrice = $netPrice;
        $this->taxRate = $taxRate;
    }

    public function total()
    {
        return $this->netPrice 
             + $this->taxRate * $this->netPrice / 100;
    }
}