Рубрики
Uncategorized

Шаблон адаптера против Узор моста

Шаблон адаптера и шаблон моста привели к большой путанице. В этом посте мы… Помечено как веб-разработчик, программирование, php, учебник.

Шаблоны для всех нас (Серия из 2 частей)

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

🔌 Шаблон адаптера

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

– Ждать чего? Давайте попробуем это еще раз!

Проблема

Представьте себе одну ленту , которая хочет отображать последние темы из нескольких источников, таких как: Reddit и Новости хакеров. Для этих источников у нас есть два клиента API: |/Api Reddit и Api хакерских новостей . Оба возвращают список тем, но их API-интерфейсы не совпадают.

class RedditApi {
    public function fetchTopicItems(): RedditFeedIterator {
        // Returns a `RedditFeedIterator` that provides `Topic` objects, that hold a `title`, `date` and `url`.
    }
}

class HackerNewsApi {
    public function getTopics(): array {
        // returns an array of ['topic_title' => '...', 'topic_date' => '...', 'topic_url' => '...']
    }
}

Мы не хотим, чтобы наш канал знал о различных реализациях, потому что в будущем мы можем захотеть добавить еще один источник, и это будет означать добавление еще большего количества кода в канал. Вместо этого мы применим шаблон адаптера.

Решение

Шаблон адаптера состоит из этих 4 элементов:

  • 🙋 Клиент: Это класс, который хочет подключаться к нескольким источникам. Это было бы Подача в нашем примере.
  • 📚 Адаптер: Источник, к которому Клиент хочет подключиться. В нашем примере у нас есть два: Api Reddit и Api хакерских новостей .
  • 🎯 Цель: Интерфейс или контракт, определяющий единый API, который Клиент будет подключаться к.
  • 🔌 Адаптер: Класс, который реализует Целевой интерфейс и делегирует Адаптер источнику и форматирует его вывод.

Сначала давайте остановимся на интерфейсе Target ; мы назовем его Интерфейс адаптера темы и у него будет метод getTopics() , который возвращает повторяющийся список тем, где каждая тема представляет собой массив с заголовком , дата и url . Таким образом, это может быть массив массивов или генератор/итератор массивов.

Если вы не знакомы с генераторами или итераторами, пожалуйста, ознакомьтесь с моим Генераторы над массивами пост.

interface TopicAdapterInterface
{
    /**
     * @return iterable Iterable of topic array ['title' => '...', 'date' => '...', 'url' => '...']
     */
    public function getTopics(): iterable;
}

Теперь мы можем создать класс Feed , который использует эти адаптеры. Мы будем перебирать каждый адаптер и выдавать их результаты, так что мы получим единый непрерывный поток тем в качестве Генератора . Это, конечно, не учитывает дату, но для этого примера этого достаточно.

class Feed
{
    /**
     * @param TopicAdapterInterface[] $adapters The adapters.
     */
    public function __construct(public array $adapters) {}

    public function getAllTopics(): iterable
    {
        foreach ($this->adapters as $adapter) {
            yield from $adapter->getTopics();
        }
    }
}

Таким образом, у нас есть Клиент Подача , a Цель Интерфейс адаптера темы и два Адаптеры Api Reddit и Api хакерских новостей . Это означает, что нам не хватает только двух Адаптеры . Сначала мы создадим их, а затем посмотрим, что заставляет их тикать.

Чтобы немного упростить работу с итераторами, я буду использовать функцию iterator_map() из моего пакета doekenorg/итератор-функции .

class RedditTopicAdapter implements TopicAdapterInterface
{
    public function __construct(public RedditApi $reddit_api) {}

    public function getTopics(): iterable
    {
        return iterator_map(
            fn (Topic $topic) => [
                'title' => $topic->getTitle(),
                'date' => $topic->getDate('Y-m-d H:i:s'),
                'url' => $topic->getUrl(),
            ],
            $this->reddit_api->fetchTopicItems(),
        );
    }
}

class HackerNewsTopicAdapter implements TopicAdapterInterface
{
    public function __construct(public HackerNewsApi $hacker_news_api) {}

    public function getTopics(): iterable
    {
        return iterator_map(
            fn (array $topic) => [
                'title' => $topic['topic_title'],
                'date' => \DateTime::createFromFormat('H:i:s Y-m-d', $topic['topic_date'])->format('Y-m-d H:i:s'),
                'url' => $topic['topic_url'],
            ],
            $this->hacker_news_api->getTopics(),
        );
    }
}

Здесь вы можете увидеть два наших адаптера: Адаптер темы Reddit и Адаптер темы Хакерских новостей . Оба этих класса реализуют интерфейс адаптера темы и предоставляют необходимый метод getTopics() . Каждый из них получает свое Адаптер вводится как зависимость и используется для извлечения тем и форматирования их в требуемый массив.

Это означает, что наш Feed теперь может использовать эти адаптеры, вставив их в свой конструктор. Чтобы соединить все это вместе, это выглядело бы примерно так:

$hacker_news_adapter = new HackerNewsAdapter(new HackerNewsApi());
$reddit_adapter = new RedditTopicAdapter(new RedditApi());
$feed = new Feed([$hacker_news_adapter, $reddit_adapter]);

foreach ($feed->getAllTopics() as $topic) {
    var_dump($topic); // arrays of [`title`, `date` and `url`]
}

Преимущества шаблона адаптера

  • 🔄 Вы можете подключить дополнительный адаптер позже, без необходимости менять его Клиент реализацию.
  • 🖖 Только Адаптер должен знать о Адаптер , который обеспечивает разделение задач.
  • 🔬 Код Клиента легко тестируется, поскольку он полагается только на Целевой интерфейс.
  • 📦 При работе с контейнером IoC вы обычно можете получить/пометить все сервисы с помощью определенного интерфейса, что очень упрощает поиск и ввод или автоматическую проводку все Адаптеры в Клиент .

Примеры из реального мира

Шаблон адаптера является одним из наиболее часто используемых шаблонов из-за его расширяемости. Он даже может быть расширен другими пакетами без необходимости изменения исходных пакетов. Вот несколько примеров этого из реального мира.

Адаптеры кэша

Большинство фреймворков имеют систему кэширования, которая имеет единый API для работы с ней, предоставляя адаптеры для различных реализаций, таких как: redis, memcache или кэш файловой системы. Laravel называет эти адаптеры Магазин и вы можете найти эти хранилища в illuminate/cache . Они предоставляют Целевой интерфейс для такого хранилища в репозитории illuminate/contracts .

Адаптеры файловой системы

Еще одна распространенная вещь – запись данных в файлы. Файлы, которые могут находиться в другом месте, например: на FTP-сервере, в папке Dropbox или на Google Диске. Одним из наиболее часто используемых пакетов для записи данных в файлы является thephpleague/flysystem . Эти пакеты предоставляют Адаптер файловой системы интерфейс, который может иметь определенные реализации. И из-за этого целевого интерфейса другие могут создавать сторонние пакеты, которые предоставляют другую файловую систему; например: spatial/flysystem-dropbox от Spatie.

🔀 Рисунок моста

Шаблон моста часто путают с шаблоном адаптера, и на то есть веские причины. Давайте посмотрим, какую проблему пытается решить этот шаблон и чем он отличается от шаблона адаптера.

Проблема

Допустим, у нас есть два редактора: a MarkdownEditor and WysiwygEditor . Оба редактора могут прочитать и отформатировать некоторый файл и обновить исходный код этого файла. Редактор Markdown , очевидно, возвращает текст с уценкой, в то время как редактор Wysiwyg возвращает HTML.

class WysiwygEditor
{
    public function __construct(public string $file_path) {}

    protected function format(): string
    {
        return '

Source

'; // The formatted source. } public function read(): string { return file_get_contents($this->file_path); } public function store(): void { file_put_contents($this->file_path, $this->format()); } } class MarkdownEditor { public function __construct(public string $file_path) {} protected function format(): string { return '# Source'; // The formatted source. } public function read(): string { return file_get_contents($this->file_path); } public function store(): void { file_put_contents($this->file_path, $this->format()); } }

В какой-то момент нам понадобится редактор Markdown и редактор WYSIWYG, который может читать и хранить файлы на FTP-сервере. Мы могли бы создать новый редактор, который расширяет Редактор уценки или редактор Wysiwyg и перезаписывает метод чтения() и сохранения() . Однако это, скорее всего, приведет к большому количеству дублирующегося кода между ними. Вместо этого мы будем использовать шаблон моста.

Решение

Узор моста также состоит из 4 элементов:

  • 🎨 Абстракция: Абстрактный базовый класс , который делегирует некоторые предопределенные функции Разработчику . В нашем примере это будет Редактор аннотаций .
  • 🧑 Искусство Утонченная абстракция: Конкретная реализация абстракции . В нашем примере это будет Редактор уценки и Редактор Wysiwyg .
  • 🖌️ |/Реализовать: Интерфейс, который Абстракция использует для делегирования. В нашем примере это будет интерфейс файловой системы
  • 🖼️ Конкретный исполнитель: Конкретная реализация Исполнителя , которая фактически выполняет работу. В нашем примере это будет Локальная файловая система и файловая система Ftp .

Именно в этот момент я думаю, что одна из вещей, которая затрудняет понимание этой модели, заключается в следующем:

В отличие от шаблона адаптера, где есть фактический Адаптер ; Шаблон моста не имеет Моста . Но не беспокойтесь, мы увидим то, что делает этот Мост достаточно скоро!

Рефакторинг кода

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

abstract class AbstractEditor {
    public function __construct(public string $file_path) {}

    abstract protected function format(): string;

    public function read(): string
    {
        return file_get_contents($this->file_path);
    }

    public function store(): void
    {
        file_put_contents($this->file_path, $this->format());
    }
}

class WysiwygEditor extends AbstractEditor
{
    protected function format(): string
    {
        return '

Source

'; // The formatted source. } } class MarkdownEditor extends AbstractEditor { protected function format(): string { return '# Source'; // The formatted source. } }

В этом рефакторинге мы создали Абстрактный редактор , который теперь содержит весь дублированный код, который был между редакторами, и заставил редакторов расширить эту абстракцию. Таким образом, редакторы или Усовершенствованные абстракции сосредоточены только на том, что они делают лучше всего: форматировании исходного файла.

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

interface FilesystemInterface {
    public function read(string $file_path): string;

    public function store(string $file_path, string $file_contents): void;
}

class LocalFileSystem implements FilesystemInterface {
    public function read(string $file_path): string
    {
        return file_get_contents($file_path);
    }

    public function store(string $file_path, string $file_contents): void
    {
        file_put_contents($file_path, $file_contents);
    }
}

abstract class AbstractEditor {
    public function __construct(private FilesystemInterface $filesystem, private string $file_path) {}

    abstract protected function format(): string;

    public function read(): string
    {
        return $this->filesystem->read($this->file_path);
    }

    public function store(): void
    {
        $this->filesystem->store($this->file_path, $this->format());
    }
}

Итак, вот “Мост” . Это связь между Абстракцией и Разработчиком . Он соединяет один редактор с одной файловой системой. Но теперь они могут различаться независимо друг от друга. Мы можем добавить несколько редакторов, каждый из которых имеет свое собственное форматирование, например ямл , json или csv . И все эти редакторы могут использовать любую файловую систему для чтения и хранения этих файлов.

Итак, теперь мы можем создать файловую систему Ftp , которая считывает и хранит форматированный контент на FTP-сервере.

class FtpFileSystem implements FilesystemInterface {
    public function read(string $file_path): string
    {
        // Imagine the ultimate FTP file reading code here.
    }

    public function store(string $file_path, string $file_contents): void
    {
        // Imagine the ultimate FTP file writing code here.
    }
}

Используя шаблон моста, мы сделали возможным создание 4 различных комбинаций реализации:

// 1. A local markdown file editor
new MardownEditor(new LocalFileSystem(), 'local-file.md')
// 2. An FTP markdown file editor
new MardownEditor(new FtpFileSystem(), 'ftp-file.md')
// 3. A local WYSIWYG file editor
new WysiwygEditor(new LocalFileSystem(), 'local-file.html')
// 4. An FTP WYSIWYG file editor
new WysiwygEditor(new FtpFileSystem(), 'ftp-file.html')

И если бы мы добавили еще один Редактор аннотаций и другой Файловая система у нас было бы 9 возможных комбинаций, при добавлении только 2 классов 🤯 !

Преимущества модели моста

Как мы уже видели, использование шаблона моста имеет некоторые преимущества:

  • 💧 Код более СУХОЙ (не повторяйтесь) , извлекая Абстракцию .
  • 🧱 Он более расширяем за счет создания двух отдельных абстракций, которые могут изменяться независимо.
  • 🔬 Отдельные классы меньше, и поэтому их легче тестировать и понимать.

Сходство с шаблоном адаптера

Еще одна причина, по которой некоторым трудно понять разницу между шаблоном моста и шаблоном адаптера, заключается в том, что соединительная часть “моста” на самом деле выглядит как Адаптер .

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

Этот последний, вероятно, самый запутанный; поскольку Усовершенствованный разработчик на самом деле может быть Адаптер к зависимости или Адаптер , но это не требуется. Усовершенствованный разработчик часто будет самостоятельным классом, в то время как Адаптер всегда будет делегировать. Но эти два понятия действительно не являются взаимоисключающими.

Спасибо за чтение

Надеюсь, вам понравилось читать эту статью! Если да, пожалуйста, оставьте ❤️ или a и подумайте о подписке! Я пишу посты на PHP почти каждую неделю. Вы также можете следить за мной на твиттер для получения дополнительной информации и случайных чаевых.

Шаблоны для всех нас (Серия из 2 частей)

Оригинал: “https://dev.to/doekenorg/adapter-pattern-vs-bridge-pattern-11nd”