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 Ruby desarrolladores 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.

Ejemplo de consulta en GraphQL:

{
  allUsers {
     usuarios {
        id
        login
        correo electrónico

       }
     }
   }

Respuesta de muestra:

{
  "allUsers": {
    "users": [
     {
        "id": 1,
        "login": "user1",
        "email": "[email protected]"
      },
      {
        "id": 2,
        "login": "user2",
        "email": "[email protected]"
      },
    ]
  }
}

Esto es probablemente todo lo que necesitamos saber por el momento. Así que vayamos al grano.

Presentación del problema

To best understand the problem and its solution, let’s create an example. It would be good if the example was both original and fairly down-to-earth. One that each of us can encounter someday. How about… animals? Yes! Great idea!

polimorfismo ruby y grapql - animales

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

¿Qué vemos en el código ¿Arriba?

  1. El AnimalType hereda de Tipos::BaseUnion.
  2. 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?

{
  "allZoos": [

   {
      "name": "Natura Artis Magistra",
      "city": "Amsterdam",
      "animales": [
        {
          "__typename": "ElefanteTipo"
          "name": "Franco"
          "edad": 28,
          "trunkLength": 9.27
         },
         {
         "__typename": "DogType"
         "name": "Jack"
         "age": 9,
         "raza": "Jack Russell Terrier"
        },
      ]
    }
  ]
} 

Análisis

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:

Seguir leyendo

GraphQL Ruby. ¿Y el rendimiento?

Ferrocarriles y otros medios de transporte

Desarrollo Rails con TMUX, Vim, Fzf + Ripgrep

es_ESSpanish