Facade: простой интерфейс для сложной системы
Facade: простой интерфейс для сложной системы
Хорошая регистрация пользователя - это пять-семь шагов внутри и одна функция снаружи. Facade - про эту одну функцию.
Проблема
Чтобы зарегистрировать пользователя, нужно:
- Валидировать данные
- Создать запись в БД
- Отправить welcome-email
- Записать событие в аналитику
- Создать настройки по умолчанию
Если клиентский код вызывает каждый сервис отдельно:
// в обработчике
if err := validator.Check(email, pass); err != nil { return err }
user, err := userRepo.Create(email, pass)
if err != nil { return err }
_ = mailer.SendWelcome(user.Email)
tracker.Event("register", user.ID)
settingsRepo.CreateDefaults(user.ID)
<?php
declare(strict_types=1);
// в обработчике
$validator->check($email, $pass);
$user = $userRepo->create($email, $pass);
try {
$mailer->sendWelcome($user->email);
} catch (Throwable) {
// некритично
}
$tracker->event('register', $user->id);
$settingsRepo->createDefaults($user->id);
// в обработчике
validator.check(email, pass);
const user = await userRepo.create(email, pass);
try {
await mailer.sendWelcome(user.email);
} catch {
// некритично
}
tracker.event('register', user.id);
await settingsRepo.createDefaults(user.id);
Тот же код повторяется в API-обработчике, в CLI, в тестах. Каждый «клиент» знает все 5 шагов и правильный порядок - клиентский код не следует SRP, потому что вынужден держать в голове чужие детали.
Решение: Facade
Facade - один метод (или структура), который скрывает координацию нескольких подсистем. Клиент вызывает Register() и не думает о деталях.
Фасад регистрации
type RegisterFacade struct {
validator Validator
users UserRepo
mail Mailer
tracker Tracker
settings SettingsRepo
}
func NewRegisterFacade(
v Validator, u UserRepo, m Mailer, t Tracker, s SettingsRepo,
) *RegisterFacade {
return &RegisterFacade{
validator: v, users: u, mail: m, tracker: t, settings: s,
}
}
func (f *RegisterFacade) Register(email, pass string) (*User, error) {
// 1. Валидация
if err := f.validator.Check(email, pass); err != nil {
return nil, fmt.Errorf("validation: %w", err)
}
// 2. Создание пользователя
user, err := f.users.Create(email, pass)
if err != nil {
return nil, fmt.Errorf("create user: %w", err)
}
// 3. Некритичные операции - ошибки не блокируют регистрацию
if err := f.mail.SendWelcome(email); err != nil {
log.Printf("welcome email failed: %v", err)
}
f.tracker.Event("register", user.ID)
f.settings.CreateDefaults(user.ID)
return user, nil
}
Клиентский код прост:
user, err := facade.Register(email, pass)
final class RegisterFacade
{
public function __construct(
private readonly Validator $validator,
private readonly UserRepo $users,
private readonly Mailer $mail,
private readonly Tracker $tracker,
private readonly SettingsRepo $settings,
private readonly LoggerInterface $logger,
) {}
public function register(string $email, string $pass): User
{
// 1. Валидация
$this->validator->check($email, $pass);
// 2. Создание пользователя
$user = $this->users->create($email, $pass);
// 3. Некритичные операции - ошибки не блокируют регистрацию
try {
$this->mail->sendWelcome($email);
} catch (Throwable $e) {
$this->logger->warning('welcome email failed', ['err' => $e->getMessage()]);
}
$this->tracker->event('register', $user->id);
$this->settings->createDefaults($user->id);
return $user;
}
}
Клиентский код прост:
$user = $facade->register($email, $pass);
class RegisterFacade {
#validator;
#users;
#mail;
#tracker;
#settings;
#logger;
constructor({ validator, users, mail, tracker, settings, logger }) {
this.#validator = validator;
this.#users = users;
this.#mail = mail;
this.#tracker = tracker;
this.#settings = settings;
this.#logger = logger ?? console;
}
async register(email, pass) {
// 1. Валидация
this.#validator.check(email, pass);
// 2. Создание пользователя
const user = await this.#users.create(email, pass);
// 3. Некритичные операции - ошибки не блокируют регистрацию
try {
await this.#mail.sendWelcome(email);
} catch (err) {
this.#logger.warn?.('welcome email failed', { err: err?.message });
}
this.#tracker.event('register', user.id);
await this.#settings.createDefaults(user.id);
return user;
}
}
Клиентский код прост:
const user = await facade.register(email, pass);
В JS зависимости часто передают одним объектом-options ({ validator, users, ... }) - это «именованные аргументы» через деструктуризацию: добавил/убрал зависимость - не нужно следить за порядком параметров.
Один вызов вместо пяти. Порядок, обработка ошибок, логирование - всё внутри фасада.
Facade vs Service Layer (Use Case)
В Clean Architecture Facade и Use Case очень похожи:
| Facade (GoF) | Use Case / Service Layer |
|---|---|
| Упрощает доступ к подсистемам | Содержит бизнес-правила |
| Координирует вызовы | Координирует вызовы + валидация |
| Может быть «тонким» прокси | Обычно содержит логику |
| Паттерн проектирования | Архитектурный слой |
На практике Use Case в Clean Architecture - это Facade с бизнес-правилами. RegisterFacade из примера выше - по сути use-case.
Когда фасад полезен
| Сценарий | Зачем фасад |
|---|---|
| Регистрация: валидация + БД + email | Один вызов вместо пяти |
| Оплата: проверка → списание → чек | Клиент не знает деталей |
| Импорт данных: парсинг → маппинг → БД | Один метод Import() |
| Деплой: сборка → тест → пуш → SSH | make deploy = фасад |
Фасад в Go stdlib
http.ListenAndServe(addr, handler)
- фасад над: создание Server, TCP-listener, Accept-loop, goroutine per conn
json.Marshal(v)
- фасад над: рефлексия, encoder, буфер, типы
exec.Command(name, args).Run()
- фасад над: fork, exec, pipe, wait
http.ListenAndServe - одна строка, за которой скрыта сложная машинерия: создание TCP-сокета, принятие соединений, запуск горутин, парсинг HTTP.
Когда фасад маскирует проблемы
Фасад - это «удобная дверца». Но если за дверцей хаос, фасад его не исправит, а только спрячет:
God Facade - метод DoEverything() на 200 строк. Это не фасад, это монолит с красивым именем. Фасад должен делегировать, а не содержать логику.
Сокрытие плохого API - если подсистемы спроектированы плохо, фасад лишь откладывает рефакторинг. Лучше исправить подсистемы.
Фасад над фасадом - если ты оборачиваешь фасад другим фасадом, что-то пошло не так с декомпозицией.
Когда НЕ использовать
Одна подсистема - если ты координируешь один сервис, фасад - лишняя обёртка. Вызывай сервис напрямую.
Клиенту нужен контроль - если разные клиенты вызывают шаги в разном порядке или пропускают некоторые, один фасад не подойдёт. Возможно, нужен Builder или несколько фасадов.
Тривиальная координация - если «фасад» это 2 вызова подряд, проще оставить как есть.
Связь с другими паттернами
Facade + Adapter - адаптер внутри фасада для внешних SDK
Facade + Observer - фасад публикует событие после выполнения
Facade + Factory - фабрика создаёт фасад с нужными зависимостями
Facade + Mediator - похожи: оба координируют, но Mediator управляет
взаимодействием МЕЖДУ объектами
Мини-задание
- Выдели один фасад для частого сценария в приложении (регистрация, оплата, импорт)
- Проверь: фасад только координирует? Или содержит бизнес-логику? Если да - вынеси логику в домен
- Найди
http.ListenAndServeв исходниках Go - сколько шагов он скрывает? - Подумай: есть ли в проекте «God Service» на 500 строк? Может, его стоит разбить на фасад + отдельные сервисы?