Статьи

Прорыв рефакторинга на CoffeeMachine

Сегодня я напишу о концепции, которую я узнал из Domain-Driven Design, книги Эрика Эванса, посвященной шаблону Domain Model. DDD — это не только сущности, объекты-значения, репозитории, фабрики и службы — это целостный подход к разработке приложения, в котором модель домена является главным приоритетом. Эта методология хорошо адаптируется к очень сложным областям, но мы можем извлечь ценные уроки, которые применимы везде.

Рефакторинг

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

Почему мы используем этот подход? Потому что, когда код свежий, его проще реорганизовать, а когда неинкапсулированная часть разбрасывается в базе кода, он становится более жестким (вот почему мы беспокоимся о инкапсуляции / сокрытии информации). Например, самый простой момент для изменения API это когда никто не использует его, и только класс, который выставляет его и его тесты, знает какие-либо детали. В то время как TDDing, вы пишете тест для класса перед Api, так что вы можете изменить Api еще до того, как он существует.

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

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

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

Прорыв, достижение, открытие

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

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

На затраченные по оси X усилия, на значение по оси Y, полученное путем рефакторинга, в сторону более глубокого понимания. Крошечные, маленькие рефакторинги открывают путь для потрясающих. Мы не говорим ни о крупном рефакторинге, ни о переписывании с нуля: гигантские скачки сделать сложно. Но шаг за шагом, улучшая структуру кода, можно представить шаблон, который мы хотели бы сделать явным, и позволить нам вводить новые объекты и классы без реального серьезного рефакторинга (который, например, изменяет 42 класса за один коммит, и мы не можем зафиксировать ранее, потому что это нарушит сборку.)

Пример Эванса — об управлении кредитами, которые связаны с несколькими кредиторами (вы знаете, сложные системы). После нескольких настроек кода и рефакторингов он представляет интерфейс Share с несколькими реализациями, новый класс, который возникает из модели предметной области, естественно, благодаря предыдущим изменениям в коде.

DDD для сложных систем, но и веб-приложения в настоящее время являются сложными системами. Мы не будем перетасовывать данные из формы в базу данных на страницу. Мы используем ORM, библиотеки и фреймворки. Мы взаимодействуем с веб-сервисами и потоками. Мы читаем и производим файлы. Мы отвечаем на запросы от разных типов клиентов — браузеров, мобильных браузеров и других приложений, и все они могут общаться в простой старой парадигме HTTP или в AJAX. Прорыв рефакторинга может произойти везде.

Примеры

Здесь важно показать какой-то код, иначе я бы никогда не объяснил себя В этом примере показано, как непрерывный рефакторинг правил проверки привел к введению объекта параметра.

Здесь я представляю только ключевые шаги, но весь код находится на github . Я очень часто демонстрировал шаги, которые я предпринял во время TDDing и рефакторинга.

Итак, у нас есть CoffeeMachine, и мы хотим загрузить его расходными материалами. Проблема в том, что кофе поставляется в одной упаковке, а горячий шоколад — в упаковке по 10 штук, и мы можем вставить это только в CoffeeMachine.

Тест после кодирования немного таков:

<?php
require_once 'CoffeeMachine.php';

class CoffeeMachineTest extends PHPUnit_Framework_TestCase
{
private $machine;

public function setUp()
{
$this->machine = new CoffeeMachine(array('Coffee', 'Chocolate'));
}

public function testMachineIsLoadedWithZeroSuppliesAtCreation()
{
$this->assertSuppliesAre(0, 'Coffee');
$this->assertSuppliesAre(0, 'Chocolate');
}

public function testMachineCanBeLoadedWithCoffeeSupplies()
{
$this->machine->loadSupplies('Coffee', 5);

$this->assertSuppliesAre(5, 'Coffee');
$this->assertSuppliesAre(0, 'Chocolate');
}

public function testMachineCanBeLoadedWithChocolateSupplies()
{
$this->machine->loadSupplies('Chocolate');

$this->assertSuppliesAre(0, 'Coffee');
$this->assertSuppliesAre(10, 'Chocolate');
}

private function assertSuppliesAre($number, $beverageName)
{
$this->assertEquals($number, $this->machine->getSupplies($beverageName));
}
}

в то время как производственный код немного уродлив:

<?php
class CoffeeMachine
{
/**
* @array
*/
private $supplies;

public function __construct(array $beverages)
{
$this->supplies = array();
foreach ($beverages as $name) {
$this->supplies[$name] = 0;
}
}

public function loadSupplies($beverageName, $quantity = 10)
{
$this->supplies[$beverageName] += $quantity;
}

public function getSupplies($beverageName)
{
return $this->supplies[$beverageName];
}
}

Чтобы немного улучшить ситуацию, мы добавляем требование, чтобы вы не могли вызывать $ machine-> loadSupplies (‘Coffee’), и использовали параметр 10 по умолчанию в тесте (он предназначен для Chocolate.) Рабочий код отвечает:

<?php
class CoffeeMachine
{
/**
* @array
*/
private $supplies;

public function __construct(array $beverages)
{
$this->supplies = array();
foreach ($beverages as $name) {
$this->supplies[$name] = 0;
}
}

public function loadSupplies($beverageName, $quantity = null)
{
if ($quantity === null) {
if ($beverageName == "Chocolate") {
$quantity = 10;
} else {
throw new InvalidArgumentException("Only Chocolate has a predefined quantity of supplies to load.");
}
}
$this->supplies[$beverageName] += $quantity;
}

public function getSupplies($beverageName)
{
return $this->supplies[$beverageName];
}
}

Не намного лучше. Мы выделяем метод для обработки возможных значений $ количества:

<?php
class CoffeeMachine
{
/**
* @array
*/
private $supplies;

public function __construct(array $beverages)
{
$this->supplies = array();
foreach ($beverages as $name) {
$this->supplies[$name] = 0;
}
}

public function loadSupplies($beverageName, $optionalQuantity = null)
{
$quantity = $this->getSuppliesQuantity($beverageName, $optionalQuantity);

$this->supplies[$beverageName] += $quantity;
}

private function getSuppliesQuantity($beverageName, $quantity)
{
if ($quantity === null) {
if ($beverageName == "Chocolate") {
return 10;
} else {
throw new InvalidArgumentException("Only Chocolate has a predefined quantity of supplies to load.");
}
}
return $quantity;
}

public function getSupplies($beverageName)
{
return $this->supplies[$beverageName];
}
}

Теперь мы встряхиваем вещи и добавляем, что расходные материалы для кофе поставляются в упаковках, в которых количество элементов кратно 5: 5, 10 и 15 — правильные запасы, а 11 или 4 — нет.

<?php
class CoffeeMachine
{
/**
* @array
*/
private $supplies;

public function __construct(array $beverages)
{
$this->supplies = array();
foreach ($beverages as $name) {
$this->supplies[$name] = 0;
}
}

public function loadSupplies($beverageName, $optionalQuantity = null)
{
$quantity = $this->getSuppliesQuantity($beverageName, $optionalQuantity);

$this->supplies[$beverageName] += $quantity;
}

private function getSuppliesQuantity($beverageName, $quantity)
{
if ($quantity === null) {
if ($beverageName == "Chocolate") {
return 10;
} else {
throw new InvalidArgumentException("Only Chocolate has a predefined quantity of supplies to load.");
}
}
if ($beverageName == "Coffee" and $quantity % 5 != 0) {
throw new InvalidArgumentException("Coffee supplies must come in multiples of 5.");
}
return $quantity;
}

public function getSupplies($beverageName)
{
return $this->supplies[$beverageName];
}
}

Метод getSuppliesQuantity () становится длинным. Мы проводим рефакторинг, чтобы разграничить шоколад и кофе:

<?php
class CoffeeMachine
{
/**
* @array
*/
private $supplies;

public function __construct(array $beverages)
{
$this->supplies = array();
foreach ($beverages as $name) {
$this->supplies[$name] = 0;
}
}

public function loadSupplies($beverageName, $optionalQuantity = null)
{
$quantity = $this->getSuppliesQuantity($beverageName, $optionalQuantity);
$this->supplies[$beverageName] += $quantity;
}

private function getSuppliesQuantity($beverageName, $quantity)
{
if ($beverageName == "Chocolate") {
if ($quantity === null) {
return 10;
}
}

if ($beverageName == "Coffee") {
if ($quantity === null) {
throw new InvalidArgumentException("Coffee supplies number must be defined.");
}
if ($quantity % 5 != 0) {
throw new InvalidArgumentException("Coffee supplies must come in multiples of 5.");
}
}
return $quantity;
}

public function getSupplies($beverageName)
{
return $this->supplies[$beverageName];
}
}

Теперь поставки кофе и шоколада хорошо разделены. Здесь происходит прорыв: мы представляем два объекта, которые обрабатывают проверку $ amount:

<?php
class CoffeeSupply implements Supply {
private $quantity;

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

private function checkQuantity($quantity)
{
if ($quantity === null) {
throw new InvalidArgumentException("Coffee supplies number must be defined.");
}
if ($quantity % 5 != 0) {
throw new InvalidArgumentException("Coffee supplies must come in multiples of 5.");
}
return $quantity;
}

public function getBeverageName()
{
return 'Coffee';
}

public function getQuantity()
{
return $this->quantity;
}
}

<?php
class ChocolateSupply implements Supply {
public function getBeverageName()
{
return 'Chocolate';
}

public function getQuantity()
{
return 10;
}
}

Обратите внимание, как просто становится класс CoffeeMachine:

<?php
class CoffeeMachine
{
/**
* @array
*/
private $supplies;

public function __construct(array $beverages)
{
$this->supplies = array();
foreach ($beverages as $name) {
$this->supplies[$name] = 0;
}
}

public function loadSupplies(Supply $supply)
{
$beverageName = $supply->getBeverageName();
$quantity = $supply->getQuantity();
$this->supplies[$beverageName] += $quantity;
}

public function getSupplies($beverageName)
{
return $this->supplies[$beverageName];
}
}

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

Таким образом, если вам интересно, полезно ли извлекать закрытый метод, просто выделите его: в будущем он может оказаться в другом классе. Если вам интересно, имеет ли смысл переупорядочивание параметров нескольких методов для согласованности, подумайте, что вы можете в конечном итоге найти объект Parameter. Не бойтесь небольших изменений, которые приведут вас к прорыву.