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 Rubino sviluppatori 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.

Esempio di query in GraphQL:

{
  allUsers {
     utenti {
        id
        login
        email

       }
     }
   }

Esempio di risposta:

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

Probabilmente questo è tutto quello che c'è da sapere al momento. Quindi, veniamo al punto.

Presentazione 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 di ruby e grapql - animali

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

Cosa vediamo nel codice sopra?

  1. AnimalType eredita da Tipi::BaseUnion.
  2. 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?

{
  "allZoos": [

   {
      "name": "Natura Artis Magistra",
      "città": "Amsterdam",
      "animali": [
        {
          "__typename": "ElephantType"
          "nome": "Franco",
          "età": 28,
          "lunghezza della proboscide": 9.27
         },
         {
         "__typename": "DogType"
         "nome": "Jack",
         "età": 9,
         "razza": "Jack Russell Terrier"
        },
      ]
    }
  ]
} 

Analisi

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:

Per saperne di più

GraphQL Ruby. E le prestazioni?

Rotaie e altri mezzi di trasporto

Sviluppo di Rails con TMUX, Vim, Fzf + Ripgrep

it_ITItalian