Tässä artikkelissa esittelen polymorfismin käyttöä GraphQL:ssä. Ennen kuin aloitan, on kuitenkin syytä palauttaa mieleen, mitä polymorfismi ja GraphQL ovat.
Polymorfismi
Polymorfismi on keskeinen osa oliosuuntautunut ohjelmointi. Yksinkertaistaen se perustuu siihen, että eri luokkien objekteilla on pääsy samaan rajapintaan, mikä tarkoittaa, että voimme odottaa jokaiselta niistä samaa toiminnallisuutta, mutta sitä ei välttämättä ole toteutettu samalla tavalla. Osoitteessa Ruby-kehittäjät voi saada polymorfismi kolmella tavalla:
Perinnöllisyys
Perinnöllisyys koostuu vanhemman luokan ja lapsiluokkien luomisesta (eli vanhemman luokan periyttämisestä). Alaluokat saavat vanhemman luokan toiminnallisuuden ja mahdollistavat myös toiminnallisuuden muuttamisen ja lisäämisen.
Esimerkki:
luokka Document
attr_reader :name
end
class PDFDocument < Dokumentti
def extension
:pdf
end
end
class ODTDocument < Document
def extension
:odt
end
end
Moduulit
Moduulit Ruby on monia käyttötarkoituksia. Yksi niistä on yhdistelmät (lue lisää yhdistelmistä luvussa Lopullinen hajoaminen: Ruby vs. Python). Rubyn sekoituksia voidaan käyttää samalla tavalla kuin rajapintoja muissa ohjelmointikielissä (esim. Java), voit esimerkiksi määritellä niissä metodeja, jotka ovat yhteisiä objekteille, jotka sisältävät tietyn yhdistelmän. On hyvä käytäntö sisällyttää moduuleihin vain lukemiseen tarkoitettuja metodeja, eli metodeja, jotka eivät muuta tämän objektin tilaa.
Esimerkki:
moduuli Verotettava
def tax
hinta * 0.23
end
end
luokka Auto
include Taxable
attr_reader :price
end
luokka Book
include Taxable
attr_reader :price
end
Ankka kirjoittaminen
Tämä on yksi dynaamisesti tyypiteltyjen kielten tärkeimmistä ominaisuuksista. Nimi tulee kuuluisasta testistä: jos se näyttää ankalta, ui kuin ankka ja kukkoilee kuin ankka, se on todennäköisesti ankka. The ohjelmoija ei tarvitse olla kiinnostunut siitä, mihin luokkaan kyseinen objekti kuuluu. Tärkeää ovat metodit, joita voidaan kutsua tällä objektilla.
Käyttämällä yllä olevassa esimerkissä määriteltyjä luokkia:
luokka Auto
attr_reader :price
def initialize(hinta)
@price = hinta
end
end
luokka Book
attr_reader :price
def initialize(hinta)
@price = hinta
end
end
car = Car.new(20.0)
book = Book.new(10.0)
[auto, kirja].map(&:hinta)
GrapQL
GraphQL on suhteellisen uusi kyselykieli sovellusrajapintoja varten. Sen etuihin kuuluu se, että sen syntaksi on hyvin yksinkertainen, ja lisäksi asiakas päättää itse, mitä hän haluaa saada, sillä jokainen asiakas saa juuri sen, mitä hän haluaa, eikä mitään muuta.
Tämä on luultavasti kaikki, mitä meidän tarvitsee tällä hetkellä tietää. Mennään siis asiaan.
Ongelman esittely
Jotta ymmärtäisimme parhaiten ongelman ja sen ratkaisun, luodaan esimerkki. Olisi hyvä, jos esimerkki olisi sekä omaperäinen että melko maanläheinen. Sellainen, jonka jokainen meistä voi kohdata joskus. Miten olisi... eläimet? Kyllä! Loistava idea!
Oletetaan, että meillä on backend-sovellus, joka on kirjoitettu kielellä Ruby on Rails. Se on jo mukautettu edellä mainittua järjestelmää varten. Oletetaan myös, että meillä on jo GraphQL konfiguroitu. Haluamme antaa asiakkaalle mahdollisuuden tehdä kysely seuraavan rakenteen mukaisesti:
{
allZoos : {
zoo: {
name
kaupunki
eläimet: {
...
}
}
}
}
Mitä pitäisi laittaa kolmen pisteen tilalle, jotta saataisiin puuttuvat tiedot - se selviää myöhemmin.
Täytäntöönpano
Seuraavassa esittelen tavoitteen saavuttamiseen tarvittavat vaiheet.
Kyselyn lisääminen QueryType-luokkaan
Ensin on määriteltävä, mitä kysely allZoos tarkalleen ottaen tarkoittaa. Tätä varten meidän on käytävä tiedostossa nimeltäapp/graphql/types/query_type.rb ja määrittele kysely:
moduuli Tyypit
class QueryType < Types::BaseObject
kenttä :all_zoos, [Types::ZooType], null: false
def all_zoos
Zoo.all
end
end
end
Kysely on jo määritelty. Nyt on aika määritellä paluutyypit.
Tyyppien määritelmä
Ensimmäinen tarvittava tyyppi on ZooType. Määritellään se tiedostossa app/graphql/types/ zoo_type.rb:
moduuli Tyypit
class ZooType < Types::BaseObject
field :name, String, null: false
field :city, String, null: false
field :animals, [Types::AnimalType], null: false
end
end
Nyt on aika määritellä tyyppi AnimalType:
moduuli Tyypit
class AnimalType < Types::BaseUnion
possible_types ElephantType, CatType, DogType
def self.resolve_type(obj, ctx)
if obj.is_a?(Elefantti)
ElephantType
elsif obj.is_a?(Cat)
CatType
elsif obj.is_a?(Dog)
DogType
end
end
end
end
Meidän on lueteltava kaikki tyypit, jotka voivat muodostaa tietyn liiton. 3.Me ohitamme funktion self.resolve_object(obj, ctx),jonka on palautettava tietyn objektin tyyppi.
Seuraavaksi määritellään eläintyypit. Tiedämme kuitenkin, että jotkin kentät ovat kaikille eläimille yhteisiä. Sisällytetään ne tyyppiin AnimalInterface:
moduuli Tyypit
moduuli AnimalInterface
include Types::BaseInterface
field :name, String, null: false
field :age, Kokonaisluku, null: false
end
end
Kun tämä rajapinta on valmis, voimme määritellä tiettyjen eläinten tyypit:
moduuli Tyypit
class ElephantType < Types::BaseObject
implements Types::AnimalInterface
kenttä :trunk_length, Float, null: false
end
end
moduuli Types
class CatType < Types::BaseObject
implements Types::AnimalInterface
field :hair_type, String, null: false
end
end
moduuli Tyypit
class DogType < Types::BaseObject
implements Types::AnimalInterface
field :breed, String, null: false
end
end
Juuri noin! Valmiina! Vielä yksi kysymys: miten voimme käyttää sitä, mitä olemme tehneet asiakkaan puolella?
Kyselyn rakentaminen
{
allZoos : {
zoo: {
name
kaupunki
eläimet: {
__typename
... on ElephantType {
name
age
trunkLength
}
... on CatType {
name
age
hairType
}
... on DogType {
name
age
rotu
}
}
}
}
}
Voimme käyttää tässä ylimääräistä __typename-kenttää, joka palauttaa tietyn elementin tarkan tyypin (esim. CatType). Miltä esimerkkivastaus näyttää?
Yksi tämän lähestymistavan haittapuoli on ilmeinen. Kyselyssä meidän on syötettävä nimi ja ikä jokaiseen tyyppiin, vaikka tiedämme, että kaikilla eläimillä on nämä kentät. Tämä ei ole häiritsevää, kun kokoelma sisältää täysin erilaisia objekteja. Tässä tapauksessa eläimillä on kuitenkin lähes kaikki samat kentät. Voidaanko tätä jotenkin parantaa?
Totta kai! Teemme ensimmäisen muutoksen tiedostoon app/graphql/types/zoo_type.rb:
moduuli Tyypit
class ZooType < Types::BaseObject
field :name, String, null: false
field :city, String, null: false
field :animals, [Types::AnimalInterface], null: false
end
end
Emme enää tarvitse aiemmin määrittelemäämme liittoa. Muutamme Types::AnimalType osoitteeseen Types::AnimalInterface.
Seuraava askel on lisätä funktio, joka palauttaa tyypin, joka on peräisin Tyypit :: AnimalInterface ja lisää myös luettelo orphan_types-tyypeistä, eli tyypeistä, joita ei koskaan käytetä suoraan:
moduuli Tyypit
moduuli AnimalInterface
include Types::BaseInterface
field :name, String, null: false
field :age, Kokonaisluku, 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
Tämän yksinkertaisen menettelyn ansiosta kysely on vähemmän monimutkainen:
{
allZoos : {
zoo: {
name
kaupunki
eläimet: {
__typename
name
ikä
... on ElephantType {
trunkLength
}
... on CatType {
hairType
}
... on DogType {
breed
}
}
}
}
}
Yhteenveto
GraphQL on todella hyvä ratkaisu. Jos et vielä tiedä sitä, kokeile sitä. Luota minuun, se on sen arvoista. Se tekee loistavaa työtä ratkaistessaan ongelmia, joita esiintyy esimerkiksi REST API:issa. Kuten edellä osoitin, polymorfismi ei ole todellinen este sille. Esittelin kaksi menetelmää sen ratkaisemiseksi. Muistutus:
Jos käytät luetteloa objekteista, joilla on yhteinen pohja tai yhteinen rajapinta - käytä rajapintoja,
Jos operoit rakenteeltaan erilaisten objektien luettelolla, käytä eri rajapintaa - käytä unionia