Neste artigo, apresentarei o uso de polimorfismo no GraphQL. Antes de começar, porém, vale a pena relembrar o que é polimorfismo e o que é GraphQL.
Polimorfismo
Polimorfismo é um componente essencial do programação orientada para os objectos. Para simplificar, baseia-se no facto de os objectos de classes diferentes terem acesso à mesma interface, o que significa que podemos esperar de cada um deles a mesma funcionalidade, mas não necessariamente implementada da mesma forma. Em Rubi criadores pode obter polimorfismo de três formas:
Herança
Herança consiste em criar uma classe-mãe e classes-filhas (ou seja, herdar da classe-mãe). As subclasses recebem a funcionalidade da classe-mãe e permitem-lhe também alterar e acrescentar uma funcionalidade.
Exemplo:
classe Documento
attr_reader :nome
fim
class PDFDocument < Document
def extensão
:pdf
end
fim
classe ODTDocument < Documento
def extensão
:odt
end
fim
Módulos
Módulos em Rubi têm muitas utilizações. Uma delas são os mixins (leia mais sobre mixins em A derradeira análise: Ruby vs. Python). Os mixins em Ruby podem ser utilizados de forma semelhante às interfaces noutras linguagens de programação (por exemplo, em Java), por exemplo, pode definir neles métodos comuns aos objectos que irão conter um determinado mixin. É uma boa prática incluir métodos só de leitura nos módulos, ou seja, métodos que não modificam o estado deste objeto.
Exemplo:
módulo Taxable
def imposto
preço * 0,23
fim
fim
classe Carro
include Taxable
attr_reader :price
end
class Livro
include Taxable
attr_reader :preço
fim
Dactilografia de patos
Esta é uma das principais caraterísticas das linguagens tipadas dinamicamente. O nome vem do famoso teste: se parece um pato, nada como um pato e grasna como um pato, então provavelmente é um pato. O teste programador não tem de estar interessado em saber a que classe pertence o objeto dado. O que é importante são os métodos que podem ser chamados a este objeto.
Utilizando as classes definidas no exemplo acima:
classe Carro
attr_reader :preço
def initialize(price)
@preço = preço
fim
fim
classe Livro
attr_reader :preço
def initialize(price)
@preço = preço
fim
fim
carro = Carro.new(20.0)
livro = Livro.new(10.0)
[carro, livro].map(&:preço
GrapQL
GraphQL é uma linguagem de consulta relativamente nova para APIs. As suas vantagens incluem o facto de ter uma sintaxe muito simples e, além disso, o cliente decide exatamente o que pretende obter, uma vez que cada cliente recebe exatamente o que pretende e nada mais.
Isto é provavelmente tudo o que precisamos de saber neste momento. Portanto, vamos ao que interessa.
Apresentação do problema
Para melhor compreender o problema e a sua solução, vamos criar um exemplo. Seria bom que o exemplo fosse original e bastante realista. Um exemplo que cada um de nós podem encontrar um dia. E que tal... animais? Sim! Óptima ideia!
Suponhamos que temos uma aplicação backend escrita em Ruby on Rails. Já está adaptado para lidar com o esquema acima. Vamos também assumir que já temos GraphQL configurado. Pretendemos que o cliente possa efetuar um pedido de informação dentro da seguinte estrutura:
{
allZoos : {
zoo: {
nome
cidade
animais: {
...
}
}
}
}
O que deve ser colocado em vez dos três pontos para obter a informação em falta - descobriremos mais tarde.
Implementação
A seguir, apresento os passos necessários para atingir o objetivo.
Adicionar uma consulta ao QueryType
Primeiro, é necessário definir o que significa exatamente a consulta allZoos. Para isso, temos de visitar o ficheiroapp/graphql/types/query_type.rb e definir a consulta:
módulo Types
classe QueryType < Types::BaseObject
campo :all_zoos, [Types::ZooType], null: false
def todos_zoos
Zoo.all
end
end
fim
A consulta já está definida. Agora é altura de definir os tipos de retorno.
Definição de tipos
O primeiro tipo necessário será o ZooType. Vamos defini-lo no ficheiro app/graphql/types/ zoo_type.rb:
módulo Types
classe ZooType < Types::BaseObject
campo :nome, String, null: false
campo :cidade, String, null: false
campo :animais, [Types::AnimalType], null: false
fim
fim
Agora é altura de definir o tipo AnimalType:
módulo Tipos
class AnimalType < Types::BaseUnion
tipos_possíveis ElephantType, CatType, DogType
def self.resolve_type(obj, ctx)
if obj.is_a?(Elephant)
ElefanteTipo
elsif obj.is_a?(Cat)
CatType
elsif obj.is_a?(Cão)
TipoCão
fim
fim
end
fim
Temos de enumerar todos os tipos que podem constituir uma determinada união. 3) Substituímos a função self.resolve_object(obj, ctx),que deve devolver o tipo de um determinado objeto.
O próximo passo é definir os tipos de animais. No entanto, sabemos que alguns campos são comuns a todos os animais. Vamos incluí-los no tipo AnimalInterface:
módulo Types
módulo AnimalInterface
include Types::BaseInterface
campo :nome, String, null: false
campo :idade, Integer, null: false
fim
fim
Com esta interface, podemos proceder à definição dos tipos de animais específicos:
módulo Types
class ElephantType < Types::BaseObject
implementa Types::AnimalInterface
campo :comprimento_do_tronco, Float, null: false
fim
fim
módulo Tipos
class CatType < Types::BaseObject
implementa Types::AnimalInterface
campo :hair_type, String, null: false
fim
fim
módulo Tipos
class DogType < Types::BaseObject
implementa Types::AnimalInterface
campo :raça, String, null: false
fim
fim
É isso mesmo! Pronto! Uma última pergunta: como é que podemos utilizar o que fizemos do lado do cliente?
Construir a consulta
{
allZoos : {
zoo: {
nome
cidade
animais: {
__typename
... on ElephantType {
nome
idade
trunkLength
}
... on CatType {
nome
idade
hairType
}
... em DogType {
nome
idade
raça
}
}
}
}
}
Podemos utilizar um campo adicional __typename aqui, que devolverá o tipo exato de um determinado elemento (por exemplo, CatType). Qual será o aspeto de um exemplo de resposta?
É evidente uma desvantagem desta abordagem. Na consulta, temos de introduzir o nome e a idade em cada tipo, apesar de sabermos que todos os animais têm estes campos. Isto não é incómodo quando a coleção contém objectos completamente diferentes. No entanto, neste caso, os animais partilham quase todos os campos. Poderá ser melhorado de alguma forma?
Claro que sim! Fazemos a primeira alteração no ficheiro app/graphql/types/zoo_type.rb:
módulo Types
classe ZooType < Types::BaseObject
campo :nome, String, null: false
campo :cidade, String, null: false
campo :animais, [Types::AnimalInterface], null: false
fim
fim
Já não precisamos da união que definimos anteriormente. Mudamos Types::AnimalType para Types::AnimalInterface.
O próximo passo é adicionar uma função que retorna um tipo de Tipos :: Interface Animal e também adicionar uma lista de orphan_types, ou seja, tipos que nunca são utilizados diretamente:
módulo Types
módulo AnimalInterface
include Types::BaseInterface
campo :nome, String, null: false
campo :idade, Integer, null: false
métodos_de_definição do
def resolve_type(obj, ctx)
se obj.is_a?(Elefante)
ElephantType
elsif obj.is_a?(Cat)
CatType
elsif obj.is_a?(Cão)
TipoCão
fim
fim
end
orphan_types Types::ElephantType, Types::CatType, Types::DogType
fim
end
Graças a este procedimento simples, a consulta tem uma forma menos complexa:
{
allZoos : {
zoo: {
nome
cidade
animais: {
__typename
nome
idade
... on ElephantType {
trunkLength
}
... em CatType {
hairType
}
... em DogType {
raça
}
}
}
}
}
Resumo
GraphQL é uma solução realmente óptima. Se ainda não a conhece, experimente-a. Confie em mim, vale a pena. Ela faz um ótimo trabalho na solução de problemas que aparecem, por exemplo, em APIs REST. Como mostrei acima, polimorfismo não é um verdadeiro obstáculo para ele. Apresentei dois métodos para o resolver. Lembrete:
Se operar sobre uma lista de objectos com uma base comum ou uma interface comum - utilize as interfaces,
Se operar numa lista de objectos com uma estrutura diferente, utilize uma interface diferente - utilize a união