ИТ развој

Beyond Rewrite: Building APIs with Predictable Core Logic

Повеќе од преработување: Градење API-ја со предвидлива основна логика

Повеќето неуспеси на API не започнуваат на HTTP границата. Тие започнуваат кога деловните одлуки се расфрлани низ контролери, барања кон базата на податоци, настани од рамката и „уште само еден“ помошен метод. Препишувањето може некое време да направи таквиот распоред да изгледа почисто, но не го прави автоматски системот полесен за разбирање.

Потрајната цел е предвидлива основна логика: мал, експлицитен центар каде што живеат важните правила, влезовите се валидираат, исходите се именуваат, а несаканите ефекти се контролираат. Кога таа основа е стабилна, менувањето PHP рамка, заменувањето ORM барање, додавањето Docker или изложувањето нова верзија на API станува ограничена инженерска задача наместо ризично ископување.

Направете ја деловната одлука експлицитна

API крајна точка треба да ја координира работата, а не да го содржи значењето на работата. Контролерот може да автентицира барање, да преведе JSON во влезен објект, да повика апликациска услуга и да го претвори резултатот во HTTP одговор. Не треба да одлучува дали нарачката може да се откаже, дали салдото смее да падне под нула или како се применува правило за цени.

Тие одлуки припаѓаат во код што може да се повика без HTTP, врска со база на податоци или активен контејнер. Ова не бара формалности за секоја тривијална крајна точка. Значи да се идентификуваат правилата за кои би било скапо да се згрешат и да им се даде јасно место.

final class CancelOrder
{
    public function __construct(
        private OrderRepository $orders,
        private TransactionManager $transactions,
    ) {}

    public function execute(OrderId $orderId, CustomerId $customerId): CancelOrderResult
    {
        return $this->transactions->run(function () use ($orderId, $customerId) {
            $order = $this->orders->get($orderId);

            if (!$order->belongsTo($customerId)) {
                return CancelOrderResult::notFound();
            }

            if (!$order->canBeCancelled()) {
                return CancelOrderResult::notCancellable($order->status());
            }

            $order->cancel();
            $this->orders->save($order);

            return CancelOrderResult::cancelled($order);
        });
    }
}

Суштината не е во името на класата. Суштината е дека правилото за откажување е видливо, тестирано и независно од тоа дали повикувачот пристигнал преку REST, задача од командна линија или потрошувач на пораки.

Користете граници за да ја ограничите неизвесноста

Бекенд-системите комуницираат со неизвесни нешта: влез од клиентот, мрежни повици, достапност на базата на податоци, редици и време на часовникот. Предвидливото јадро не се преправа дека неизвесноста исчезнала. Ја задржува на рабовите и намерно ги претставува значајните исходи.

На пример, нарачка што недостасува не е нужно исклучок. Нарачка што не може да се откаже бидејќи е испорачана, исто така не е нужно неуспех на серверот. И двете се валидни исходи на барање. Контролерот може доследно да ги преведе тие исходи:

$result = $cancelOrder->execute($orderId, $customerId);

return match ($result->type()) {
    CancelOrderResultType::CANCELLED => response()->json($result->order(), 200),
    CancelOrderResultType::NOT_FOUND => response()->json(['error' => 'not_found'], 404),
    CancelOrderResultType::NOT_CANCELLABLE => response()->json([
        'error' => 'order_not_cancellable',
        'status' => $result->status(),
    ], 409),
};

Неочекуваните инфраструктурни неуспеси се различни. Истек на време на базата на податоци треба да се евидентира, следи и обработи според политиката за грешки на услугата. Неговото вметнување во генерички одговор „не може да се откаже нарачката“ крие оперативен проблем и ги тера клиентите да се однесуваат неправилно.

Валидирајте на работ, спроведувајте во јадрото

Валидацијата на барањето треба рано да отфрли неисправни товари: полиња што недостасуваат, невалидни UUID-ја, неподдржани enum вредности и неточни типови на податоци. Но основната логика сепак мора да ги штити своите инваријанти. Утре друг повикувач може да го заобиколи HTTP валидаторот, или претходно валидно барање може да стане невалидно по промени во податоците.

Корисно правило е едноставно: валидирајте ја формата на границата; спроведувајте ја вистината во доменот. Првото ги подобрува повратните информации од API. Второто го штити системот.

Трансакциите се дел од случајот на употреба

Трансакциите со базата на податоци често се третираат како детаљ на репозиториумот. Тоа е премногу ниско ниво кога деловна операција запишува повеќе записи или мора да зачува правило за конзистентност. Случајот на употреба треба да ја воспостави трансакциската граница бидејќи знае што мора да успее или да не успее заедно.

Разгледајте создавање фактура и резервирање залихи. Ако фактурата е зачувана, но резервирањето на залихите не успее, исходот не е само незгоден; може да биде семантички погрешен. Ставете ги поврзаните локални запишувања во една трансакција. Каде што е можно, држете ги далечинските повици надвор од таа трансакција, бидејќи задржувањето заклучувања на базата додека се чека друга услуга предизвикува спорење и истекување на времето.

Кога надворешно известување мора да следи по потврдено запишување, користете пристап во стил на outbox: зачувајте запис за настан во истата трансакција, а потоа дозволете му на работник да го објави. Работникот мора да толерира повторни обиди, а потрошувачите мора да толерираат дуплирана испорака. Пораката што може повторно да се обиде треба да има стабилен идентификатор за работата надолу по текот да може да се направи идемпотентна.

  • Потврдете ја локалната состојба и записот за настанот заедно.
  • Објавувајте настани асинхроно по потврдата.
  • Повторувајте го објавувањето со ограничен backoff и набљудливост.
  • Направете ги потрошувачите безбедни кога истиот настан пристигнува повеќе од еднаш.

Ова е помалку гламурозно од директен HTTP повик по зачувување, но се однесува подобро кога мрежите откажуваат токму во најлошиот момент.

Нека перзистентноста му служи на моделот

ORM може да биде продуктивен, но практичните методи не треба да станат архитектурата. Барањата што кодираат деловно значење заслужуваат имиња и тестови. Ограничувањата во базата на податоци треба да ги поддржат важните инваријанти, особено уникатноста и односите со странски клучеви, бидејќи само проверките во апликацијата може да изгубат трка при истовремени барања.

На пример, не е доволно да проверите дали постои адреса на е-пошта, а потоа да вметнете корисник. Две барања можат истовремено да ја поминат проверката. Уникатното ограничување во базата на податоци е конечниот авторитет; апликацискиот код треба да го фати и да го преведе очекуваниот конфликт во корисен API резултат.

Перформансите го следат истиот принцип. Измерете ја вистинската патека на барањето пред да додадете кешови или денормализирани табели. Избегнувајте да вчитувате цели графови на објекти само затоа што ORM го олеснува тоа. Користете пагинација со стабилно подредување, избирајте само потребни полиња за крајни точки со многу читања и проверувајте ги генерираните барања кога некоја патека ќе стане важна.

Одржувајте го распоредувањето едноставно

Docker е вреден кога го прави договорот за извршување јасен: верзијата на PHP, потребните екстензии, стартувањето на процесот и конфигурациските влезови треба да бидат експлицитни. Тој не е замена за дизајн на апликацијата. Контејнер што работи локално, но зависи од недокументирани променливи на околината, сè уште е кревок.

Градете слики предвидливо, пренесувајте конфигурација преку околината или поддржан механизам за тајни и направете здравствените проверки да ја одразуваат подготвеноста, наместо само постоењето на процесот. Активен PHP процес не докажува дека може да стигне до својата база на податоци, безбедно да ги изврши потребните миграции или правилно да опслужува сообраќај.

Распоредувањето исто така има потреба од патека за неуспех. Промените на шемата треба да бидат компатибилни со тековно распоредената апликација секогаш кога е возможно постепено распоредување. Додадете nullable колона пред да ја направите задолжителна. Запишете нови податоци пред да зависите од нив. Отстранете ги старите патеки само откако системот повеќе не ги бара. Најбезбедната миграција обично е низа, а не драматско префрлување.

Предвидливоста е средство што се умножува

Добрата бекенд-архитектура не е за максимизирање слоеви или постигнување модерен дијаграм. Таа е за правење на промената разбирлива. Развивачот треба да може да одговори: каде се спроведува ова правило, кои исходи можат да се случат, кои податоци се менуваат заедно и што се случува ако зависност откаже?

Кога тие одговори живеат во експлицитна основна логика, API-јата стануваат помирни. Тестовите стануваат повредни. Инцидентите стануваат полесни за дијагностицирање. Препишувањата стануваат изборни наместо неизбежни. Најдобрата основа не е онаа што никогаш не се менува; таа е онаа што дозволува промената да се случи без секое издание да се претвори во претпоставка.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.