Ядро, яке не знає, куди пише
Ідею описав Алістер Кокберн: у застосунку є ядро з логікою, і воно не залежить ні від того, хто його викликає, ні від того, куди воно пише. Шестикутник на схемах просто зручна форма, кількість сторін нічого не означає.
Ядро оголошує , тобто інтерфейси, якими воно користується. реалізує порт для конкретної технології: Eloquent для бази, Laravel Mail для пошти. Ядро знає лише порт, а котрий адаптер підставити, вирішує контейнер.
Адаптери стоять по два боки. викликає ядро: HTTP-контролер, Livewire-компонент, Artisan-команда, тест. викликає саме ядро: репозиторій, пошту, годинник. Тест може бути ведучим адаптером, а фейки в памʼяті веденими, тож ядро перевіряється без бази й мережі.
Ядро і два боки
- Ядро (src/Contact: Domain і Application) містить: SubmitContactMessage, ContactMessageRepository, ContactNotifier, Clock.
- ContactForm використовує SubmitContactMessage (handle()).
- SubmitContactMessage використовує ContactMessageRepository.
- SubmitContactMessage використовує ContactNotifier.
- SubmitContactMessage використовує Clock.
- EloquentContactMessageRepository реалізує ContactMessageRepository.
- MailContactNotifier реалізує ContactNotifier.
- SystemClock реалізує Clock.
- FrozenClock реалізує Clock.
- 1Порт оголошує ядро й називає його своїми словами: ContactNotifier, а не MailerInterface.
- 2Ядро залежить лише від портів. Адаптер залежить від порту, ніколи навпаки.
- 3Привʼязку порту до адаптера робить конфігурація. У Laravel це сервіс-провайдер.
- 4Для тестів у порту є свій адаптер, як FrozenClock для Clock.
Сценарій, який бачить лише порти
Сценарій контактної форми отримує три порти в конструкторі й не знає, що за ними Eloquent, Laravel Mail і системний годинник. ContactServiceProvider привʼязує ContactMessageRepository до EloquentContactMessageRepository, а ContactNotifier до MailContactNotifier. Clock спільний для всіх модулів і живе в src/Shared.
Як порти й адаптери зазвичай псують
Порт мовою технології
Інтерфейс з методами where() і paginate(). Ядро знову знає про базу, тільки тепер через інтерфейс.
Натомість: порт описує потребу ядра: save(), findById(), notify().
Інтерфейс на кожен клас
Кожен клас має свій інтерфейс з єдиною реалізацією, навіть доменний сервіс без жодних зовнішніх залежностей.
Натомість: порт потрібен на межі із зовнішнім світом: база, пошта, час, HTTP. Усередині ядра класи викликають один одного напряму.
Фасади в ядрі
Сценарій викликає Mail::send() чи now(). Порти формально є, але тест усе одно потребує Laravel.
Натомість: час через Clock, пошта через порт, як ContactNotifier. Фасади живуть лише в адаптерах.
Чи потрібен вам порт
- Порт
- Інтерфейс на межі ядра, названий мовою ядра: ContactNotifier, Clock.
- Адаптер
- Реалізація порту під конкретну технологію: MailContactNotifier шле лист через Laravel Mail.
- Ведучий бік
- Driving side: адаптери, що викликають ядро. Контролер, консольна команда, тест.
- Ведений бік
- Driven side: адаптери, які викликає ядро. База, пошта, годинник, сторонні API.
Питання зі співбесід
У домені або в шарі застосунку, бо порт належить ядру. Тоді інфраструктура залежить від ядра, а не навпаки.
Шари впорядковують код згори вниз, і в класичній схемі все залежить від інфраструктури внизу. Hexagonal ділить на ядро й зовнішній світ: і контролер, і база підключаються адаптерами через порти. На практиці їх поєднують, як у PHP Ukraine: шари всередині модуля, порти на межі ядра.
Тест стає ведучим адаптером і викликає сценарій напряму, а ведені порти отримують фейки: репозиторій на масиві, FrozenClock. Справжні адаптери перевіряють окремими інтеграційними тестами проти бази чи API.