Facade: простой интерфейс для сложной системы

Facade: простой интерфейс для сложной системы

Хорошая регистрация пользователя - это пять-семь шагов внутри и одна функция снаружи. Facade - про эту одну функцию.

Проблема

Чтобы зарегистрировать пользователя, нужно:

  1. Валидировать данные
  2. Создать запись в БД
  3. Отправить welcome-email
  4. Записать событие в аналитику
  5. Создать настройки по умолчанию

Если клиентский код вызывает каждый сервис отдельно:

// в обработчике
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() и не думает о деталях.

5 сервисов → один метод фасада → клиент

Фасад регистрации

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, ... }) - это «именованные аргументы» через деструктуризацию: добавил/убрал зависимость - не нужно следить за порядком параметров.

Один вызов вместо пяти. Порядок, обработка ошибок, логирование - всё внутри фасада.

Без фасада каждый клиент (handler, CLI, тест) повторяет одну и ту же последовательность вызовов. Фасад - единая точка входа. Изменил порядок шагов? Правишь одно место.

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()
Деплой: сборка → тест → пуш → SSHmake 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 - если подсистемы спроектированы плохо, фасад лишь откладывает рефакторинг. Лучше исправить подсистемы.

Фасад над фасадом - если ты оборачиваешь фасад другим фасадом, что-то пошло не так с декомпозицией.

Фасад координирует вызовы, но НЕ содержит бизнес-правила. «Пароль минимум 8 символов» - это домен/валидатор. Фасад просто вызывает validator.Check(). Это применение [SRP на уровне модуля](../solid/02-srp.md): у фасада ровно одна причина меняться - изменение оркестрации шагов.

Когда НЕ использовать

Одна подсистема - если ты координируешь один сервис, фасад - лишняя обёртка. Вызывай сервис напрямую.

Клиенту нужен контроль - если разные клиенты вызывают шаги в разном порядке или пропускают некоторые, один фасад не подойдёт. Возможно, нужен Builder или несколько фасадов.

Тривиальная координация - если «фасад» это 2 вызова подряд, проще оставить как есть.

Связь с другими паттернами

Facade + Adapter - адаптер внутри фасада для внешних SDK
Facade + Observer - фасад публикует событие после выполнения
Facade + Factory - фабрика создаёт фасад с нужными зависимостями
Facade + Mediator - похожи: оба координируют, но Mediator управляет
                        взаимодействием МЕЖДУ объектами

Мини-задание

  • Выдели один фасад для частого сценария в приложении (регистрация, оплата, импорт)
  • Проверь: фасад только координирует? Или содержит бизнес-логику? Если да - вынеси логику в домен
  • Найди http.ListenAndServe в исходниках Go - сколько шагов он скрывает?
  • Подумай: есть ли в проекте «God Service» на 500 строк? Может, его стоит разбить на фасад + отдельные сервисы?

Зарегистрируйтесь бесплатно, чтобы пройти квиз, решить задание с автопроверкой и вести прогресс.