Даже в языках, где нет конструкций, кроме классов, нет ограничений, которые могут заставить программиста писать объектно-ориентированный код. Во многих случаях простое размещение ряда функций в классах не приводит к разработке.
Преобразовать Процедурный 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; } }