Razvoj

Beyond Rewrite: Building APIs with Predictable Core Logic

Više od prepisivanja: Izgradnja API-ja s predvidljivom temeljnom logikom

Većina neuspjeha API-ja ne počinje na HTTP granici. Počinju kada su poslovne odluke raspršene po kontrolerima, upitima u bazi podataka, događajima okvira i pomoćnim metodama tipa „samo još jedna”. Prepisivanje koda može neko vrijeme učiniti taj raspored urednijim, ali ne čini sustav automatski lakšim za razumijevanje.

Trajniji je cilj predvidiva jezgrena logika: malo, eksplicitno središte u kojem žive važna pravila, provjeravaju se ulazi, imenuju ishodi i kontroliraju nuspojave. Kada je ta jezgra stabilna, promjena PHP okvira, zamjena ORM upita, dodavanje Dockera ili izlaganje nove verzije API-ja postaje ograničen inženjerski zadatak umjesto rizičnog iskapanja.

Učinite poslovnu odluku eksplicitnom

Krajnja točka API-ja trebala bi koordinirati rad, a ne sadržavati njegovo značenje. Kontroler može autentificirati zahtjev, prevesti JSON u ulazni objekt, pozvati aplikacijsku uslugu i pretvoriti rezultat u HTTP odgovor. Ne bi smio odlučivati može li se narudžba otkazati, smije li saldo pasti ispod nule ili kako se primjenjuje pravilo određivanja cijena.

Te odluke pripadaju kodu koji se može pozvati bez HTTP-a, veze s bazom podataka ili pokrenutog kontejnera. To ne zahtijeva ceremonijalnost za svaku trivijalnu krajnju točku. To znači prepoznati pravila koja bi bilo skupo pogrešno implementirati i dati im jasno mjesto.

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);
        });
    }
}

Poanta nije u nazivu klase. Poanta je u tome da je pravilo otkazivanja vidljivo, testabilno i neovisno o tome je li pozivatelj došao putem REST-a, zadatka iz naredbenog retka ili potrošača poruka.

Upotrijebite granice za obuzdavanje neizvjesnosti

Pozadinski sustavi rade s neizvjesnim stvarima: unosom klijenta, mrežnim pozivima, dostupnošću baze podataka, redovima i vremenom sata. Predvidiva jezgra ne pretvara se da je neizvjesnost nestala. Zadržava je na rubovima i namjerno predstavlja smislene ishode.

Na primjer, narudžba koja nedostaje nije nužno iznimka. Narudžba koja se ne može otkazati jer je poslana također nije nužno kvar poslužitelja. Oboje su valjani ishodi zahtjeva. Kontroler može dosljedno prevesti te ishode:

$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),
};

Neočekivani infrastrukturni kvarovi su drukčiji. Istek vremena baze podataka treba zabilježiti, nadzirati i obraditi u skladu s pravilima usluge za pogreške. Spajanje toga u općeniti odgovor „nije moguće otkazati narudžbu” skriva operativni problem i navodi klijente na pogrešno ponašanje.

Provjeravajte na rubu, provodite u jezgri

Provjera zahtjeva trebala bi rano odbaciti neispravne terete: polja koja nedostaju, nevažeće UUID-ove, nepodržane enum vrijednosti i netočne vrste podataka. No jezgrena logika i dalje mora štititi svoje invarijante. Drugi pozivatelj možda će sutra zaobići HTTP validator ili prethodno valjan zahtjev može postati nevaljan nakon promjena podataka.

Korisno je pravilo jednostavno: provjeravajte oblik na granici; provodite istinitost u domeni. Prvo poboljšava povratne informacije API-ja. Drugo štiti sustav.

Transakcije su dio slučaja upotrebe

Transakcije baze podataka često se smatraju detaljem repozitorija. To je preniska razina kada poslovna operacija zapisuje više zapisa ili mora očuvati pravilo dosljednosti. Slučaj upotrebe trebao bi uspostaviti transakcijsku granicu jer zna što mora uspjeti ili pasti zajedno.

Razmotrite stvaranje računa i rezerviranje zaliha. Ako je račun pohranjen, ali rezervacija zaliha ne uspije, ishod nije samo nezgodan; može biti semantički pogrešan. Povezane lokalne zapise smjestite u jednu transakciju. Ako je moguće, udaljene pozive držite izvan te transakcije jer zadržavanje zaključavanja baze podataka tijekom čekanja druge usluge potiče sukobe i isteke vremena.

Kada vanjska obavijest mora uslijediti nakon potvrđenog zapisa, koristite pristup u stilu outboxa: pohranite zapis događaja u istoj transakciji, a zatim neka ga radnik objavi. Radnik mora tolerirati ponovne pokušaje, a potrošači moraju tolerirati dvostruku isporuku. Poruka koju je moguće ponovno pokušati poslati trebala bi imati stabilan identifikator kako bi se daljnji rad mogao učiniti idempotentnim.

  • Zajedno potvrdite lokalno stanje i zapis događaja.
  • Objavite događaje asinkrono nakon potvrde.
  • Ponovno pokušajte objavu s ograničenim odgodama i mogućnošću praćenja.
  • Učinite potrošače sigurnima kada isti događaj stigne više puta.

To je manje glamurozno od izravnog HTTP poziva nakon spremanja, ali se bolje ponaša kada mreže zakažu baš u pogrešnom trenutku.

Neka perzistencija služi modelu

ORM može biti produktivan, ali praktične metode ne bi trebale postati arhitektura. Upiti koji kodiraju poslovno značenje zaslužuju nazive i testove. Ograničenja baze podataka trebala bi podržati važne invarijante, osobito jedinstvenost i odnose stranih ključeva, jer same provjere aplikacije mogu izgubiti utrku pri istodobnim zahtjevima.

Na primjer, nije dovoljno provjeriti postoji li adresa e-pošte, a zatim umetnuti korisnika. Dva zahtjeva mogu istodobno proći provjeru. Jedinstveno ograničenje baze podataka konačni je autoritet; aplikacijski kod trebao bi uhvatiti i prevesti očekivani sukob u koristan rezultat API-ja.

Izvedba slijedi isto načelo. Izmjerite stvarnu putanju upita prije dodavanja predmemorija ili denormaliziranih tablica. Izbjegavajte učitavanje cijelih grafova objekata samo zato što ORM to olakšava. Koristite paginaciju sa stabilnim redoslijedom, odaberite samo potrebna polja za krajnje točke s mnogo čitanja i pregledajte generirane upite kada neka putanja postane važna.

Neka implementacija bude dosadna

Docker je vrijedan kada razjašnjava ugovor izvođenja: verzija PHP-a, potrebna proširenja, pokretanje procesa i ulazi konfiguracije trebaju biti eksplicitni. Nije zamjena za dizajn aplikacije. Kontejner koji radi lokalno, ali ovisi o nedokumentiranim varijablama okruženja, i dalje je krhak.

Gradite slike predvidivo, prosljeđujte konfiguraciju kroz okruženje ili podržani mehanizam za tajne i neka provjere zdravlja odražavaju spremnost, a ne samo postojanje procesa. Pokrenut PHP proces ne dokazuje da može dosegnuti svoju bazu podataka, sigurno pokrenuti potrebne migracije ili ispravno posluživati promet.

Implementacija također treba putanju kvara. Promjene sheme trebale bi biti kompatibilne s trenutačno implementiranom aplikacijom kad god je moguća postupna implementacija. Dodajte nullable stupac prije nego što ga učinite obveznim. Zapišite nove podatke prije nego što o njima ovisite. Uklonite stare putanje tek nakon što ih sustav više ne treba. Najsigurnija migracija obično je niz koraka, a ne dramatičan prijelaz.

Predvidivost je imovina koja se umnožava

Dobra arhitektura pozadinskog sustava nije stvar maksimiziranja slojeva ni postizanja modernog dijagrama. Riječ je o tome da promjena bude razumljiva. Programer bi trebao moći odgovoriti: gdje se ovo pravilo provodi, koji se ishodi mogu dogoditi, koji se podaci mijenjaju zajedno i što se događa ako ovisnost zakaže?

Kada ti odgovori žive u eksplicitnoj jezgrenoj logici, API-ji postaju mirniji. Testovi postaju vredniji. Incidente je lakše dijagnosticirati. Prepisivanja postaju opcionalna umjesto neizbježna. Najbolji temelj nije onaj koji se nikada ne mijenja; to je onaj koji omogućuje promjenu bez pretvaranja svakog izdanja u nagađanje.

Portret autora bloga

Mihajlo

Ja sam Mihajlo — programer vođen znatiželjom, disciplinom i stalnom željom da stvorim nešto smisleno. Dijelim uvide, tutorijale i besplatne usluge kako bih pomogao drugima da pojednostave svoj rad i rastu u svijetu softvera i umjetne inteligencije koji se neprestano razvija.