En este artículo, voy a presentar el uso del polimorfismo en GraphQL. Antes de empezar, sin embargo, vale la pena recordar lo que el polimorfismo y GraphQL son.
Polimorfismo
Polimorfismo es un componente clave de programación orientada a objetos. Para simplificar las cosas, se basa en el hecho de que los objetos de diferentes clases tienen acceso a la misma interfaz, lo que significa que podemos esperar de cada uno de ellos la misma funcionalidad, pero no necesariamente implementada de la misma manera. En Desarrolladores Ruby puede obtener polimorfismo de tres maneras:
Herencia
Herencia consiste en crear una clase padre y clases hijas (es decir, que heredan de la clase padre). Las subclases reciben la funcionalidad de la clase padre y también permiten cambiar y añadir una funcionalidad.
Por ejemplo:
clase Documento
attr_reader :nombre
fin
clase PDFDocument < Documento
def extensión
:pdf
end
end
clase ODTDocument < Documento
def extensión
:odt
end
end
Módulos
Módulos en Ruby tienen muchos usos. Uno de ellos son los mixins (más información sobre mixins en El desglose definitivo: Rubí vs. Python). Los mixins en Ruby pueden utilizarse de forma similar a las interfaces en otros lenguajes de programación (por ejemplo, en Java), por ejemplo, puedes definir en ellos métodos comunes a los objetos que contendrán un mixin determinado. Es una buena práctica incluir métodos de sólo lectura en los módulos, es decir, métodos que no modificarán el estado de este objeto.
Por ejemplo:
módulo Taxable
def impuesto
precio * 0,23
end
end
clase Coche
include Imponible
attr_reader :precio
end
clase Libro
include Imponible
attr_reader :precio
end
Mecanografía
Esta es una de las características clave de los lenguajes tipados dinámicamente. El nombre proviene de la famosa prueba: si parece un pato, nada como un pato y grazna como un pato, probablemente sea un pato. El sitio programador no tiene por qué interesarle a qué clase pertenece el objeto dado. Lo importante son los métodos que se pueden invocar sobre este objeto.
Utilizando las clases definidas en el ejemplo anterior:
clase Coche
attr_reader :precio
def inicializar(precio)
@precio = precio
end
fin
clase Libro
attr_reader :precio
def inicializar(precio)
@precio = precio
end
fin
coche = Coche.nuevo(20.0)
libro = Libro.nuevo(10.0)
[coche, libro].map(&:precio
GrapQL
GraphQL es un lenguaje de consulta relativamente nuevo para APIs. Entre sus ventajas destaca el hecho de que tiene una sintaxis muy sencilla y, además, el cliente decide qué quiere obtener exactamente, ya que cada cliente obtiene exactamente lo que quiere y nada más.
Esto es probablemente todo lo que necesitamos saber por el momento. Así que vayamos al grano.
Presentación del problema
Para entender mejor el problema y su solución, creemos un ejemplo. Sería bueno que el ejemplo fuera a la vez original y bastante realista. Uno con el que cada uno de nosotros pueda encontrarse algún día. ¿Qué tal... animales? Sí. Buena idea.
Supongamos que tenemos una aplicación backend escrita en Ruby on Rails. Ya está adaptado para manejar el esquema anterior. Supongamos también que ya tenemos GraphQL configurado. Queremos que el cliente pueda realizar una consulta con la siguiente estructura:
{
allZoos : {
zoo: {
nombre
ciudad
animales: {
...
}
}
}
}
Lo que hay que poner en lugar de los tres puntos para obtener la información que falta, lo averiguaremos más adelante.
Aplicación
A continuación presentaré los pasos necesarios para lograr el objetivo.
Añadir una consulta a QueryType
En primer lugar, hay que definir qué significa exactamente la consulta allZoos. Para ello, tenemos que visitar el archivoapp/graphql/types/query_type.rb y definir la consulta:
módulo Types
clase TipoConsulta < Types::BaseObject
campo :all_zoos, [Types::ZooType], null: false
def todos_zoos
Zoo.all
end
end
end
La consulta ya está definida. Ahora es el momento de definir los tipos de retorno.
Definición de los tipos
El primer tipo requerido será ZooType. Definámoslo en el archivo app/graphql/types/ zoo_type.rb:
módulo Types
clase ZooType < Tipos::BaseObjeto
campo :nombre, String, null: false
campo :ciudad, String, null: false
campo :animales, [Types::AnimalType], null: false
end
end
Ahora es el momento de definir el tipo AnimalType:
módulo Types
clase AnimalType < Types::BaseUnion
possible_types Tipo_elefante, Tipo_gato, Tipo_perro
def self.resolver_tipo(obj, ctx)
if obj.is_a?(Elefante)
Tipo_elefante
elsif obj.is_a?(Gato)
CatType
elsif obj.is_a?(Perro)
Tipo perro
end
end
end
end
Tenemos que enumerar todos los tipos que pueden componer una unión determinada. 3.Reemplazamos la función self.resolve_object(obj, ctx),que debe devolver el tipo de un objeto dado.
El siguiente paso consiste en definir los tipos de animales. Sin embargo, sabemos que algunos campos son comunes a todos los animales. Incluyámoslos en el tipo AnimalInterface:
módulo Types
módulo AnimalInterface
include Types::BaseInterface
campo :nombre, String, null: false
campo :edad, Integer, null: false
end
end
Teniendo esta interfaz, podemos proceder a definir los tipos de animales específicos:
módulo Types
clase TipoElefante < Tipos::ObjetoBase
implementa Types::AnimalInterface
campo :longitud_tronco, Float, null: false
end
end
módulo Types
clase TipoGato < Types::BaseObject
implementa Types::AnimalInterface
campo :tipo_pelo, String, null: false
end
end
módulo Types
clase TipoPerro < Types::BaseObject
implementa Types::AnimalInterface
campo :raza, String, null: false
end
end
¡Eso es! Listo. Una última pregunta: ¿cómo podemos utilizar lo que hemos hecho desde el lado del cliente?
Construcción de la consulta
{
allZoos : {
zoo: {
nombre
ciudad
animales: {
nombre
... en ElefanteTipo {
nombre
edad
trunkLength
}
... en CatType {
nombre
edad
hairType
}
... on TipoPerro {
nombre
edad
raza
}
}
}
}
}
Aquí podemos utilizar un campo adicional __typename, que devolverá el tipo exacto de un elemento dado (por ejemplo, CatType). ¿Qué aspecto tendrá una respuesta de ejemplo?
Un inconveniente de este enfoque es evidente. En la consulta, debemos introducir el nombre y la edad en cada tipo, aunque sepamos que todos los animales tienen estos campos. Esto no es molesto cuando la colección contiene objetos completamente diferentes. En este caso, sin embargo, los animales comparten casi todos los campos. ¿Se puede mejorar de alguna manera?
Por supuesto. Hacemos el primer cambio en el archivo app/graphql/types/zoo_type.rb:
módulo Types
clase ZooType < Tipos::BaseObjeto
campo :nombre, String, null: false
campo :ciudad, String, null: false
campo :animales, [Types::AnimalInterface], null: false
end
end
Ya no necesitamos la unión que hemos definido antes. Cambiamos Tipos::AnimalTipo a Tipos::InterfazAnimal.
El siguiente paso es añadir una función que devuelva un tipo de Tipos :: AnimalInterface y también añadir una lista de orphan_types, es decir, tipos que nunca se utilizan directamente:
módulo Types
módulo AnimalInterface
include Types::BaseInterface
campo :nombre, String, null: false
campo :edad, Integer, null: false
definition_methods do
def resolver_tipo(obj, ctx)
if obj.is_a?(Elefante)
Tipo_elefante
elsif obj.is_a?(Gato)
CatType
elsif obj.is_a?(Perro)
Tipo perro
end
end
end
orphan_types Tipos::ElefanteTipo, Tipos::GatoTipo, Tipos::PerroTipo
fin
end
Gracias a este sencillo procedimiento, la consulta tiene una forma menos compleja:
{
allZoos : {
zoo: {
nombre
ciudad
animales: {
nombre
nombre
edad
... en ElephantType {
trunkLength
}
... en TipoGato {
hairType
}
... on TipoPerro {
raza
}
}
}
}
}
Resumen
GraphQL es una solución realmente genial. Si aún no la conoces, pruébala. Créeme, merece la pena. Hace un gran trabajo resolviendo problemas que aparecen, por ejemplo, en las APIs REST. Como he mostrado anteriormente, polimorfismo no es un obstáculo real para ello. He presentado dos métodos para abordarlo. Recordatorio:
Si opera con una lista de objetos con una base común o una interfaz común, utilice las interfaces,
Si opera sobre una lista de objetos con una estructura diferente, utilice una interfaz diferente: utilice la unión