I denne artikkelen vil jeg presentere bruken av polymorfisme i GraphQL. Før jeg begynner, er det imidlertid verdt å minne om hva polymorfisme og GraphQL er.
Polymorfisme
Polymorfisme er en nøkkelkomponent i objektorientert programmering. For å forenkle det hele er det basert på det faktum at objekter i ulike klasser har tilgang til det samme grensesnittet, noe som betyr at vi kan forvente samme funksjonalitet fra hver av dem, men ikke nødvendigvis implementert på samme måte. I Ruby-utviklere kan oppnå polymorfisme på tre måter:
Arv
Arv består i å opprette en overordnet klasse og underordnede klasser (dvs. som arver fra den overordnede klassen). Underklasser får funksjonaliteten til den overordnede klassen, og gjør det også mulig å endre og legge til funksjonalitet.
Eksempel:
klasse Dokument
attr_reader :navn
end
class PDFDocument < Dokument
def extension
:pdf
end
end
class ODTDocument < Dokument
def extension
:odt
end
end
Moduler
Moduler i Ruby har mange bruksområder. En av dem er mixins (les mer om mixins i Den ultimate sammenbruddet: Ruby vs. Python). Mixins i Ruby kan brukes på samme måte som grensesnitt i andre programmeringsspråk (f.eks. i Java), kan du for eksempel definere metoder som er felles for objekter som skal inneholde et gitt mixin. Det er lurt å inkludere skrivebeskyttede metoder i modulene, det vil si metoder som ikke endrer objektets tilstand.
Eksempel:
modul Taxable
def skatt
pris * 0,23
end
end
klasse Bil
include Taxable
attr_reader :price
end
class Book
include Taxable
attr_reader :pris
end
Skriving på and
Dette er et av de viktigste kjennetegnene ved dynamisk typede språk. Navnet kommer fra den berømte testen "hvis det ser ut som en and, svømmer som en and og kvakker som en and, så er det sannsynligvis en and". Den programmerer trenger ikke å være interessert i hvilken klasse det gitte objektet tilhører. Det som er viktig, er hvilke metoder som kan kalles på dette objektet.
Bruk klassene som er definert i eksempelet ovenfor:
klasse Bil
attr_reader :pris
def initialize(pris)
@price = pris
end
end
klasse Bok
attr_reader :pris
def initialize(pris)
@pris = pris
end
end
car = Car.new(20.0)
book = Book.new(10.0)
[bil, bok].map(&:pris
GrapQL
GraphQL er et relativt nytt spørrespråk for API-er. Fordelene er blant annet at det har en svært enkel syntaks, og i tillegg bestemmer kunden selv hva han eller hun vil ha, ettersom hver kunde får akkurat det han eller hun vil ha og ikke noe annet.
Dette er sannsynligvis alt vi trenger å vite for øyeblikket. Så, la oss komme til poenget.
Presentasjon av problemet
La oss lage et eksempel for å forstå problemet og løsningen på det best mulig. Det ville være bra om eksemplet var både originalt og ganske jordnært. Et eksempel som hver og en av oss kan støte på en dag. Hva med ... dyr? Ja! Det er en god idé!
Anta at vi har en backend-applikasjon skrevet i Ruby on Rails. Den er allerede tilpasset for å håndtere ordningen ovenfor. La oss også anta at vi allerede har GraphQL konfigurert. Vi ønsker å gjøre det mulig for klienten å gjøre en forespørsel innenfor følgende struktur:
{
allZoos : {
zoo: {
navn
by
animals: {
...
}
}
}
}
Hva som skal settes i stedet for de tre prikkene for å få den manglende informasjonen - det finner vi ut senere.
Implementering
Nedenfor vil jeg presentere trinnene som trengs for å nå målet.
Legge til en spørring i QueryType
Først må du definere hva spørringen allZoos egentlig betyr. For å gjøre dette må vi gå til filenapp/graphql/types/query_type.rb og definere spørringen:
modul Types
class QueryType < Types::BaseObject
field :all_zoos, [Types::ZooType], null: false
def all_zoos
Zoo.all
end
end
end
Spørringen er allerede definert. Nå er det på tide å definere returtypene.
Definisjon av typer
Den første typen som kreves, er ZooType. La oss 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
end
end
Nå er det på tide å 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)
ElephantType
elsif obj.is_a?(Cat)
CatType
elsif obj.is_a?(Dog)
DogType
end
end
end
end
Vi må liste opp alle typer som kan utgjøre en gitt union. 3. Vi overstyrer funksjonen self.resolve_object(obj, ctx),som må returnere typen til et gitt objekt.
Neste trinn er å definere dyretypene. Vi vet imidlertid at noen felt er felles for alle dyrene. La oss inkludere dem i typen AnimalInterface:
modul Typer
modul AnimalInterface
include Types::BaseInterface
field :name, String, null: false
field :age, Heltall, null: false
end
end
Med dette grensesnittet kan vi gå videre til å definere typer av spesifikke dyr:
modul Typer
class ElephantType < Types::BaseObject
implementerer Types::AnimalInterface
field :snabel_lengde, Float, null: false
end
end
modul Typer
class CatType < Types::BaseObject
implementerer Types::AnimalInterface
field :hair_type, String, null: false
end
end
modul Typer
class DogType < Types::BaseObject
implementerer Types::AnimalInterface
field :rase, String, null: false
end
end
Sånn, ja! Nå er vi klare! Et siste spørsmål: Hvordan kan vi bruke det vi har gjort fra kundens side?
Bygge opp spørringen
{
allZoos : {
zoo: {
navn
by
animals: {
__typename
... on ElephantType {
navn
alder
trunkLength
}
... on CatType {
navn
alder
hairType
}
... on DogType {
navn
alder
rase
}
}
}
}
}
Vi kan bruke et ekstra __typename-felt her, som returnerer den nøyaktige typen av et gitt element (f.eks. CatType). Hvordan vil et eksempel på et svar se ut?
En ulempe med denne tilnærmingen er åpenbar. I spørringen må vi skrive inn navn og alder i hver type, selv om vi vet at alle dyr har disse feltene. Dette er ikke så plagsomt når samlingen inneholder helt forskjellige objekter. I dette tilfellet deler imidlertid dyrene nesten alle feltene. Kan det forbedres på en eller annen måte?
Selvsagt! Vi gjør den første endringen 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
end
end
Vi trenger ikke lenger den unionen vi har definert tidligere. Vi endrer Types::AnimalType til Types::AnimalInterface.
Neste steg er å legge til en funksjon som returnerer en type fra Typer :: AnimalInterface og også legge til en liste over orphan_types, altså typer som aldri brukes direkte:
modul Typer
modul AnimalInterface
include Types::BaseInterface
field :name, String, null: false
field :age, Heltall, null: false
definition_methods do
def resolve_type(obj, ctx)
if obj.is_a?(Elephant)
ElephantType
elsif obj.is_a?(Cat)
CatType
elsif obj.is_a?(Dog)
DogType
end
end
end
orphan_types Types::ElephantType, Types::CatType, Types::DogType
end
end
Takket være denne enkle prosedyren har spørringen en mindre kompleks form:
{
allZoos : {
zoo: {
navn
by
animals: {
__typenavn
navn
alder
... on ElephantType {
trunkLength
}
... på CatType {
hairType
}
... on DogType {
rase
}
}
}
}
}
Sammendrag
GraphQL er en veldig bra løsning. Hvis du ikke vet det ennå, prøv det. Tro meg, det er verdt det. Den gjør en god jobb med å løse problemer som oppstår i for eksempel REST API-er. Som jeg viste ovenfor, polymorfisme er ikke et reelt hinder for det. Jeg presenterte to metoder for å takle det. Påminnelse:
Hvis du opererer på en liste over objekter med en felles base eller et felles grensesnitt - bruk grensesnittene,
Hvis du opererer på en liste med objekter med en annen struktur, må du bruke et annet grensesnitt - bruk union