Даже в языках, где нет конструкций, кроме классов, нет ограничений, которые могут заставить программиста писать объектно-ориентированный код. Во многих случаях простое размещение ряда функций в классах не приводит к разработке.
Преобразовать Процедурный Design к объектам имеют большие преимущества, но он достигает очень большой масштаб (потенциально все приложения).
Что значит объектно-ориентированный?
В 2011 году больше нет оснований для написания процедурного кода в веб-приложении:
- все библиотеки и фреймворки, которые стоит включить, являются объектно-ориентированными, даже часть кода PHP (SPL, но наиболее важно PDO и даже DateTime).
- Все остальные успешные языки в веб-пространстве либо объектно-ориентированные, либо функциональные, либо оба.
- Литература по разработке программного обеспечения основана на объектах и их схемах.
Однако использование классов и расширенных ключевых слов недостаточно для создания объектно-ориентированного дизайна; целые книги написаны на эту тему.
Этот рефакторинг пытается решить типичный случай процедурного проектирования, включенного в объектную модель:
- классы, содержащие поведение и зависящие от многих других.
- тупые классы, являющиеся только контейнером для данных, или худшие примитивные типы без методов вообще.
В процедурном дизайне принято разделять обязанности в этом шаблоне процедуры / записи , но в эти тупые классы можно добавить методы высокого уровня, чтобы инкапсулировать часть данных, которые они содержат, и упростить процедурные классы, использующие их. Это лишь отправная точка к «объектно-ориентированной», но часто упускаемая из виду.
Принцип « Говори, а не спрашивай» обобщает то, что мы хотели бы сделать, в нескольких словах:
Процедурный кодекс получает информацию, затем принимает решения. Объектно-ориентированный код говорит объектам что-то делать. — Алек Шарп
Вместо бесконечной серии вызовов от процедуры к получателям и установщикам мы хотим передавать сообщения даже объектам более низкого уровня.
меры
Предварительный шаг — превратить примитивные структуры данных в объект данных, оборачивая их и предоставляя получатели. Если вы видите переменные, такие как массивы или строки, переданные в коде для рефакторинга, этот шаг необходим для предоставления класса для размещения потенциальных новых методов.
- Вставьте процедурный код в один класс . Этот шаг позволяет нам извлекать код по строкам, отличным от исходных, в остальной части рефакторинга: например, процедурный код часто делится на временные шаги, в то время как объекты могут вместо этого разделять разные части доступных данных.
- Извлечь методы на процедурный класс . Смотрите следующие шаги для подсказок о том, что извлечь.
- Методы, которые имеют один из немых объектов в качестве аргумента, могут быть перемещены на сам объект , исключив его как параметр, но сохранив оставшиеся. Метод 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;
}
}