Кицьки: Рефакторинг. Частина третя або причісуємо шорсткості

imageпершою і другий частинах серії статей ми виконали трохи роботи по розділенню того коду і тих зайвих дій, які ми понаписували в одній функції. В основному ж ми мали справу з класами HttpClient і Cache, і їх різними реалізаціями, щоб написати досліджуваний клієнт для апі кицьок.

Подання даних

До цього ми звертали багато уваги на поведінку і загальну структуру коду, але забіывали про на дані, з якими маємо справу. Зараз у нас все є рядками, включаючи обчислене значення CatApi::getRandomImage(). Тобто викликаючи цей метод, ми «знаємо», що отримаємо рядок. Кажу «знаємо», так як PHP може повернути всі — об'єкт, ресурс, масив і т. д. Тим не менш, хоч у випадку з RealCatApi::getRandomImage() ми можемо бути впевнені, що нам прийде рядок, так як ми наводимо значення до неї, ми не можемо точно сказати, що цей рядок буде «корисна» (валидна) для того, хто викликав цей метод: це може бути і порожній рядок, рядок, яка не містить URL (типу «I am not a URL») і так далі.

class RealCatApi implements CatAPi
{
...

/**
* @return string URL of a random image
*/
public function getRandomImage()
{
try {
$responseXml = ...;
} catch (HttpRequestFailed $exception) {
...
}

$responseElement = new \SimpleXMLElement($responseXml);

return (string) $responseElement->data->images[0]->image->url;
}
}

Щоб зробити наш код правильніше і надійніше, хорошим рішенням буде зробити так, щоб ми були впевнені в тому, що повертаємо коректне значення.

Перше, що ми можемо зробити — перевірити пост-умови нашого методу:

$responseElement = new \SimpleXMLElement($responseXml);

$url = (string) $responseElement->data->images[0]->image->url;

if (filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new \RuntimeException('The Cat Api did not return a valid URL');
}

return $url;

Хоч і правильно, але може погано читатися. Буде ще гірше, якщо з'явиться ще кілька функцій, для яких також потрібні подібні перевірки. Потрібно якось переиспользовать цю логіку валідації. У будь-якому випадку повертається значення зараз все ще залишається такою ж марною рядком і буде круто, якщо ми переконаємо всіх, що це дійсно URL. У такому разі будь-яка частина програми, яка використовує метод CatApi::getRandomImage(), буде точно знати, що це URL, в ньому немає помилок і взагалі це не email який-небудь.

Об'єкт-значення для URL

Замість написання пост-умов для реалізацій CatApi::getRandomImage(), ми можемо написати перед-умови для URL наших картинок. Як ми переконаємося, що URL картинки може існувати лише в тому випадку, коли він валиден? Правильно, створимо ще один об'єкт і переконаємося, що він не створюється з використанням чого-небудь, що не є валідним адресою:

class Url
{
private $url;

public function __construct($url)
{
if (!is_string($url)) {
throw new \InvalidArgumentException('URL was expected to be a string');
}

if (filter_var($url, FILTER_VALIDATE_URL) === false) {
throw new \RuntimeException('The provided URL is invalid');
}

$this->url = $url;
}
}

Такий тип об'єкта називається об'єкт-значення (value-object).

Такий об'єкт неможливо створити неправильним:

new Url('I am not a valid URL');

// RuntimeException: "The provided URL is invalid"

Тепер будь-який об'єкт класу URL точно представляє валідний адресу, іншого бути не може. Можемо знову поміняти код нашої функції, щоб повертати новий об'єкт:

$responseElement = new \SimpleXMLElement($responseXml);

$url = (string) $responseElement->data->images[0]->image->url;

return new Url($url);

Зазвичай, у value-об'єктів є методи для їх створення з примітивних типів і конвертування в них, щоб була можливість підготувати їх для збереження/завантаження, або ж створення з інших value-об'єктів. Для цього випадку нам знадобляться методи fromString() і __toString(). Також, ці методи приводять до того, що з'являється можливість реалізації методів паралельного створення (таких як fromParts($scheme, $host, $path, ...)) і специлаьных геттеров (host(), isSecure()..). Звичайно, не варто писати ці методи до того, як вони дійсно знадобляться.

class Url
{
private $url;

private function __construct($url)
{
$this->url = $url;
}

public static function fromString($url)
{
if (!is_string($url)) {
...
}

...

return new self($url);
}

public function __toString()
{
return $this->url;
}
}

Тепер потрібно змінити метод getRandomImage() і переконатися, що значення для картинки за замовчуванням також повертається URL об'єкт:

class RealCatApi implements CatAPi
{
...

/**
* @return Url URL of a random image
*/
public function getRandomImage()
{
try {
$responseXml = ...;
} catch (HttpRequestFailed $exception) {
return Url::fromString('http://cdn.my-cool-website.com/default.jpg');
}

$responseElement = new \SimpleXMLElement($responseXml);

return Url::fromString((string) $responseElement->data->images[0]->image->url);
}
}

Природно, подібні зміни відіб'ються в інтерфейсі Cache і будь-якому класі, який реалізує його (FileCache, наприклад) — тому потрібно щоб той приймав і повертав URL об'єкти:

class FileCache implements Cache
{
...

public function put(Url $url)
{
file_put_contents($this->cacheFilePath, (string) $url);
}

public function get()
{
return Url::fromString(file_get_contents($this->cacheFilePath));
}
}

Розбираємо XML відповідь

Залишається змінити ось цю частину коду:

$responseElement = new \SimpleXMLElement($responseXml);

$url = (string) $responseElement->data->images[0]->image->url;

Чесно кажучи, я сам не люблю SimpleXML, але проблема тут не в ньому. Проблема тут в тому, що ми завжди очікуємо, що отримаємо валідний відповідь, що містить кореневий елемент, що містить один елемент з назвою data, який містить як мінімум один елемент images, що містить один елемент image, у якого є один елемент url, значення якого, як нам здається, рядок URL адресу. В будь-якій точці ланцюжка, припущення може бути помилковим і це призведе до помилки.

Наше завдання на даний момент — обробляти ці помилки, замість того, щоб PHP кидав виключення. Для цього визначимо своє виключення, яке потім будемо ловити. І знову ми повинні приховати всі деталі про те, які назви елементів та ієрархія існує XML відповіді. Такий об'єкт, також, повинен обробити будь-які винятки. Першим кроком буде введення простого DTO (data transfer object), що представляє сутність зображення від апі кицьок.

Image class
{
private $url;

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

/**
* @return string
*/
public function url()
{
return $this->url;
}
}

Видно, що цей DTO приховує такі поняття як, і інші елементи з відповіді. Ми зацікавлені лише в URL-адресі, тому ми робимо можливим доступ до нього через простий геттер url()

Тепер код в getRandomImage() виглядає якось так:

$responseElement = new \SimpleXMLElement($responseXml);
$image = new Image((string) $responseElement->data->images[0]->image->url);

$url = $image->url();

Як видно, це поки не дуже допомагає, так як ми все ще залежимо від цього ланцюжка елементів XML.

Замість створення DTO безпосередньо, краще буде зробити це через фабрику, яка буде знати про те, яка структура буде у XML відповіді.

class ImageFromXmlResponseFactory
{
public function fromResponse($response)
{
$responseElement = new \SimpleXMLElement($response);

$url = (string) $responseElement->data->images[0]->image->url;

return new Image($url);
}
}

Залишається лише впровадити інстанси ImageFromXmlResponseFactory в клас RealCatApi, це скоротить код в методі RealCatApi::getRandomImage() до такого стану:

$image = $this->imageFactory->fromResponse($responseXml);

$url = $image->url();

return Url::fromString($url);

Винесення коду подібним чином дає можливість писати деякі речі кращим чином. Маленькі класи простіше тестувати. Як я зазначав раніше, наші тести все ще тестують, в основному, «ідеальний сценарій» і більшість граничних випадків не вкрите тестами. Ось, наприклад, що може статися.

  1. Порожній відповідь від сервера
  2. Битий XML
  3. Валідний XML, структура якого змінилася
  4. ...
Виносячи логіку обробки XML в окремі класи, ви зможете зосередитися тільки на тих речах, які відповідають саме за цю задачу. І це дає можливість використовувати реальний TDD, в якому ви визначаєте ситуацію (як в списку вище) та очікувані результати.

Висновок

На цьому, серія «Refactoring the Cat API client» (Кицьки: Рефакторинг) підходить до кінця і, сподіваюся, вона вам сподобалася. Якщо у вас є пропозиції по поліпшенню радості просимо в коментарі до посту. Удачі!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.