I denne artikel vil jeg præsentere brugen af polymorfisme i GraphQL. Før jeg går i gang, er det dog værd at huske på, hvad polymorfisme og GraphQL er.
Polymorfisme
Polymorfisme er en nøglekomponent i objektorienteret programmering. For at forenkle tingene er det baseret på det faktum, at objekter i forskellige klasser har adgang til det samme interface, hvilket betyder, at vi kan forvente den samme funktionalitet fra hver af dem, men ikke nødvendigvis implementeret på samme måde. I Ruby-udviklere kan opnå polymorfisme på tre måder:
Nedarvning
Nedarvning består i at skabe en overordnet klasse og underordnede klasser (dvs. arve fra den overordnede klasse). Underklasser får forælderklassens funktionalitet og giver dig også mulighed for at ændre og tilføje funktionalitet.
Et eksempel:
klasse Dokument
attr_reader :navn
slut
class PDFDocument < Dokument
def udvidelse
:pdf
end
slut
class ODTDocument < Dokument
def udvidelse
:odt
end
end
Moduler
Moduler i Ruby har mange anvendelsesmuligheder. En af dem er mixins (læs mere om mixins i Den ultimative opdeling: Ruby vs. Python). Mixins i Ruby kan bruges på samme måde som interfaces i andre programmeringssprog (f.eks. i Java), kan man f.eks. definere metoder, der er fælles for de objekter, der skal indeholde et givet mixin. Det er god praksis at inkludere skrivebeskyttede metoder i moduler, dvs. metoder, der ikke ændrer objektets tilstand.
Et eksempel:
modul Skattepligtig
def skat
pris * 0,23
slut
slut
klasse Bil
include Skattepligtig
attr_reader :pris
end
klasse Bog
inkluderer skattepligtig
attr_reader :pris
end
Anden skriver
Det er en af nøglefunktionerne i dynamisk typede sprog. Navnet kommer fra den berømte test: Hvis den ligner en and, svømmer som en and og kvækker som en and, så er det nok en and. Den programmør behøver ikke at interessere sig for, hvilken klasse det givne objekt tilhører. Det, der er vigtigt, er de metoder, der kan kaldes på dette objekt.
Ved hjælp af de klasser, der er defineret i eksemplet ovenfor:
klasse Bil
attr_reader :pris
def initialize(pris)
@pris = pris
slut
slut
klasse Bog
attr_reader :pris
def initialize(pris)
@pris = pris
slut
slut
car = Car.new(20.0)
book = Book.new(10.0)
[bil, bog].map(&:pris
GrapQL
GraphQL er et relativt nyt forespørgselssprog til API'er. Fordelene er, at det har en meget enkel syntaks, og desuden bestemmer kunden selv, hvad han eller hun vil have, da hver kunde får præcis det, han eller hun vil have, og intet andet.
Det er nok alt, hvad vi har brug for at vide lige nu. Så lad os komme til sagen.
Præsentation af problemet
Lad os lave et eksempel for at forstå problemet og dets løsning bedst muligt. Det ville være godt, hvis eksemplet både var originalt og ret jordnært. Et, som vi alle kan støde på en dag. Hvad med ... dyr? Ja, ja! Det er en god idé!
Antag, at vi har en backend-applikation skrevet i Ruby on Rails. Den er allerede tilpasset til at håndtere ovenstående skema. Lad os også antage, at vi allerede har GraphQL konfigureret. Vi ønsker at gøre det muligt for kunden at foretage en forespørgsel inden for følgende struktur:
{
alleZoos : {
zoo: {
navn
by
dyr: {
...
}
}
}
}
Hvad der skal sættes i stedet for de tre prikker for at få de manglende oplysninger - det finder vi ud af senere.
Implementering
Nedenfor vil jeg præsentere de trin, der er nødvendige for at nå målet.
Tilføjelse af en forespørgsel til QueryType
Først skal du definere, hvad forespørgslen allZoos helt præcist betyder. For at gøre dette skal vi besøge filenapp/graphql/types/query_type.rb og definere forespørgslen:
modul Typer
klasse QueryType < Types::BaseObject
field :all_zoos, [Types::ZooType], null: false
def all_zoos
Zoo.alle
end
end
slut
Forespørgslen er allerede defineret. Nu er det tid til at definere returtyperne.
Definition af typer
Den første type, der kræves, er ZooType. Lad os definere den i filen app/graphql/types/ zoo_type.rb:
modul Typer
class ZooType < Types::BaseObject
field :name, String, null: false
field :city, String, null: false
field :animals, [Types::AnimalType], null: false
slut
end
Nu er det tid til at definere typen AnimalType:
modul Typer
class AnimalType < Types::BaseUnion
possible_types ElephantType, CatType, DogType
def self.resolve_type(obj, ctx)
if obj.is_a?(Elephant)
ElefantType
elsif obj.is_a?(Cat)
CatType
elsif obj.is_a?(Dog)
DogType
slut
slut
slut
slut
Vi er nødt til at liste alle typer, der kan udgøre en given union. 3. Vi tilsidesætter funktionen self.resolve_object(obj, ctx),som skal returnere typen af et givet objekt.
Næste skridt er at definere dyretyperne. Vi ved dog, at nogle felter er fælles for alle dyr. Lad os inkludere dem i typen AnimalInterface:
modul Typer
modul AnimalInterface
include Types::BaseInterface
field :name, String, null: false
field :age, Heltal, null: false
slut
slut
Når vi har denne grænseflade, kan vi fortsætte med at definere typerne af specifikke dyr:
modul Typer
class ElephantType < Types::BaseObject
implementerer Types::AnimalInterface
field :trunk_length, Float, null: false
slut
end
modul Typer
class CatType < Types::BaseObject
implementerer Types::AnimalInterface
field :hair_type, String, null: false
slut
end
modul Typer
class DogType < Types::BaseObject
implementerer Types::AnimalInterface
field :race, String, null: false
slut
end
Så er det nu! Så er vi klar! Et sidste spørgsmål: Hvordan kan vi bruge det, vi har gjort fra klientens side?
Opbygning af forespørgslen
{
alleZoos : {
zoo: {
navn
by
animals: {
__typenavn
... on ElephantType {
navn
alder
snabellængde
}
... på CatType {
navn
alder
hairType
}
... on DogType {
navn
alder
race
}
}
}
}
}
Vi kan bruge et ekstra __typename-felt her, som vil returnere den nøjagtige type af et givet element (f.eks. CatType). Hvordan vil et eksempel på et svar se ud?
En ulempe ved denne tilgang er tydelig. I forespørgslen skal vi indtaste navn og alder i hver type, selv om vi ved, at alle dyr har disse felter. Det er ikke generende, når samlingen indeholder helt forskellige objekter. Men i dette tilfælde deler dyrene næsten alle felter. Kan det forbedres på en eller anden måde?
Naturligvis! Vi foretager den første ændring i filen app/graphql/types/zoo_type.rb:
modul Typer
class ZooType < Types::BaseObject
field :name, String, null: false
field :city, String, null: false
field :animals, [Types::AnimalInterface], null: false
slut
end
Vi har ikke længere brug for den forening, vi tidligere har defineret. Vi ændrer os Typer::AnimalType til Typer::AnimalInterface.
Næste skridt er at tilføje en funktion, der returnerer en type fra Typer :: AnimalInterface og også tilføje en liste over orphan_types, altså typer, der aldrig bliver brugt direkte:
modul Typer
modul AnimalInterface
include Types::BaseInterface
field :name, String, null: false
field :age, Heltal, null: false
definition_methods do
def resolve_type(obj, ctx)
if obj.is_a?(Elephant)
ElefantType
elsif obj.is_a?(Cat)
CatType
elsif obj.is_a?(Dog)
DogType
slut
slut
slut
orphan_types Types::ElephantType, Types::CatType, Types::DogType
slut
slut
Takket være denne enkle procedure har forespørgslen en mindre kompleks form:
{
alleZoos : {
zoo: {
navn
by
animals: {
__typenavn
navn
alder
... på ElephantType {
snabelLængde
}
... på CatType {
hairType
}
... på DogType {
race
}
}
}
}
}
Sammenfatning
GraphQL er en rigtig god løsning. Hvis du ikke kender den endnu, så prøv den. Tro mig, det er det værd. Den gør et godt stykke arbejde med at løse problemer, der opstår i for eksempel REST API'er. Som jeg viste ovenfor, polymorfisme er ikke en reel hindring for det. Jeg præsenterede to metoder til at tackle det. En påmindelse:
Hvis du arbejder med en liste af objekter med en fælles base eller en fælles grænseflade, skal du bruge grænsefladerne,
Hvis du arbejder med en liste af objekter med en anden struktur, skal du bruge en anden grænseflade - brug unionen