Фасад

Фасад — это структурный паттерн проектирования, который предоставляет простой интерфейс к сложной подсистеме из множества взаимосвязанных классов, не пряча саму подсистему и не лишая клиента возможности работать с ней напрямую в нетиповых случаях.
Проблема
Автосервис проводит полную диагностику автомобиля. Она состоит из нескольких независимых подсистем, каждая — отдельный класс со своей логикой:
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(), придётся заглянуть внутрь него, а не только в код клиента.
Итог
Фасад не добавляет подсистеме новых возможностей и не решает проблему несовместимости интерфейсов — он лишь упрощает самый частый сценарий использования сложной подсистемы, сводя его к одному понятному вызову, и оставляет саму подсистему нетронутой и доступной напрямую для всего, что в этот сценарий не укладывается.