Квест → Как хакнуть форму
Прошли: 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’!