Мост (Bridge) — шаблон проектирования

21 января 2020
Шаблон предлагает разделить крупный монолитный класс на класс-сущность (абстракцию) и классы-функциональности (реализации функций). Класс-сущность содержит ссылки на объекты классов-функциональностей, таким образом ссылки выполняют роль моста, связывая сущность и её функциональность. Клиентский код преимущественно работает с объектом сущности. Данный подход позволяет заменить объёмный класс отдельными, более простыми и логически обособленными композициями, а также развивать их с меньшей зависимостью друг от друга. 

Некоторая литература декларирует, что Мост позволяет изменять сущность и её функциональности независимо друг от друга, но это не совсем так и сбивает разработчика с толку. Зависимость снижается, но вовсе не пропадает — как минимум у сущности сохраняется зависимость от интерфейса функциональности. Поэтому нужно быть внимательным, внося изменения в объявления свойств и методов (то есть в интерфейс) класса-функциональности, которые используются сущностью.

Паттерн Мост немного похож на Адаптер, только здесь роль класса-адаптера выполняет класс-сущность и его дочерние классы.

Пример задачи: создание медиаконвертеров.

Ниже приводится реализация паттерна на языке PHP. Скачать исходники (ZIP, 3 Кб)

Общая идея заключается в том, чтобы не писать один монструозный класс конвертера, а выделить управленческий класс, с которым будет работать клиентский код, затем выделить отдельные техническо-функциональные классы для конвертирования в конкретные форматы  (например, для технической конвертации в JPG будет использоваться один класс, для PNG — другой класс) и связать их с управленческим классом. 



converters.classes.php 

Cодержит классы конвертеров:
  • Converter — абстрактный класс конвертера. Содержит свойство mediaContainer, в котором хранится объект (ссылка на объект) медиаконтейнера. Именно это свойство является мостом, связывающим сущность (конвертер) с функциональностью (контейнером, выполняющего конвертацию файла в конкретный формат).

    Также класс содержит абстрактный метод convert($pathFileFrom, $pathFileTo) для выполнения конвертации. Объявив метод, мы определили интерфейс класса, ведь именно с этим интерфейсом предстоит работать пользовательскому коду.
  • ConverterImage — класс для конвертации изображений. Здесь метод convert обрёл определение. Обратите внимание, что процесс конвертирования $this->mediaContainer->convert($pathFileFrom, $pathFileTo) выполняется через объект медиаконтейнера, находящегося в свойстве mediaContainer.
  • ConverterAudio — класс для конвертации аудиофайлов. Метод convert здесь тоже обрёл определение. 
<?
//Подключение классов медиаконтейнеров
require_once(__DIR__.'/media_containers.classes.php');

/**
 * Абстрактный класс конвертера
 */
abstract class Converter
{
    /**
     * Медиаконтейнер. Отвечает за формат выходного файла и выполняет 
     * непосредственное конвертирование из файла-источника.
     * 
     * @var MediaContainer
     */
    protected $mediaContainer;

    /**
     * Массив разрешённых к конвертированию форматов файла-источника. 
     * Форматы файлов указываются в нижнем регистре (пример: 'mp3')
     * @var string[]
     */
    protected const VALID_SOURCE_FILE_FORMATS = array();

    /**
     * Конструктор объекта
     * @param string $formatMediaContainer - формат выходного файла, для которого (формата) существует класс-медиаконтейнер.
     *                                       Например, если указать формат 'JPG', то должен существовать класс MediaContainerJPG.
     *                                       Имя класса выбирается путём соединения строки 'MediaContainer' и указанного формата (значения переменной $formatMediaContainer)
     */
    public function __construct($formatMediaContainer)
    {
        $classMediaContainer = 'MediaContainer'.$formatMediaContainer;
        $this->mediaContainer = new $classMediaContainer();
    }

    /**
     * Выполняет конвертирование файла-источника в выходной файл
     * @param string $pathFileFrom - путь к файлу-источнику
     * @param string $pathFileTo - путь к выходному файлу
     * 
     * @return string[] - вернёт массив со сведениями о результатах конвертации. 
     *                 В случае успеха элемент-ключ 'status' будет содержать значение 'success', 
     *                 а в случае ошибки будет содержать значение 'statusDesc' и в элементе-ключе 'statusDesc' 
     *                 будет содержаться описание ошибки.
     */
    abstract public function convert($pathFileFrom, $pathFileTo);

    /**
     * Проверяет файл-источник на соотвествие разрешённых к конвертированию форматов
     * @param string $pathFile - путь к файлу-источнику
     * @return bool - вернёт 'true' в случае соотвествия и 'false' в противном случае
     */
    public function sourceFileIsValid($pathFile)
    {
        if (count($this::VALID_SOURCE_FILE_FORMATS) == 0) {
            return true;
        }

        $pathFile = trim(mb_strtolower($pathFile, 'utf-8'));
        $fileFormat = end(explode('.', $pathFile));
        if ($fileFormat == '' || (count($this::VALID_SOURCE_FILE_FORMATS) > 0 && in_array($fileFormat, $this::VALID_SOURCE_FILE_FORMATS) == false)) {
            return false;
        } else {
            return true;
        }
    }
}

/**
 * Конвертер картинок
 */
class ConverterImage extends Converter
{
    /**
     * Массив разрешённых к конвертированию форматов файла-источника. 
     * Форматы файлов указываются в нижнем регистре (пример: 'jpg')
     * @var string[]
     */
    protected const VALID_SOURCE_FILE_FORMATS = array(
        'jpg',
        'jpeg',
        'bmp',
        'png'
    );

    /**
     * Выполняет конвертирование файла-источника в выходной файл
     * @param string $pathFileFrom - путь к файлу-источнику
     * @param string $pathFileTo - путь к выходному файлу
     * @return string[]
     */
    public function convert($pathFileFrom, $pathFileTo)
    {
        $return = array(
            'status' => '',
            'statusDesc' => ''
        );

        if ($this->sourceFileIsValid($pathFileFrom) == false) {
            $return['status'] = 'error';
            $return['statusDesc'] = 'Недопустимый формат файла-источника картинки. Допускаются следующие форматы файлов: '.implode(', ', $this::VALID_SOURCE_FILE_FORMATS);
        } else {
            $result = $this->mediaContainer->convert($pathFileFrom, $pathFileTo);
            if ($result == true) {
                $return['status'] = 'success';
                $return['statusDesc'] = $result['statusDesc'];
            } else {
                $return['status'] = 'error';
                $return['statusDesc'] = 'Не удалось выполнить конвертирование файла';
            }
        }

        return $return;
    }
}

/**
 * Конвертер музыкальных файлов
 */
class ConverterAudio extends Converter
{
    /**
     * Массив разрешённых к конвертированию форматов файла-источника. 
     * Форматы файлов указываются в нижнем регистре (пример: 'mp3')
     * @var string[]
     */
    protected const VALID_SOURCE_FILE_FORMATS = array(
        'mp3',
        'wma',
        'ogg'
    );

    /**
     * Выполняет конвертирование файла-источника в выходной файл
     * @param string $pathFileFrom - путь к файлу-источнику
     * @param string $pathFileTo - путь к выходному файлу
     * @return string[]
     */
    public function convert($pathFileFrom, $pathFileTo)
    {
        $return = array(
            'status' => '',
            'statusDesc' => ''
        );

        if ($this->sourceFileIsValid($pathFileFrom) == false) {
            $return['status'] = 'error';
            $return['statusDesc'] = 'Недопустимый формат музыкального файла-источника. Допускаются следующие форматы файлов: '.implode(', ', $this::VALID_SOURCE_FILE_FORMATS);
        } else {
            $result = $this->mediaContainer->convert($pathFileFrom, $pathFileTo);
            if ($result == true) {
                $return['status'] = 'success';
                $return['statusDesc'] = $result['statusDesc'];
            } else {
                $return['status'] = 'error';
                $return['statusDesc'] = 'Не удалось выполнить конвертирование файла';
            }
        }

        return $return;
    }
}

?>

media_containers.classes.php

Содержит классы медиаконтейнеров. Медиаконтейнер отвечает за формат выходного файла и именно он делает непосредственную конвертацию. В нашем примере представлены следующие контейнеры:
  • MediaContainer — абстрактный класс медиаконтейнера. Содержит абстрактный метод convert($pathFileFrom, $pathFileTo), с помощью которого выполняется конвертация.
  • MediaContainerJPG — класс-медиаконтейнер определённым методом convert для конвертации картинки  в формат JPG.
  • MediaContainerPNG — класс-медиаконтейнер с определённым методом convert для конвертации картинки  в формат PNG.
  • MediaContainerMP3 — класс-медиаконтейнер с определённым методом convert для конвертации аудиофайла в формат MP3.
  • MediaContainerOGG — класс-медиаконтейнер с определённым методом convert для конвертации аудиофайла в формат OGG.
<?
/**
 * Абстрактный класс медиаконтейнера
 */
abstract class MediaContainer
{
    /**
     * Выполняет конвертирование файла-источника в выходной файл
     * @param string $pathFileFrom - путь к файлу-источнику
     * @param string $pathFileTo - путь к выходному файлу
     * 
     * @return string[] - вернёт массив со сведениями о результатах конвертации. 
     *                 В случае успеха элемент-ключ 'status' будет содержать значение 'success', 
     *                 а в случае ошибки будет содержать значение 'error' и в элементе-ключе 'statusDesc' 
     *                 будет содержаться описание ошибки.
     */
    abstract public function convert($pathFileFrom, $pathFileTo);
}

/**
 * Контейнер картинок формата JPG
 */
class MediaContainerJPG extends MediaContainer
{
    public function convert($pathFileFrom, $pathFileTo)
    {
        $return = array(
            'status' => '',
            'statusDesc' => ''
        );

        //тут располагается код, выполняющий непосредственное конвертирование файла в JPG

        $return['status'] = 'success';
        $return['statusDesc'] = 'Файл '.$pathFileFrom.' конвертирован в JPG и сохранён по пути '.$pathFileTo;

        return $return;
    }
}

/**
 * Контейнер картинок формата PNG
 */
class MediaContainerPNG extends MediaContainer
{
    public function convert($pathFileFrom, $pathFileTo)
    {
        $return = array(
            'status' => '',
            'statusDesc' => ''
        );

        //тут располагается код, выполняющий непосредственное конвертирование файла в PNG

        $return['status'] = 'success';
        $return['statusDesc'] = 'Файл '.$pathFileFrom.' конвертирован в PNG и сохранён по пути '.$pathFileTo;

        return $return;
    }
}

/**
 * Контейнер музыкальных файлов формата MP3
 */
class MediaContainerMP3 extends MediaContainer
{
    public function convert($pathFileFrom, $pathFileTo)
    {
        $return = array(
            'status' => '',
            'statusDesc' => ''
        );

        //тут располагается код, выполняющий непосредственное конвертирование файла в MP3

        $return['status'] = 'success';
        $return['statusDesc'] = 'Файл '.$pathFileFrom.' конвертирован в MP3 и сохранён по пути '.$pathFileTo;

        return $return;
    }
}

/**
 * Контейнер музыкальных файлов формата OGG
 */
class MediaContainerOGG extends MediaContainer
{
    public function convert($pathFileFrom, $pathFileTo)
    {
        $return = array(
            'status' => '',
            'statusDesc' => ''
        );

        //тут располагается код, выполняющий непосредственное конвертирование файла в OGG

        $return['status'] = 'success';
        $return['statusDesc'] = 'Файл '.$pathFileFrom.' конвертирован в OGG и сохранён по пути '.$pathFileTo;

        return $return;
    }
}


?>

test.php

Скрипт с клиентским кодом, в котором создаются объекты конвертеров и выполняется конвертация файлов. При попытке конвертирования файла mp4 в jpg произойдёт ошибка, ведь данные форматы не являются родственными. 

Обратите внимание, что клиентский код работает исключительно с классами конвертеров (сущностями) и не касается классов медиаконтейнеров (функциональностей), хотя непосредственное конвертирование выполняется в медиаконтейнерах.
<?
//Подключение классов конвертеров
require_once(__DIR__.'/converters.classes.php');

//Создаём JPG-конвертер
$jpgConverter = new ConverterImage('JPG');

/* 
Тут должна возникнуть ошибка конвертирования, т.к в источнике указан файл mp4, 
а такой формат недопустим для конвертации в jpg
*/
$result = $jpgConverter->convert('/images_from/1.mp4', '/images_to/1.jpg');
echo '<pre>'.print_r($result, true).'</pre>';

//Конвертируем файл в JPG
$result = $jpgConverter->convert('/images_from/1.png', '/images_to/1.jpg');
echo '<pre>'.print_r($result, true).'</pre>';

//Создаём PNG-конвертер и конвертируем файл
$pngConverter = new ConverterImage('PNG');
$result = $pngConverter->convert('/images_from/2.bmp', '/images_to/2.png');
echo '<pre>'.print_r($result, true).'</pre>';

//Создаём MP3-конвертер и конвертируем файл
$mp3Converter = new ConverterAudio('MP3');
$result = $mp3Converter->convert('/music_from/1.ogg', '/music_to/1.mp3');
echo '<pre>'.print_r($result, true).'</pre>';

//Создаём OGG-конвертер и конвертируем файл
$oggConverter = new ConverterAudio('OGG');
$result = $oggConverter->convert('/music_from/1.wma', '/music_to/1.ogg');
echo '<pre>'.print_r($result, true).'</pre>';

?>

Результат работы скрипта test.php
Array
(
    [status] => error
    [statusDesc] => Недопустимый формат файла-источника картинки. Допускаются следующие форматы файлов: jpg, jpeg, bmp, png
)

Array
(
    [status] => success
    [statusDesc] => Файл /images_from/1.png конвертирован в JPG и сохранён по пути /images_to/1.jpg
)

Array
(
    [status] => success
    [statusDesc] => Файл /images_from/2.bmp конвертирован в PNG и сохранён по пути /images_to/2.png
)

Array
(
    [status] => success
    [statusDesc] => Файл /music_from/1.ogg конвертирован в MP3 и сохранён по пути /music_to/1.mp3
)

Array
(
    [status] => success
    [statusDesc] => Файл /music_from/1.wma конвертирован в OGG и сохранён по пути /music_to/1.ogg
)