Квест → Как хакнуть форму
Прошли: 77
Создание и внедрение интерфейсов в наш код — это важно. Это помогает с подменой компонентов, облегчает тестирование, отделяет "что" от "как".
Но просто влепить интерфейс к классу и забыть — этого недостаточно.
Мы также должны подумать о том, на что мы "натянем" этот интерфейс.
Допустим, мы создаем систему очередей для чтения RSS канала. И нам нужно ставить в очередь URL-адреса. В зависимости от наших потребностей, в качестве механизма очередей мы можем использовать что-то вроде в RabbitMq или базы данных.
Мы ещё не решили точно, но в любом случае мы начнем с интерфейса для этой воображаемой очереди:
declare(strict_types=1); namespace Example\Infrastructure\Queue; use Example\Domain\Rss\FeedUrl; interface FeedUrlQueue { public function add(FeedUrl $feedUrl); }
Имея такой миленький интерфейс, мы можем разработать и покрыть тестами ту часть кода, которая будет использовать реализацию этого интерфейса.
Через некоторое время мы решили, что для начала мы будем использовать базу данных в качестве механизма очередей. Соответственно, мы создаем реализацию FeedUrlQueue
интерфейса:
declare(strict_types=1); namespace Example\Infrastructure\Storage\Database; use Example\Domain\Rss\FeedUrl; class FeedUrlTable extends AbstractTable implements FeedUrlQueue { public function add(FeedUrl $feedUrl) { $qb = $this->getQueryBuilder(); $query = $qb->insert('feed_urls') ->values( [ 'url' => '?', ] ) ->setParameter(0, (string) $feedUrl); $query->execute(); } }
Отлично! У нас есть интерфейс, конкретная реализация, есть возможность писать новые реализации и подменять ими текущую, как говорится, малой кровью.
Хорошая работа.
А действительно ли она хорошо сделана?
Конечно, и я повторюсь: у нас есть интерфейс, конкретная реализация, есть возможность писать новые реализации и легко подменять ими старую.
Для меня здесь есть три вещи, которые выбиваются, говоря мне, что что-то не так с этим кодом.
Во-первых, класс, который представляет собой Table
, также является FeedUrlQueue
. Тут действительно не должно быть двух вещей одновременно. Он либо должен быть очередью, либо таблицей и, безусловно, не тем и другим одновременно.
Во-вторых, класс, чья единственная ответственность должна быть сохранением URL-а в базе, – независимо от того, откуда этот URL приходит, – теперь ограничен сохранением URL-а RSS канала, который приходит из очереди. Ок, возможно (а может нет) это то ограничение, которое мы и хотим применить.
И в-третьих, он также несет ответственность за решение как преобразовать доменный объект FeedUrl
в строку, которая можно сохранить в базе. Есть ли у него магический метод __toString
, с помощью которого мы можем преобразовать его в строку? Или, может, это устаревший код с одним из тех методов toString()
, которые нужно вызвать самим? Мы не узнаем, не взглянув.
Лучше было бы реализовать что-то вроде DatabaseFeedUrlQueue
, который реализует FeedUrlQueue
и использует FeedUrlTable
:
declare(strict_types=1); namespace Example\Infrastructure\Queue; use Example\Domain\Rss\FeedUrl; class DatabaseFeedUrlQueue implements FeedUrlQueue { protected $table; public function __construct(FeedUrlTable $table) { $this->table = $table; } public function add(FeedUrl $feedUrl) { $payload = [ 'url' => (string) $feedUrl ]; $this->table->save($payload); } }
а FeedUrlTable
станет чем-то вроде этого:
declare(strict_types=1); namespace Example\Infrastructure\Storage\Database; class FeedUrlTable extends AbstractTable { public function save(array $payload) { $qb = $this->getQueryBuilder(); $query = $qb->insert('feed_urls') ->values( [ 'url' => '?', ] ) ->setParameter(0, $payload['url']); $query->execute(); } }
С помощью такого рефакторинга кода, как этот, мы в значительной степени исправили все три проблемы сразу:
DatabaseFeedUrlQueue
- это FeedUrlQueue
, а FeedUrlTable
теперь не является двумя сущностями одновременно;DatabaseFeedUrlQueue
отвечает за создание хранимых данных, а FeedUrlTable
обязан сохранять их;Да, теперь у нас есть еще один класс, который нужно поддерживать, но в целом поддержка, я считаю, уменьшается, т. к. сейчас гораздо прозрачнее назначение каждого класса.
Happy hackin’!