In questo articolo presenterò l'uso del polimorfismo in GraphQL. Prima di iniziare, però, vale la pena ricordare cosa sono il polimorfismo e GraphQL.
Polimorfismo
Polimorfismo è un componente chiave di programmazione orientata agli oggetti. Per semplificare le cose, si basa sul fatto che oggetti di classi diverse hanno accesso alla stessa interfaccia, il che significa che possiamo aspettarci da ciascuno di essi la stessa funzionalità, ma non necessariamente implementata nello stesso modo. In Sviluppatori Ruby può ottenere polimorfismo in tre modi:
Eredità
Eredità consiste nel creare una classe madre e delle classi figlie (cioè che ereditano dalla classe madre). Le sottoclassi ricevono le funzionalità della classe madre e consentono anche di modificare e aggiungere funzionalità.
Esempio:
classe Documento
attr_reader :name
fine
classe PDFDocument < Documento
def estensione
:pdf
fine
fine
classe ODTDocument < Documento
def estensione
:odt
fine
fine
Moduli
Moduli in Rubino hanno molti usi. Uno di questi è rappresentato dai mixin (per saperne di più sui mixin, vedere la sezione La scomposizione definitiva: Ruby vs. Python). I mixin in Ruby possono essere usati in modo simile alle interfacce in altri linguaggi di programmazione (ad esempio, in Java), per esempio, si possono definire in essi metodi comuni agli oggetti che conterranno un determinato mixin. È buona norma includere nei moduli metodi di sola lettura, quindi metodi che non modifichino lo stato dell'oggetto.
Esempio:
modulo Imponibile
def imposta
prezzo * 0,23
fine
fine
classe Auto
include Taxable
attr_reader :price
fine
classe Libro
include Taxable
attr_reader :price
fine
Dattilografia per anatre
Questa è una delle caratteristiche principali dei linguaggi tipizzati dinamicamente. Il nome deriva dal famoso test: se sembra un'anatra, nuota come un'anatra e starnazza come un'anatra, allora probabilmente è un'anatra. Il programmatore non deve interessarsi a quale classe appartiene l'oggetto dato. Ciò che conta sono i metodi che possono essere richiamati su questo oggetto.
Utilizzando le classi definite nell'esempio precedente:
classe Auto
attr_reader :price
def initialize(prezzo)
@prezzo = prezzo
fine
fine
classe Libro
attr_reader :price
def initialize(prezzo)
@prezzo = prezzo
fine
fine
car = Car.new(20.0)
book = Book.new(10.0)
[auto, libro].map(&:prezzo
GrapQL
GraphQL è un linguaggio di interrogazione relativamente nuovo per le API. Tra i suoi vantaggi c'è il fatto che ha una sintassi molto semplice e, inoltre, è il cliente a decidere cosa vuole ottenere esattamente, poiché ogni cliente riceve esattamente ciò che desidera e nient'altro.
Probabilmente questo è tutto quello che c'è da sapere al momento. Quindi, veniamo al punto.
Presentazione del problema
Per capire meglio il problema e la sua soluzione, creiamo un esempio. Sarebbe bene che l'esempio fosse originale e abbastanza concreto. Uno di quelli che ognuno di noi può incontrare un giorno. Che ne dite di... animali? Sì! Ottima idea!
Supponiamo di avere un'applicazione backend scritta in Ruby on Rails. È già adattato per gestire lo schema precedente. Supponiamo inoltre di avere già GraphQL configurato. Vogliamo consentire al cliente di effettuare una richiesta all'interno della seguente struttura:
{
allZoos : {
zoo: {
nome
città
animali: {
...
}
}
}
}
Cosa si dovrebbe mettere al posto dei tre punti per ottenere le informazioni mancanti - lo scopriremo più avanti.
Attuazione
Di seguito presenterò i passi necessari per raggiungere l'obiettivo.
Aggiunta di una query a QueryType
Per prima cosa, è necessario definire il significato esatto della query allZoos. Per farlo, occorre visitare il fileapp/graphql/types/query_type.rb e definire la query:
modulo Tipi
classe QueryType < Types::BaseObject
field :all_zoos, [Types::ZooType], null: false
def tutti_zoo
Zoo.all
fine
fine
fine
La query è già definita. Ora è il momento di definire i tipi di ritorno.
Definizione dei tipi
Il primo tipo richiesto sarà ZooType. Definiamolo nel file app/graphql/types/ zoo_type.rb:
modulo Tipi
classe ZooType < Types::BaseObject
campo :name, String, null: false
campo :city, Stringa, null: false
campo :animals, [Types::AnimalType], null: false
fine
fine
Ora è il momento di definire il tipo AnimalType:
modulo Tipi
classe AnimalType < Types::BaseUnion
possibili_tipi ElefanteTipo, GattoTipo, CaneTipo
def self.resolve_type(obj, ctx)
if obj.is_a?(Elefante)
Tipo Elefante
elsif obj.is_a?(Cat)
Tipo Gatto
elsif obj.is_a?(Dog)
Tipo Cane
fine
fine
fine
fine
Dobbiamo elencare tutti i tipi che possono comporre una determinata unione. 3.Sovrascriviamo la funzione self.resolve_object(obj, ctx),che deve restituire il tipo di un dato oggetto.
Il passo successivo consiste nel definire i tipi di animali. Tuttavia, sappiamo che alcuni campi sono comuni a tutti gli animali. Includiamoli nel tipo AnimalInterface:
modulo Tipi
modulo AnimalInterface
include Types::BaseInterface
campo :name, String, null: false
campo :age, Integer, null: false
fine
fine
Avendo questa interfaccia, possiamo procedere a definire i tipi di animali specifici:
modulo Tipi
classe ElephantType < Types::BaseObject
implementa Types::AnimalInterface
campo :lunghezza_tronco, Float, null: false
fine
fine
modulo Tipi
classe CatType < Types::BaseObject
implementa Types::AnimalInterface
campo :hair_type, String, null: false
fine
fine
modulo Tipi
classe DogType < Types::BaseObject
implementa Types::AnimalInterface
campo :razza, String, null: false
fine
fine
Ecco fatto! Pronti! Un'ultima domanda: come possiamo utilizzare ciò che abbiamo fatto dal lato del cliente?
Creazione della query
{
allZoos : {
zoo: {
nome
città
animali: {
__typename
... su ElephantType {
nome
età
lunghezza della proboscide
}
... su CatType {
nome
età
tipo di pelo
}
... su DogType {
nome
età
razza
}
}
}
}
}
Possiamo usare un campo aggiuntivo __typename, che restituirà il tipo esatto di un dato elemento (per esempio, CatType). Come sarà una risposta di esempio?
Questo approccio presenta uno svantaggio. Nella query, dobbiamo inserire il nome e l'età per ogni tipo, anche se sappiamo che tutti gli animali hanno questi campi. Questo non è un problema quando la collezione contiene oggetti completamente diversi. In questo caso, però, gli animali condividono quasi tutti i campi. Si può migliorare in qualche modo?
Naturalmente! Apportiamo la prima modifica al file app/graphql/types/zoo_type.rb:
modulo Tipi
classe ZooType < Types::BaseObject
campo :name, String, null: false
campo :city, Stringa, null: false
campo :animals, [Types::AnimalInterface], null: false
fine
fine
Non abbiamo più bisogno dell'unione che abbiamo definito prima. Cambiamo Tipi::AnimalType a Types::AnimalInterface.
Il passo successivo è aggiungere una funzione che restituisca un tipo da Tipi :: Interfaccia Animale e aggiungere anche un elenco di tipi orfani, cioè di tipi che non vengono mai utilizzati direttamente:
modulo Tipi
modulo AnimalInterface
include Types::BaseInterface
campo :name, String, null: false
campo :age, Integer, null: false
definizione_metodi do
def resolve_type(obj, ctx)
if obj.is_a?(Elefante)
Tipo Elefante
elsif obj.is_a?(Cat)
Tipo Gatto
elsif obj.is_a?(Dog)
Tipo Cane
fine
fine
fine
orphan_types Types::ElephantType, Types::CatType, Types::DogType
fine
fine
Grazie a questa semplice procedura, la query ha una forma meno complessa:
{
allZoos : {
zoo: {
nome
città
animali: {
__typename
nome
età
... su ElephantType {
lunghezza della proboscide
}
... su CatType {
tipoCapelli
}
... su DogType {
razza
}
}
}
}
}
Sintesi
GraphQL è una soluzione davvero eccezionale. Se non la conoscete ancora, provatela. Credetemi, ne vale la pena. Fa un ottimo lavoro nel risolvere i problemi che si presentano, ad esempio, nelle API REST. Come ho mostrato sopra, polimorfismo non è un vero e proprio ostacolo. Ho presentato due metodi per affrontarlo. Promemoria:
Se si opera su un elenco di oggetti con una base comune o un'interfaccia comune, utilizzare le interfacce,
Se si opera su un elenco di oggetti con una struttura diversa, si deve utilizzare un'interfaccia diversa: utilizzare l'unione