GraphQL Mutation Design: Anemic Mutations

Mutaties zijn een van de moeilijkste onderdelen van een GraphQL-schema om te ontwerpen. We besteden veel tijd aan het praten over GraphQL-vragen en hoe gemakkelijk ze te gebruiken zijn. Mutaties krijgen echter veel minder liefde.

Ik heb de afgelopen dagen nagedacht over hoe ik denk over mutatieontwerp en hoe dit verband houdt met RPC, REST en domeingestuurd ontwerp. Ik zal een aantal berichten posten over een paar onderwerpen met betrekking tot GraphQL Mutation Design.

In dit bericht zullen we ons concentreren op wat ik anemische mutaties heb genoemd.

Anemic Mutations ™ ️

Er is iets dat Anemic Domain Models wordt genoemd in de domeingestuurde ontwerpwereld. Snel uitgelegd, is het AnemicDomainModel een patroon waarin ons domeinmodel alleen gegevens bevat, zonder de bijbehorende gedragingen.

Ik denk dat we een behoorlijk interessante link kunnen leggen tussen dit "anti-patroon" en het ontwerpen van een goed mutatiesysteem in onze GraphQL API's. Hier is een voorbeeld van wat ik zou beschrijven als een anemische mutatie. Stel je een Checkout-object voor, onderdeel van een GraphQL-schema voor e-commerce:

Het eerste waar we naar kunnen kijken, is hoe alle invoervelden op UpdateCheckoutInput optioneel zijn. Omdat ze hebben gekozen voor een eenvoudige CRUD-mutatie, en omdat ze misschien gedeeltelijke updates willen toestaan ​​tijdens het afrekenproces, is het in het begin logisch om alles optioneel te maken. Er zijn een paar dingen aan dit ontwerp die ik echt niet leuk vind.

Ten eerste, omdat we besloten om een ​​"grove korrelige" mutatie te gebruiken waarmee we wijzigingen in dit Checkout-object kunnen aanbrengen, moesten we alles op nul zetten (optioneel). Een van de sterke punten van GraphQL is het type systeem. Door eventuele nullabiliteitsbeperkingen op de invoervelden te verwijderen, hebben we al deze validatie vrijwel uitgesteld tot de runtime, in plaats van het schema te gebruiken om de API-gebruiker naar een correct gebruik te leiden.

Het tweede punt is wat deze mutatie tot een anemische mutatie maakt. We hebben het invoertype op een zeer gegevensgerichte manier ontworpen, in plaats van ons te concentreren op gedrag. Stel je bijvoorbeeld voor dat onze API-gebruiker de API wil gebruiken om een ​​knop 'Toevoegen aan winkelwagentje' te maken:

  • Omdat de mutatie zoveel op gegevens is gericht en niet op gedrag, moeten onze klanten raden hoe ze een specifieke actie kunnen ondernemen. Wat als het toevoegen van een item aan onze kassa daadwerkelijk updates vereist voor een paar andere attributen? Onze klant zou alleen leren dat door fouten tijdens runtime, of in het ergste geval, in een verkeerde staat terecht kan komen door te vergeten een attribuut bij te werken.
  • We hebben cognitieve overbelasting toegevoegd aan klanten omdat ze de set velden moeten selecteren die moeten worden bijgewerkt wanneer ze een bepaalde actie willen ondernemen, zoals 'Toevoegen aan winkelwagentje'.
  • Omdat we ons richten op de vorm van de interne gegevens van een Checkout en niet op het potentiële gedrag van een Checkout, geven we niet expliciet aan dat het zelfs mogelijk is om deze acties te doen, we laten ze raden door te kijken naar ons datamodel.

Laten we eens kijken hoe we deze mutatie kunnen ontwerpen zodat deze ons expliciet vertelt hoe we een item aan een kassa kunnen toevoegen:

We hebben de meeste problemen opgelost waarover we eerder hebben gesproken:

  • Ons schema is sterk getypeerd. Niets is optioneel in deze mutatie, onze klanten weten precies wat te bieden om een ​​item aan een kassa toe te voegen.
  • Niet meer raden. In plaats van te zoeken welke gegevens moeten worden bijgewerkt, voegen we een item toe. Onze klanten geven er niet om welke gegevens in deze gevallen moeten worden bijgewerkt, ze willen gewoon een item toevoegen.
  • Het aantal mogelijke fouten dat kan optreden tijdens de uitvoering van deze mutatie is sterk verminderd. Onze resolver kan fijnere korrelige fouten retourneren.
  • Er is geen manier voor de klant om in een rare staat te geraken door deze mutatie te gebruiken, want dat wordt afgehandeld door onze resolver.

Een interessant neveneffect van het op deze manier ontwerpen van mutaties is dat het ontwikkelen van deze mutaties veel eenvoudiger wordt in de backend. Omdat de mutatie-invoer veel voorspelbaarder is, kunnen we veel niet-benodigde validaties in de resolver verwijderen. Een ander heel cool ding is dat het uitzenden van evenementen ook gemakkelijker wordt. Stel je voor dat je een ItemAdded-evenement probeert uit te zenden telkens wanneer een gebruiker een item toevoegt aan zijn kassa. In een dikke mutatie met optionele invoervelden moeten we controleren voor elk scenario met voorwaardelijke gebeurtenissen en gebeurtenissen uitzenden afhankelijk van deze voorwaarden, het wordt rommelig.

In zekere zin vind ik dit in veel overeenkomsten met een paar punten die Caleb Meredith een tijdje geleden in zijn bericht heeft gemaakt (https://dev-blog.apollodata.com/designing-graphql-mutations-e09de826ed97), waar ik erg van heb genoten .

In de komende dagen en weken publiceer ik nog een paar andere berichten over GraphQL-mutatieontwerp. Ik ga schrijven over het beheren van status met mutaties, ontwerpen voor statische query's en goed argumentontwerp.

Bedankt voor het lezen Als je dit bericht leuk vond, zou je me kunnen volgen op Twitter! Je zou waarschijnlijk ook genieten van The Little Book of GraphQL Schema Design waar ik hard aan werk om af te werken. Het zou veel betekenen als u zich zou abonneren op updates!