Перейти к содержимому
Фёдор Башинский

Фасад

6 min read
Обложка статьи «Фасад»

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

Проблема

Автосервис проводит полную диагностику автомобиля. Она состоит из нескольких независимых подсистем, каждая — отдельный класс со своей логикой:

class DynoStand
{
    public function measurePower(string $model): int
    {
        // замер мощности двигателя в л.с.
    }
}
 
class ObdScanner
{
    public function readErrorCodes(): array
    {
        // чтение кодов ошибок через OBD-II разъём
    }
}
 
class BrakeTestBench
{
    public function testBrakes(): float
    {
        // эффективность тормозной системы, %
    }
}
 
class TirePressureGauge
{
    public function readPressure(): array
    {
        // давление в шинах по каждому колесу, бар
    }
}
 
class DiagnosticReportBuilder
{
    public function build(array $data): string
    {
        // формирование итогового отчёта
    }
}

Чтобы провести полную диагностику, нужно вызвать все пять подсистем в правильном порядке и собрать результат в отчёт:

class DiagnosticController
{
    public function runFull(string $model): string
    {
        $dyno = new DynoStand();
        $power = $dyno->measurePower($model);
 
        $obd = new ObdScanner();
        $errors = $obd->readErrorCodes();
 
        $brakes = new BrakeTestBench();
        $brakeEfficiency = $brakes->testBrakes();
 
        $gauge = new TirePressureGauge();
        $pressure = $gauge->readPressure();
 
        $builder = new DiagnosticReportBuilder();
 
        return $builder->build([
            'power' => $power,
            'errors' => $errors,
            'brakes' => $brakeEfficiency,
            'pressure' => $pressure,
        ]);
    }
}

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

Решение

Фасад — это класс, который сам знает, как согласованно работать с набором подсистем, и предоставляет клиенту один простой метод вместо необходимости самому оркестровать пять объектов:

class DiagnosticFacade
{
    public function __construct(
        private readonly DynoStand $dyno = new DynoStand(),
        private readonly ObdScanner $obd = new ObdScanner(),
        private readonly BrakeTestBench $brakes = new BrakeTestBench(),
        private readonly TirePressureGauge $gauge = new TirePressureGauge(),
        private readonly DiagnosticReportBuilder $builder = new DiagnosticReportBuilder(),
    ) {}
 
    public function runFullDiagnostic(string $model): string
    {
        return $this->builder->build([
            'power' => $this->dyno->measurePower($model),
            'errors' => $this->obd->readErrorCodes(),
            'brakes' => $this->brakes->testBrakes(),
            'pressure' => $this->gauge->readPressure(),
        ]);
    }
}

Теперь везде, где нужна полная диагностика, достаточно одного вызова:

$facade = new DiagnosticFacade();
 
echo $facade->runFullDiagnostic('Model X');

Веб-контроллер, консольная команда и фоновая задача обращаются к одному и тому же DiagnosticFacade::runFullDiagnostic() и не знают ни про пять подсистем, ни про порядок их вызова. Появится шестая подсистема — достаточно завести её экземпляр в конструкторе фасада и добавить один вызов внутри runFullDiagnostic(), ни один из вызывающих кодов при этом не меняется.

Из чего состоит паттерн

  • Фасад (DiagnosticFacade) — предоставляет клиенту простой интерфейс к сложной подсистеме и знает, как согласованно обращаться к её частям.
  • Подсистема (DynoStand, ObdScanner, BrakeTestBench, TirePressureGauge, DiagnosticReportBuilder) — набор классов, реализующих реальную логику; каждый из них ничего не знает о существовании фасада.
  • Клиент (DiagnosticController и другие) — использует фасад для типового сценария вместо прямой работы с подсистемой.

Фасад не запрещает прямой доступ к подсистеме

В отличие от инкапсуляции, классы подсистемы в примере выше остаются публичными — фасад ничего от клиента не скрывает, он лишь предлагает удобный путь для типового случая. Если консольной команде нужна только проверка тормозов, а не полная диагностика, она может обратиться к BrakeTestBench напрямую, минуя фасад:

$brakes = new BrakeTestBench();
$efficiency = $brakes->testBrakes();

Это осознанное свойство паттерна, а не недоработка: фасад закрывает самый частый сценарий использования одним вызовом, но не пытается предугадать и обернуть вообще все возможные комбинации обращений к подсистеме — для редких и нестандартных случаев подсистема остаётся доступной напрямую.

Фасад vs Адаптер

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

ФасадАдаптер
ЗадачаУпростить работу со сложной подсистемойСделать несовместимый интерфейс совместимым с тем, что ждёт клиент
Сколько классов оборачиваетОбычно несколько, часто разнородныхОбычно один конкретный класс
Можно ли работать в обходДа, подсистема остаётся доступной напрямуюНет — без адаптера объект нельзя использовать там, где его ждут
Откуда берётся новый интерфейсПридумывается ради удобства клиентаПродиктован уже существующим целевым интерфейсом

DiagnosticFacade не решает проблему несовместимости — все пять подсистем и без него прекрасно вызываются напрямую, просто их пять и вызывать их нужно в правильном порядке. Если бы, например, BrakeTestBench был из стороннего SDK с методом runBrakeCheck(): string, возвращающим "92%" вместо float, и его нужно было бы привести к общему виду — это была бы задача для Адаптера, а не Фасада.

Когда применять

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

Плюсы и минусы

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

Минус — если пытаться закрыть фасадом вообще все возможные комбинации обращений к подсистеме, а не только типовой сценарий, он постепенно превращается в разросшийся «божественный объект» с десятками методов, который знает слишком много и сам становится источником связанности. Фасад также добавляет уровень косвенности: чтобы понять, что на самом деле происходит при вызове runFullDiagnostic(), придётся заглянуть внутрь него, а не только в код клиента.

Итог

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