V tomto článku představím použití polymorfismu v jazyce GraphQL. Než však začnu, je vhodné připomenout, co polymorfismus a GraphQL jsou.
Polymorfismus
Polymorfismus je klíčovou součástí objektově orientované programování. Zjednodušeně řečeno, vychází z toho, že objekty různých tříd mají přístup ke stejnému rozhraní, což znamená, že od každého z nich můžeme očekávat stejnou funkčnost, která však nemusí být nutně implementována stejným způsobem. Na adrese Ruby vývojáři může získat polymorfismus třemi způsoby:
Dědictví
Dědictví spočívá ve vytvoření nadřazené třídy a podřazených tříd (tj. dědičných od nadřazené třídy). Podtřídy přebírají funkčnost rodičovské třídy a také umožňují měnit a přidávat funkčnost.
Příklad:
třída Document
attr_reader :name
end
třída PDFDocument < Document
def rozšíření
:pdf
end
end
třída ODTDocument < Dokument
def rozšíření
:odt
end
end
Moduly
Moduly v Ruby mají mnohostranné využití. Jedním z nich jsou mixiny (více o mixinech se dočtete v článku Konečné rozdělení: Ruby vs. Python). Mixiny v jazyce Ruby lze používat podobně jako rozhraní v jiných programovacích jazycích (např. v Java), můžete v nich například definovat metody společné pro objekty, které budou obsahovat daný mixin. Dobrou praxí je zařazovat do modulů metody určené pouze pro čtení, tedy metody, které nebudou měnit stav tohoto objektu.
Příklad:
modul Zdanitelné
def tax
cena * 0,23
end
end
třída Auto
include Taxable
attr_reader :price
end
třída Book
include Taxable
attr_reader :price
end
Psaní na klávesnici Duck
To je jedna z klíčových vlastností dynamicky typovaných jazyků. Název pochází ze známého testu, že pokud to vypadá jako kachna, plave to jako kachna a kváká to jako kachna, pak je to pravděpodobně kachna. Na adrese programátor nemusí zajímat, do které třídy daný objekt patří. Důležité jsou metody, které lze nad tímto objektem volat.
Pomocí tříd definovaných ve výše uvedeném příkladu:
třída Auto
attr_reader :price
def inicializovat(cena)
@price = price
end
end
třída Book
attr_reader :price
def inicializovat(cena)
@price = price
end
end
car = Car.new(20.0)
book = Book.new(10.0)
[car, book].map(&:price
GrapQL
GraphQL je relativně nový dotazovací jazyk pro rozhraní API. Mezi jeho výhody patří to, že má velmi jednoduchou syntaxi a navíc klient rozhoduje, co přesně chce získat, protože každý zákazník dostane přesně to, co chce, a nic jiného.
To je asi vše, co v tuto chvíli potřebujeme vědět. Přejděme tedy k věci.
Představení problému
Abychom co nejlépe pochopili problém a jeho řešení, vytvoříme si příklad. Bylo by dobré, kdyby byl příklad originální a zároveň poměrně přízemní. Takový, aby každý z nás se může jednoho dne setkat. Co třeba... zvířata? Ano! Skvělý nápad!
Předpokládejme, že máme backendovou aplikaci napsanou v jazyce Ruby on Rails. Je již přizpůsoben pro zpracování výše uvedeného schématu. Předpokládejme také, že již máme GraphQL konfigurováno. Chceme klientovi umožnit zadat dotaz v následující struktuře:
{
allZoos : {
zoo: {
name
město
zvířata: {
...
}
}
}
}
Co je třeba vložit místo tří teček, abychom získali chybějící informace - zjistíme později.
Provádění
Níže uvádím kroky potřebné k dosažení cíle.
Přidání dotazu do QueryType
Nejprve je třeba definovat, co přesně znamená dotaz allZoos. Za tímto účelem musíme navštívit souborapp/graphql/types/query_type.rb a definujte dotaz:
Typy modulů
třída QueryType < Types::BaseObject
field :all_zoos, [Types::ZooType], null: false
def all_zoos
Zoo.all
end
end
end
Dotaz je již definován. Nyní je čas definovat návratové typy.
Definice typů
Prvním požadovaným typem bude ZooType. Definujme jej v souboru app/graphql/types/ zoo_type.rb:
Typy modulů
class ZooType < Types::BaseObject
field :name, String, null: false
field :city, String, null: false
pole :animals, [Types::AnimalType], null: false
end
end
Nyní je čas definovat typ AnimalType:
Typy modulů
třída 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
Musíme uvést všechny typy, které mohou tvořit danou unii. 3.Přepíšeme funkci self.resolve_object(obj, ctx),který musí vrátit typ daného objektu.
Dalším krokem je definování typů zvířat. Víme však, že některá pole jsou společná všem živočichům. Zařaďme je do typu AnimalInterface:
Typy modulů
modul AnimalInterface
include Types::BaseInterface
field :name, String, null: false
field :age, Integer, null: false
end
end
Po vytvoření tohoto rozhraní můžeme přejít k definování typů konkrétních zvířat:
Typy modulů
class ElephantType < Types::BaseObject
implementuje Types::AnimalInterface
field :trunk_length, Float, null: false
end
end
modul Typy
class CatType < Types::BaseObject
implementuje Types::AnimalInterface
field :hair_type, String, null: false
end
end
modul Typy
class DogType < Types::BaseObject
implementuje Types::AnimalInterface
field :breed, String, null: false
end
end
A je to! Připraveno! Poslední otázka: Jak můžeme využít to, co jsme udělali na straně klienta?
Sestavení dotazu
{
allZoos : {
zoo: {
name
město
zvířata: {
__typename
... on ElephantType {
name
věk
trunkLength
}
... on CatType {
name
age
hairType
}
... on DogType {
name
age
plemeno
}
}
}
}
}
Zde můžeme použít další pole __typename, které vrátí přesný typ daného prvku (např. CatType). Jak bude vypadat ukázková odpověď?
Jeden nedostatek tohoto přístupu je zřejmý. V dotazu musíme u každého typu zadat jméno a věk, přestože víme, že tato pole mají všechna zvířata. To však nevadí, pokud kolekce obsahuje zcela odlišné objekty. V tomto případě však mají zvířata téměř všechna pole společná. Dá se to nějak vylepšit?
Samozřejmě! Provedeme první změnu v souboru app/graphql/types/zoo_type.rb:
Typy modulů
class ZooType < Types::BaseObject
field :name, String, null: false
field :city, String, null: false
pole :animals, [Types::AnimalInterface], null: false
end
end
Dříve definovanou unii již nepotřebujeme. Změníme Types::AnimalType na Types::AnimalInterface.
Dalším krokem je přidání funkce, která vrací typ z Typy :: AnimalInterface a také přidat seznam orphan_types, tedy typů, které se nikdy přímo nepoužívají:
Typy modulů
modul AnimalInterface
include Types::BaseInterface
field :name, String, null: false
field :age, Integer, 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
Díky tomuto jednoduchému postupu má dotaz méně složitý tvar:
{
allZoos : {
zoo: {
name
město
zvířata: {
__typename
name
age
... on ElephantType {
trunkLength
}
... on CatType {
hairType
}
... on DogType {
plemeno
}
}
}
}
}
Souhrn
GraphQL je opravdu skvělým řešením. Pokud ho ještě neznáte, vyzkoušejte ho. Věřte mi, stojí to za to. Odvádí skvělou práci při řešení problémů objevujících se například v rozhraních REST API. Jak jsem ukázal výše, polymorfismus není pro něj skutečnou překážkou. Představil jsem dvě metody, jak ji řešit. Připomínáme:
Pokud pracujete se seznamem objektů se společným základem nebo společným rozhraním - použijte rozhraní,
Pokud pracujete se seznamem objektů s jinou strukturou, použijte jiné rozhraní - použijte union.