I den här artikeln kommer jag att presentera användningen av polymorfism i GraphQL. Innan jag börjar är det dock värt att påminna om vad polymorfism och GraphQL är.
Polymorfism
Polymorfism är en viktig komponent i objektorienterad programmering. Förenklat bygger det på att objekt av olika klasser har tillgång till samma gränssnitt, vilket innebär att vi kan förvänta oss samma funktionalitet från var och en av dem, men inte nödvändigtvis implementerad på samma sätt. I Ruby-utvecklare kan erhållas polymorfism på tre olika sätt:
Ärftlighet
Ärftlighet består i att skapa en överordnad klass och underordnade klasser (dvs. som ärver från den överordnade klassen). Underklasser får den överordnade klassens funktionalitet och gör det också möjligt att ändra och lägga till funktionalitet.
Exempel:
klass Dokument
attr_läsare :namn
slut
klass PDFDocument < Dokument
def förlängning
:pdf
slut
slut
klass ODTDocument < Dokument
def förlängning
:odt
slut
slut
Moduler
Moduler i Ruby har många användningsområden. Ett av dem är mixins (läs mer om mixins i Den ultimata uppdelningen: Ruby vs. Python). Mixins i Ruby kan användas på liknande sätt som gränssnitt i andra programmeringsspråk (t.ex. i Java) kan du till exempel definiera metoder som är gemensamma för objekt som kommer att innehålla en viss mixin. Det är en god praxis att inkludera skrivskyddade metoder i moduler, dvs. metoder som inte ändrar objektets tillstånd.
Exempel:
modul Taxable
def skatt
pris * 0,23
slut
slut
klass Bil
include Skattepliktig
attr_läsare :pris
slut
klass Bok
inkluderar Taxable
attr_läsare :pris
slut
Skrivning av anka
Detta är en av de viktigaste egenskaperna hos dynamiskt typade språk. Namnet kommer från det berömda testet om det ser ut som en anka, simmar som en anka och kvackar som en anka, då är det förmodligen en anka. Testet programmerare behöver inte vara intresserad av vilken klass det givna objektet tillhör. Det som är viktigt är de metoder som kan anropas på detta objekt.
Med hjälp av de klasser som definieras i exemplet ovan:
klass Bil
attr_reader :pris
def initialize(pris)
@pris = pris
slut
slut
klass Bok
attr_läsare :pris
def initialize(pris)
@pris = pris
slut
slut
bil = Bil.ny(20.0)
bok = Bok.ny(10.0)
[bil, bok].map(&:pris
GrapQL
GraphQL är ett relativt nytt frågespråk för API:er. Till dess fördelar hör att det har en mycket enkel syntax och dessutom bestämmer kunden exakt vad den vill få eftersom varje kund får exakt vad den vill ha och inget annat.
Detta är förmodligen allt vi behöver veta just nu. Så, låt oss komma till saken.
Presentation av problemet
För att på bästa sätt förstå problemet och dess lösning, låt oss skapa ett exempel. Det skulle vara bra om exemplet var både originellt och ganska jordnära. Ett som var och en av oss kan stöta på någon dag. Vad sägs om... djur? Ja! Ja! Bra idé!
Anta att vi har en backend-applikation som är skriven i Ruby on Rails. Den är redan anpassad för att hantera ovanstående schema. Låt oss också anta att vi redan har GraphQL konfigurerad. Vi vill göra det möjligt för kunden att göra en förfrågan inom följande struktur:
{
allaZoo : {
djurpark: {
namn
stad
djur: {
...
}
}
}
}
Vad som ska sättas i stället för de tre prickarna för att få fram den saknade informationen - det får vi reda på senare.
Implementering
Nedan kommer jag att presentera de steg som behövs för att uppnå målet.
Lägga till en fråga i QueryType
Först måste du definiera vad exakt frågan allZoos betyder. För att göra detta måste vi besöka filenapp/graphql/types/query_type.rb och definiera frågan:
modul Typer
klass QueryType < Typer::BaseObject
fält :all_zoos, [Types::ZooType], null: false
def all_zoos
Zoo.alla
slut
slut
slut
Frågeställningen är redan definierad. Nu är det dags att definiera returtyperna.
Definition av typer
Den första typen som krävs kommer att vara ZooType. Låt oss definiera den i filen app/graphql/types/ zoo_type.rb:
modul Typer
klass ZooType < Typer::BaseObject
fält :name, Sträng, null: false
fält :city, Sträng, null: false
fält :animals, [Types::AnimalType], null: false
slut
slut
Nu är det dags att definiera typen AnimalType:
modul Typer
klass AnimalType < Typer::BasUnion
möjliga_typer ElephantType, CatType, DogType
def self.resolve_type(obj, ctx)
if obj.is_a?(Elephant)
ElefantTyp
elsif obj.is_a?(Cat)
KattTyp
elsif obj.is_a?(Hund)
HundTyp
slut
slut
slut
slut
Vi måste lista alla typer som kan ingå i en viss förening. 3. Vi åsidosätter funktionen self.resolve_object(obj, ctx),som måste returnera typen av ett givet objekt.
Nästa steg är att definiera vilka typer av djur det handlar om. Vi vet dock att vissa fält är gemensamma för alla djur. Låt oss inkludera dem i typen AnimalInterface:
modul Typer
modul AnimalInterface
include Typer::BaseInterface
fält :namn, Sträng, null: false
fält :age, heltal, null: false
slut
slut
Med detta gränssnitt kan vi gå vidare med att definiera typerna av specifika djur:
modul Typer
klass ElephantType < Typer::BaseObject
implementerar Types::AnimalInterface
fält :trunk_length, Float, null: false
slut
slut
modul Typer
klass CatType < Typer::BaseObject
implementerar Types::AnimalInterface
fält :hair_type, String, null: false
slut
slut
modul Typer
klass DogType < Typer::BaseObject
implementerar Types::AnimalInterface
fält :breed, String, null: false
slut
slut
Så där ja! Nu är vi klara! En sista fråga: hur kan vi använda det vi har gjort från kundens sida?
Uppbyggnad av förfrågan
{
allaZoo : {
djurpark: {
namn
stad
djur: {
__typenamn
... på ElephantType {
namn
ålder
snabelLängd
}
... på CatType {
namn
ålder
hårTyp
}
... på DogType {
namn
ålder
ras
}
}
}
}
}
Vi kan använda ett extra __typename-fält här, som returnerar den exakta typen av ett givet element (t.ex. CatType). Hur kan ett exempel på svar se ut?
En nackdel med detta tillvägagångssätt är uppenbar. I frågan måste vi ange namn och ålder för varje typ, trots att vi vet att alla djur har dessa fält. Detta är inte så besvärande när samlingen innehåller helt olika objekt. I det här fallet delar dock djuren nästan alla fält. Kan det förbättras på något sätt?
Naturligtvis! Vi gör den första ändringen i filen app/graphql/types/zoo_type.rb:
modul Typer
klass ZooType < Typer::BaseObject
fält :name, Sträng, null: false
fält :city, Sträng, null: false
fält :animals, [Types::AnimalInterface], null: false
slut
slut
Vi behöver inte längre den union som vi tidigare definierade. Vi förändrar Typer::AnimalType till Typer::AnimalInterface.
Nästa steg är att lägga till en funktion som returnerar en typ från Typer :: DjurInterface och även lägga till en lista över orphan_types, dvs. typer som aldrig används direkt:
modul Typer
modul AnimalInterface
include Typer::BaseInterface
fält :namn, Sträng, null: false
fält :age, Heltal, null: false
definition_metoder do
def resolve_type(obj, ctx)
if obj.is_a?(Elephant)
ElefantTyp
elsif obj.is_a?(Katt)
KattTyp
elsif obj.is_a?(Hund)
HundTyp
slut
slut
slut
orphan_types Typer::ElephantType, Typer::CatType, Typer::DogType
slut
slut
Tack vare denna enkla procedur har frågan en mindre komplex form:
{
allaZoo : {
djurpark: {
namn
stad
djur: {
__typenamn
namn
ålder
... på ElephantType {
snabelLängd
}
... på CatType {
hårTyp
}
... på DogType {
ras
}
}
}
}
}
Sammanfattning
GraphQL är en riktigt bra lösning. Om du inte känner till det ännu, prova det. Lita på mig, det är värt det. Den gör ett bra jobb med att lösa problem som uppstår i till exempel REST API:er. Som jag visade ovan, polymorfism är inte ett verkligt hinder för det. Jag presenterade två metoder för att ta itu med det. Påminnelse:
Om du arbetar med en lista över objekt som har en gemensam bas eller ett gemensamt gränssnitt - använd gränssnitten,
Om du arbetar med en lista med objekt med en annan struktur, använd ett annat gränssnitt - använd unionen