PHP Profi

Что обеспечивает interface и для чего он нужен Перевод

Создание и внедрение интерфейсов в наш код — это важно. Это помогает с подменой компонентов, облегчает тестирование, отделяет "что" от "как".

Но просто влепить интерфейс к классу и забыть — этого недостаточно.

Мы также должны подумать о том, на что мы "натянем" этот интерфейс.

 

Пример

Допустим, мы создаем систему очередей для чтения 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’!

2017-11-27 оригинал

Последние посты

Комментарии

авторизуйтесь или зарегистрируйтесь, чтобы оставить комментарий