Абстрактная фабрика (Abstract Factory) — шаблон проектирования

17 января 2020
Шаблон проектирования предоставляет механизм для создания семейств связанных объектов без указания их конкретных классов. Паттерн действует как фабрика фабрик, где главная фабрика создает другие фабрики, которые, в свою очередь, создают конкретные продукты (объекты). Это позволяет в клиентском коде легко переключаться между группами связанных объектов без крупных изменений кода.

Отличие шаблона абстрактной фабрики от фабричного метода заключается в том, что фабричный метод нацелен на создании объекта продукта (фраза «продукт» используется в понятии результата работы фабрики, то есть то, что она выпускает). Абстрактная фабрика же располагается выше в архитектурной иерархии, концентрируясь на создании и проработке логики работы однотипных фабрик. Кроме того, абстрактная фабрика, как правило, использует фабричный метод как механизм создания объектов продукции. Поэтому часто можно увидеть использование этих двух шаблонов вместе.

Пример задачи: реализация тем оформления приборной панели электромобиля.

Ниже приведён пример использования шаблона на языке PHP для реализации приборной панели автомобиля. 

Панель состоит из датчика уровня заряда батареи, датчика скорости (спидометра), датчика температуры воздуха за бортом и часов. Каждый датчик реализуется отдельным классом, объекты которых затем отрисовываются на мониторе электромобиля. В настройках монитора можно выбирать темы оформления приборной панели. По умолчанию доступны две темы: стандартная и гоночная. В гоночной теме оформления выводятся только спидометр и часы, причём шрифт у спидометра установлен крупным размером.

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

Код решения на языке PHP. Скачать исходники (ZIP, 5 Кб)

abstract_sensors.php

Содержит абстрактные классы датчиков — продукции фабрик:
  • AMSensor — абстрактный класс типового датчика;
  • AMSensorBatteryLevel — абстрактный класс датчика уровня заряда батареи;
  • AMSensorSpeedometer — абстрактный класс спидометра;
  • AMSensorThermometer — абстрактный класс термометра;
  • AMSensorClock — абстрактный класс часов.
<?
/**
 * Абстрактный класс датчика
 */
abstract class AMSensor
{
    /**
     * Флаг необходимости показа датчика на мониторе
     * @var bool
     */
    public const show = true;

    /**
     * Название датчика
     * @var string
     */
    public const name = 'Название датчика';

    /**
     * Размер шрифта
     * @var int
     */
    public const fontSize = 12;

    /**
     * Цвет шрифта
     * @var string
     */
    public const fontColor = '#000000';

    /**
     * Цвет фона
     * @var string
     */
    public const backgroundColor = '#ffffff';

    /**
     * Минимальное значение датчика
     * @var int|string
     */
    public const valueMin = 0;

    /**
     * Максимальное значение датчика
     * @var int|string
     */
    public const valueMax = 0;

    /**
     * Текущее значение датчика
     * @var int|string
     */
    private $value = 0;

    /**
     * Устанавливает текущее значение датчика
     * @param int|string $value
     */
    public function setValue($value) {
        $this->value = $value;
    }

    /**
     * Возвращает текущее значение датчика
     * @return int|string
     */
    public function getValue() {
        return $this->value;
    }
    
    /**
     * Отрисовывает датчик на мониторе
     * @param AutoMonitor $monitor - объект монитора
     * @param string $logs - строка с журналом логов. Новые записи добавляются путём присоединения к строке
     */
    public function draw(&$monitor, &$logs) {
        if ($this::show == true) {
            //$monitor->add($this); //в рамках теста отрисовку не вызываю

            $logs .= "Датчик «".$this::name."» выведен на экран (".get_class($this).")\n";
        }
    }
}

/**
 * Абстрактный класс датчика уровня заряда аккумулятора
 */
abstract class AMSensorBatteryLevel extends AMSensor
{
    public const name = 'Заряд аккумулятора';
    public const valueMin = 0;
    public const valueMax = 100;
}

/**
 * Абстрактный класс датчика скорости (спидометра)
 */
abstract class AMSensorSpeedometer extends AMSensor
{
    public const name = 'Скорость';
    public const valueMin = 0;
    public const valueMax = 180;
}

/**
 * Абстрактный класс датчика температуры воздуха за бортом (термометр)
 */
abstract class AMSensorThermometer extends AMSensor
{
    public const name = 'Датчик температуры';
    public const valueMin = -60;
    public const valueMax = 100;
}

/**
 * Абстрактный класс датчика времени (часы)
 */
abstract class AMSensorClock extends AMSensor
{
    public const name = 'Время';
    public const valueMin = '-';
    public const valueMax = '-';
}

?>

abstract_theme.php

Содержит абстрактный класс AbstractMonitorTheme для темы оформления приборной панели электромобиля. В данном случае абстрактный класс является главной фабрикой. Обратите внимание, что внутри абстрактной фабрики объявлены абстрактные фабричные методы по созданию объектов-датчиков:
  • createSensorBatteryLevel — создаёт и возвращает датчик уровня заряда аккумулятора;
  • createSensorSpeedometer — создаёт и возвращает спидометр;
  • createSensorThermometer — создаёт и возвращает термометр;
  • createSensorClock — создаёт и возвращает часы.
<?
//Подключение абстрактных классов датчиков
require_once(__DIR__.'/abstract_sensors.php');

/**
 * Абстрактный класс с темой оформления монитора
 */
abstract class AbstractMonitorTheme 
{
    public const name = 'Название темы';

    /**
     * Создаёт и возвращает датчик уровня заряда аккумулятора
     * @return AMSensorBatteryLevel
     */
    abstract public function createSensorBatteryLevel(): AMSensorBatteryLevel;

    /**
     * Создаёт и возвращает датчик скорости (спидометр)
     * @return AMSensorSpeedometer
     */
    abstract public function createSensorSpeedometer(): AMSensorSpeedometer;

    /**
     * Создаёт и возвращает датчик температуры воздуха за бортом (термометр)
     * @return AMSensorThermometer
     */
    abstract public function createSensorThermometer(): AMSensorThermometer;

    /**
     * Создаёт и возвращает датчик времени (часы)
     * @return AMSensorClock
     */
    abstract public function createSensorClock(): AMSensorClock;
}
?>

standard_theme.php

Содержит реализацию темы оформления «Стандартная» в виде класса StandardMonitorTheme (представляет собой конкретную фабрику), являющимся наследником абстрактного класса AbstractMonitorTheme. Так же в файле содержатся классы со стилизованной под тему реализацией датчиков (продуктов фабрики):
  • SensorBatteryLevelStandard — класс датчика уровня заряда аккумулятора, стилизованный под тему «Стандартная»;
  • SensorSpeedometerStandard — класс спидометра, стилизованный под тему «Стандартная»;
  • SensorThermometerStandard — класс термометра, стилизованный под тему «Стандартная»;
  • SensorClockStandard — класс часов, стилизованный под тему «Стандартная».

В конце файла происходит создание объекта темы $monitorTheme = new StandardMonitorTheme();
<?
//Подключение абстрактного класса темы
require_once(__DIR__.'/abstract_theme.php');

/**
 * Стандартная тема оформления
 */
class StandardMonitorTheme extends AbstractMonitorTheme
{
    public const name = 'Стандартная';

    /**
     * Создаёт и возвращает датчик уровня заряда аккумулятора
     * @return AMSensorBatteryLevel
     */
    public function createSensorBatteryLevel(): AMSensorBatteryLevel
    {
        return new SensorBatteryLevelStandard();
    }

    /**
     * Создаёт и возвращает датчик скорости (спидометр)
     * @return AMSensorSpeedometer
     */
    public function createSensorSpeedometer(): AMSensorSpeedometer
    {
        return new SensorSpeedometerStandard();
    }

    /**
     * Создаёт и возвращает датчик температуры воздуха за бортом (термометр)
     * @return AMSensorThermometer
     */
    public function createSensorThermometer(): AMSensorThermometer
    {
        return new SensorThermometerStandard();
    }

    /**
     * Создаёт и возвращает датчик времени (часы)
     * @return AMSensorClock
     */
    public function createSensorClock(): AMSensorClock
    {
        return new SensorClockStandard();
    }
}

/**
 * Стандартный датчик уровня заряда аккумулятора
 */
class SensorBatteryLevelStandard extends AMSensorBatteryLevel
{
    public const fontSize = 10;
    public const fontColor = 'green';
    public const backgroundColor = 'black';
}

/**
 * Стандартный датчик скорости
 */
class SensorSpeedometerStandard extends AMSensorSpeedometer
{
    public const fontSize = 14;
}

/**
 * Стандартный датчик температуры
 */
class SensorThermometerStandard extends AMSensorThermometer
{
    public const fontSize = 10;
}

/**
 * Стандартные часы
 */
class SensorClockStandard extends AMSensorClock
{
    public const fontSize = 10;
}

//Создаём объект темы
$monitorTheme = new StandardMonitorTheme();
?>

racer_theme.php

Содержит реализацию темы оформления «Гоночная» в виде класса RacerMonitorTheme, тоже являющимся наследником абстрактного класса AbstractMonitorTheme. По аналогии, в файле содержатся реализации стилизованных датчиков:
  • SensorBatteryLevelRacer — класс датчика уровня заряда аккумулятора, стилизованный под тему «Гоночная»;
  • SensorSpeedometerRacer — класс спидометра, стилизованный под тему «Гоночная»;
  • SensorThermometerRacer — класс терометра, стилизованный под тему «Гоночная»;
  • SensorClockRacer — класс часов, стилизованный под тему «Гоночная».

Обратите внимание, что у датчиков батареи SensorBatteryLevelRacer и термометра SensorThermometerRacer константа show содержит значение false, то есть эти датчики будут скрыты в теме. А у датчика скорости SensorSpeedometerRacer шрифт увеличен до 20 пунктов. 
В конце файла происходит создание объекта темы $monitorTheme = new RacerMonitorTheme();
<?
//Подключение абстрактного класса темы
require_once(__DIR__.'/abstract_theme.php');

/**
 * Гоночная тема оформления
 */
class RacerMonitorTheme extends AbstractMonitorTheme
{
    public const name = 'Гоночная';

    /**
     * Создаёт и возвращает датчик уровня заряда аккумулятора
     * @return AMSensorBatteryLevel
     */
    public function createSensorBatteryLevel(): AMSensorBatteryLevel
    {
        return new SensorBatteryLevelRacer();
    }

    /**
     * Создаёт и возвращает датчик скорости (спидометр)
     * @return AMSensorSpeedometer
     */
    public function createSensorSpeedometer(): AMSensorSpeedometer
    {
        return new SensorSpeedometerRacer();
    }

    /**
     * Создаёт и возвращает датчик температуры воздуха за бортом (термометр)
     * @return AMSensorThermometer
     */
    public function createSensorThermometer(): AMSensorThermometer
    {
        return new SensorThermometerRacer();
    }

    /**
     * Создаёт и возвращает датчик времени (часы)
     * @return AMSensorClock
     */
    public function createSensorClock(): AMSensorClock
    {
        return new SensorClockRacer();
    }
}

/**
 * Стандартный датчик уровня заряда аккумулятора
 */
class SensorBatteryLevelRacer extends AMSensorBatteryLevel
{
    public const show = false;
}

/**
 * Стандартный датчик скорости
 */
class SensorSpeedometerRacer extends AMSensorSpeedometer
{
    public const fontSize = 20;
}

/**
 * Стандартный датчик температуры
 */
class SensorThermometerRacer extends AMSensorThermometer
{
    public const show = false;
}

/**
 * Стандартные часы
 */
class SensorClockRacer extends AMSensorClock
{
    public const fontSize = 10;
}

//Создаём объект темы
$monitorTheme = new RacerMonitorTheme();
?>

test_theme_standard.php

Клиентский код, содержащий подключение стандартной темы, создание и отрисовку датчиков на мониторе. Кроме того, в консоль выводятся логи действий.
<?
//Подключаем класс темы
require_once(__DIR__.'/standard_theme.php');

$logs = "Тема: ".$monitorTheme::name."\n";

//Создание и вывод уровня заряда аккумулятора
$sensor_BatteryLevel = $monitorTheme->createSensorBatteryLevel();
$sensor_BatteryLevel->draw($display, $logs);

//Создание и вывод спидометра
$sensor_Speedometer = $monitorTheme->createSensorSpeedometer();
$sensor_Speedometer->draw($display, $logs);

//Создание и вывод термометра
$sensor_Thermometer = $monitorTheme->createSensorThermometer();
$sensor_Thermometer->draw($display, $logs);

//Создание и вывод часов
$sensor_SensorClock = $monitorTheme->createSensorClock();
$sensor_SensorClock->draw($display, $logs);

//Вывод в консоль журнала логов
echo $logs;

?>

Содержимое логов стандартной темы
Тема: Стандартная
Датчик «Заряд аккумулятора» выведен на экран (SensorBatteryLevelStandard)
Датчик «Скорость» выведен на экран (SensorSpeedometerStandard)
Датчик «Датчик температуры» выведен на экран (SensorThermometerStandard)
Датчик «Время» выведен на экран (SensorClockStandard)

test_theme_racer.php

Клиентский код, содержащий подключение гоночной темы. Остальное содержимое аналогично файлу test_theme_standard.php
<?
//Подключаем класс темы
require_once(__DIR__.'/racer_theme.php');

$logs = "Тема: ".$monitorTheme::name."\n";

//Создание и вывод уровня заряда аккумулятора
$sensor_BatteryLevel = $monitorTheme->createSensorBatteryLevel();
$sensor_BatteryLevel->draw($display, $logs);

//Создание и вывод спидометра
$sensor_Speedometer = $monitorTheme->createSensorSpeedometer();
$sensor_Speedometer->draw($display, $logs);

//Создание и вывод термометра
$sensor_Thermometer = $monitorTheme->createSensorThermometer();
$sensor_Thermometer->draw($display, $logs);

//Создание и вывод часов
$sensor_SensorClock = $monitorTheme->createSensorClock();
$sensor_SensorClock->draw($display, $logs);

//Вывод в консоль журнала логов
echo $logs;

?>

Содержимое логов гоночной темы
Тема: Гонщик
Датчик «Скорость» выведен на экран (SensorSpeedometerRacer)
Датчик «Время» выведен на экран (SensorClockRacer)

Как видите, паттерн абстрактной фабрики позволяет организовать использование разных тем оформления без значимых изменений клиентского кода. По факту, файлы test_theme_standard.php и test_theme_racer.php различаются только в одной строчке, где выполняется подключение файла темы. Причём потенциальным обработчикам в клиентском коде не нужно знать, какой точный класс темы ему необходимо использовать.