Раздвојување на зависностите: Градење лабаво поврзани PHP системи
Повеќето PHP апликации не стануваат тешки поради една лоша класа. Стануваат тешки затоа што секоја класа постепено дознава премногу за остатокот од системот: двигателот на базата на податоци, давателот на е-пошта, објектот за барање на рамката, SDK-то за плаќање и точниот начин на кој се складира конфигурацијата.
Таквата погодност на почетокот изгледа продуктивно. Подоцна, мала промена станува низа измени низ контролери, услуги, команди и тестови. Раздвојувањето на зависностите е практика на запирање на тоа ширење. На секој дел од апликацијата му дава јасна задача и помал сет претпоставки за светот околу него.
Зависноста не е непријател; неконтролираната зависност е
Еден систем не може да има нула зависности. Услугата за наплата нарачки на крајот мора да наплати преку давател на плаќања, а репозиториумот на крајот мора да комуницира со база на податоци. Целта не е да се преправаме дека тие односи не постојат. Целта е тие да се постават на намерно избрани граници.
Лабавата поврзаност значи дека едно деловно правило зависи од способност, а не од одредена имплементација. На пример, работниот тек за нарачка може да има потреба од нешто што може да наплати плаќање. Не треба да знае дали таа способност ја обезбедува Stripe, банкарски портал или тест-двојник.
Практичната придобивка не е архитектонска чистота. Таа е ограничување на промените. Кога инфраструктурата се менува, треба да се изменат помалку датотеки, а важните однесувања може да се тестираат без составување на целата апликација.
Почнете од насоката на зависноста
Деловната логика треба да биде близу до центарот на дизајнот. Кодот на рамката, адаптерите за бази на податоци, HTTP клиентите, редиците и SDK-јата на добавувачите припаѓаат блиску до рабовите. Зависностите генерално треба да се насочени навнатре: работ приспособувајќи се на потребите на деловната логика, наместо деловната логика да се приспособува на секој надворешен детал.
Чест предупредувачки знак е класа ориентирана кон доменот што сама ги создава своите соработници:
final class InvoiceService
{
public function send(int $invoiceId): void
{
$invoice = new PDOInvoiceRepository();
$mailer = new VendorMailer();
// Load, render, and send the invoice.
}
}
Оваа класа сега е одговорна и за однесувањето на фактурата и за изборот на конкретна инфраструктура. Не може да се изврши без интеграција со базата на податоци и е-поштата, а менувањето на кој било давател значи измена на услугата.
Наместо тоа, изразете ги зависностите во конструкторот и зависете од мали интерфејси во сопственост на апликацијата:
interface InvoiceRepository
{
public function getById(int $id): Invoice;
}
interface InvoiceSender
{
public function send(Invoice $invoice): void;
}
final class InvoiceService
{
public function __construct(
private InvoiceRepository $invoices,
private InvoiceSender $sender,
) {
}
public function send(int $invoiceId): void
{
$this->sender->send($this->invoices->getById($invoiceId));
}
}
Репозиториум поддржан од база на податоци и адаптер за е-пошта специфичен за давател може на друго место да ги имплементираат тие интерфејси. Услугата го задржува својот фокус: да ја координира деловната операција.
Користете инјектирање зависности како дизајнерска навика
Инјектирањето зависности често се воведува како функционалност на контејнер, но контејнерот е спореден. Важната навика е зависностите да бидат видливи. Инјектирањето преку конструктор ги прави потребните соработници експлицитни, создава валидни објекти од самиот почеток и ги прави тестовите едноставни.
Во апликација со рамка, контејнерот може да го состави графот на објекти на работ на системот. Тоа е корисно, но избегнувајте пребарувањата во контејнерот да навлезат во деловниот код. Повик како container->get() ги крие вистинските зависности на класата и го претвора контејнерот во локатор на услуги.
За едноставни апликации, составувањето може да биде подеднакво јасно и без контејнер:
$repository = new PdoInvoiceRepository($pdo);
$sender = new MailProviderInvoiceSender($mailClient);
$service = new InvoiceService($repository, $sender);
Без разлика дали ова поврзување се наоѓа во провајдер на рамката, датотека за подигнување или влезна точка на команда, држете го блиску до стартувањето на апликацијата. Таму припаѓаат конкретните избори.
Дизајнирајте интерфејси околу вашиот случај на употреба
Интерфејсите се вредни кога штитат значајна граница, а не кога секоја класа добива таков по дифолт. Интерфејс наречен DatabaseInterface со десетици генерички методи обично изложува премногу инфраструктура. Тесен интерфејс како InvoiceRepository опишува што навистина ѝ е потребно на апликацијата.
Оваа разлика исто така спречува апстракцијата да стане формалност. Создадете интерфејс кога барем едно од следново е точно:
- Зависноста е надворешна инфраструктура, како складирање, пораки, време или HTTP API.
- Деловниот слој треба да остане независен од рамка или пакет на добавувач.
- Ви треба контролирана замена за тестирање на однесување што е бавно, недетерминистичко или скапо.
- Повеќе од една имплементација е веројатна сега или како што системот се развива.
Не воведувајте интерфејс само затоа што постои класа. Мал објект со вредност или фокусирана внатрешна услуга може да остане конкретна сè додека не се појави вистинска граница.
Држете ги грижите за рамката и транспортот на работ
Контролерите треба да ги преведуваат HTTP грижите во влез за апликацијата и да ги преведуваат резултатите назад во HTTP одговори. Не треба да содржат работен тек на трансакција, градење пребарувања или обработка на грешки специфична за давател.
Истото правило важи за обработувачите од командната линија, потрошувачите на редици и закажаните задачи. Секој од нив е адаптер околу истото однесување на апликацијата. Кога основниот работен тек е независен од објект за барање или објект за команда, може повторно да се користи без копирање логика.
Трансакциите во базата на податоци се корисен пример за граница што бара внимателно поставување. Услуга за случај на употреба може да ја дефинира трансакциската единица на работа, додека извршувачот на трансакции се доставува како зависност. Ова го зачувува деловното значење на атомарноста без секој случај на употреба да учи за PDO методи или фасада на рамка.
Не дозволувајте моделите на добавувачот да протекуваат низ апликацијата
Пренесувањето ORM ентитети, објекти за одговор од SDK или сурови асоцијативни низи насекаде прави остатокот од кодната база да го наследи моделот на податоци на добавувачот. Мапирајте ги надворешните податоци во објекти специфични за апликацијата на границата. Мапирањето може да изгледа повторливо, но ги локализира трошоците за идните промени и ги прави инваријантите полесни за воочување.
Ова не значи дека секоја табела бара сложен доменски модел. Прагматизмот е важен. Запит за извештаи само за читање разумно може да врати едноставна структура на податоци. Клучното прашање е дали претставувањето излегува во одлуки што треба да го надживеат тековниот избор на перзистенција или API.
Тестирајте го однесувањето без повторно да го изградите светот
Лабавата поврзаност ги подобрува тестовите затоа што важните тестови можат да работат со мали, детерминистички соработници. Лажен репозиториум или испраќач треба да го имплементира истиот договор, да бележи корисни интеракции и да се однесува доволно реалистично за сценариото. Не треба само да ги репродуцира внатрешните детали на продукциската имплементација.
Сепак, единичните тестови не се доволни. На адаптерите им требаат интеграциски тестови со вистинската база на податоци, HTTP протокол или sandbox на давателот каде што е достапен. Здравата поделба е едноставна: тестирајте ги деловните правила преку тесни договори, а потоа тестирајте го секој инфраструктурен адаптер таму каде што се среќава со вистинската технологија.
Раздвојувајте постепено, не со едно препишување
Наследениот PHP код ретко станува лабаво поврзан преку драматичен редизајн. Почнете од болна спојка: услуга што директно создава HTTP клиент, контролер што содржи деловни правила или репозиториум што враќа објекти специфични за рамката. Воведете еден мал договор, преместете ја конкретната имплементација зад него и поврзете ја на границата на апликацијата.
По секоја промена, прашајте се дали новата зависност ја прави класата полесна за разбирање и тестирање. Ако само додава слоеви без да ја разјасни одговорноста, поедноставете ја.
Трајната придобивка
Лабаво поврзаните PHP системи не се системи со најмногу интерфејси. Тие се системи во кои промените имаат очигледно место. Давателот на плаќања се менува во неговиот адаптер. Пребарувањето во базата на податоци се менува во неговиот репозиториум. Деловното правило се менува во случајот на употреба што го поседува.
Таа јасност е она што ја одржува кодната база приспособлива долго по првото издание. Градете граници таму каде што е веројатна промена, држете ја конкретната технологија на рабовите и оставете центарот на апликацијата да зборува на јазикот на бизнисот. Резултатот не е само полесно тестирање. Тоа е софтвер што останува разбирлив кога е најважно: за време на промена.