Polymorfismi

Polymorfismi on keskeinen osa oliosuuntautunut ohjelmointi. Yksinkertaistaen se perustuu siihen, että eri luokkien objekteilla on pääsy samaan rajapintaan, mikä tarkoittaa, että voimme odottaa jokaiselta niistä samaa toiminnallisuutta, mutta sitä ei välttämättä ole toteutettu samalla tavalla. Osoitteessa Ruby kehittäjät voi saada polymorfismi kolmella tavalla:

Perinnöllisyys

Perinnöllisyys koostuu vanhemman luokan ja lapsiluokkien luomisesta (eli vanhemman luokan periyttämisestä). Alaluokat saavat vanhemman luokan toiminnallisuuden ja mahdollistavat myös toiminnallisuuden muuttamisen ja lisäämisen.

Esimerkki:

luokka Document
  attr_reader :name
end

class PDFDocument < Dokumentti
  def extension
    :pdf
  end
end

class ODTDocument < Document
  def extension
    :odt
  end
end

Moduulit

Moduulit Ruby on monia käyttötarkoituksia. Yksi niistä on yhdistelmät (lue lisää yhdistelmistä luvussa Lopullinen hajoaminen: Ruby vs. Python). Rubyn sekoituksia voidaan käyttää samalla tavalla kuin rajapintoja muissa ohjelmointikielissä (esim. Java), voit esimerkiksi määritellä niissä metodeja, jotka ovat yhteisiä objekteille, jotka sisältävät tietyn yhdistelmän. On hyvä käytäntö sisällyttää moduuleihin vain lukemiseen tarkoitettuja metodeja, eli metodeja, jotka eivät muuta tämän objektin tilaa.

Esimerkki:

moduuli Verotettava
  def tax

     hinta * 0.23
  end
end

luokka Auto
  include Taxable
 attr_reader :price
end

luokka Book
  include Taxable

 attr_reader :price
end

Ankka kirjoittaminen

Tämä on yksi dynaamisesti tyypiteltyjen kielten tärkeimmistä ominaisuuksista. Nimi tulee kuuluisasta testistä: jos se näyttää ankalta, ui kuin ankka ja kukkoilee kuin ankka, se on todennäköisesti ankka. The ohjelmoija ei tarvitse olla kiinnostunut siitä, mihin luokkaan kyseinen objekti kuuluu. Tärkeää ovat metodit, joita voidaan kutsua tällä objektilla.

Käyttämällä yllä olevassa esimerkissä määriteltyjä luokkia:

luokka Auto
  attr_reader :price

 def initialize(hinta)
    @price = hinta
   end
end

luokka Book
  attr_reader :price

 def initialize(hinta)
    @price = hinta
  end
end

car = Car.new(20.0)
book = Book.new(10.0)

[auto, kirja].map(&:hinta)

GrapQL

GraphQL on suhteellisen uusi kyselykieli sovellusrajapintoja varten. Sen etuihin kuuluu se, että sen syntaksi on hyvin yksinkertainen, ja lisäksi asiakas päättää itse, mitä hän haluaa saada, sillä jokainen asiakas saa juuri sen, mitä hän haluaa, eikä mitään muuta.

Esimerkkikysely osoitteessa GraphQL:

{
  allUsers {
     users {
        id
        login
        email

       }
     }
   }

Esimerkkivastaus:

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

Tämä on luultavasti kaikki, mitä meidän tarvitsee tällä hetkellä tietää. Mennään siis asiaan.

Ongelman esittely

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!

ruby ja grapql polymorfismi - eläimet

Oletetaan, että meillä on backend-sovellus, joka on kirjoitettu kielellä Ruby on Rails. Se on jo mukautettu edellä mainittua järjestelmää varten. Oletetaan myös, että meillä on jo GraphQL konfiguroitu. Haluamme antaa asiakkaalle mahdollisuuden tehdä kysely seuraavan rakenteen mukaisesti:

{
 allZoos : {
    zoo: {
      name
      kaupunki
      eläimet: {
        ...
      }
    }
  }
}

Mitä pitäisi laittaa kolmen pisteen tilalle, jotta saataisiin puuttuvat tiedot - se selviää myöhemmin.

Täytäntöönpano

Seuraavassa esittelen tavoitteen saavuttamiseen tarvittavat vaiheet.

Kyselyn lisääminen QueryType-luokkaan

Ensin on määriteltävä, mitä kysely allZoos tarkalleen ottaen tarkoittaa. Tätä varten meidän on käytävä tiedostossa nimeltäapp/graphql/types/query_type.rb ja määrittele kysely:

   moduuli Tyypit
      class QueryType < Types::BaseObject
       kenttä :all_zoos, [Types::ZooType], null: false

       def all_zoos
          Zoo.all
       end
    end
 end

Kysely on jo määritelty. Nyt on aika määritellä paluutyypit.

Tyyppien määritelmä

Ensimmäinen tarvittava tyyppi on ZooType. Määritellään se tiedostossa app/graphql/types/ zoo_type.rb:

moduuli Tyypit
  class ZooType < Types::BaseObject
    field :name, String, null: false
    field :city, String, null: false
    field :animals, [Types::AnimalType], null: false
  end
end

Nyt on aika määritellä tyyppi AnimalType:

moduuli Tyypit
  class AnimalType < Types::BaseUnion
   possible_types ElephantType, CatType, DogType

     def self.resolve_type(obj, ctx)
       if obj.is_a?(Elefantti)
          ElephantType
       elsif obj.is_a?(Cat)
         CatType
       elsif obj.is_a?(Dog)
        DogType
      end
    end
  end
end

Mitä näemme koodi edellä?

  1. AnimalType periytyy nimestä Types::BaseUnion.
  2. Meidän on lueteltava kaikki tyypit, jotka voivat muodostaa tietyn liiton.
    3.Me ohitamme funktion self.resolve_object(obj, ctx),jonka on palautettava tietyn objektin tyyppi.

Seuraavaksi määritellään eläintyypit. Tiedämme kuitenkin, että jotkin kentät ovat kaikille eläimille yhteisiä. Sisällytetään ne tyyppiin AnimalInterface:

moduuli Tyypit
  moduuli AnimalInterface
    include Types::BaseInterface

    field :name, String, null: false
    field :age, Kokonaisluku, null: false
  end
end

Kun tämä rajapinta on valmis, voimme määritellä tiettyjen eläinten tyypit:

moduuli Tyypit
  class ElephantType < Types::BaseObject
    implements Types::AnimalInterface

    kenttä :trunk_length, Float, null: false
  end
end

moduuli Types
  class CatType < Types::BaseObject
   implements Types::AnimalInterface

   field :hair_type, String, null: false
  end
end

moduuli Tyypit
  class DogType < Types::BaseObject
    implements Types::AnimalInterface

     field :breed, String, null: false
  end
end

Juuri noin! Valmiina! Vielä yksi kysymys: miten voimme käyttää sitä, mitä olemme tehneet asiakkaan puolella?

Kyselyn rakentaminen

{
 allZoos : {
   zoo: {
      name
      kaupunki
      eläimet: {
        __typename

        ... on ElephantType {
          name
          age
          trunkLength
        }

         ... on CatType {
          name
          age
          hairType
         }
         ... on DogType {
          name
          age
          rotu
         }
       }
     }
   }
 }

Voimme käyttää tässä ylimääräistä __typename-kenttää, joka palauttaa tietyn elementin tarkan tyypin (esim. CatType). Miltä esimerkkivastaus näyttää?

{
  "allZoos": [

   {
      "name": "Natura Artis Magistra",
      "city": "Amsterdam",
      "animals": [
        {
          "__tyyppinimi": "ElephantType"
          "name": "Franco",
          "age": 28,
          "trunkLength": 9.27
         },
         {
         "__typename": "DogType"
         "name": "Jack",
         "age": 9,
         "rotu": "Jack Russell Terrier"
        },
      ]
    }
  ]
} 

Analyysi

Yksi tämän lähestymistavan haittapuoli on ilmeinen. Kyselyssä meidän on syötettävä nimi ja ikä jokaiseen tyyppiin, vaikka tiedämme, että kaikilla eläimillä on nämä kentät. Tämä ei ole häiritsevää, kun kokoelma sisältää täysin erilaisia objekteja. Tässä tapauksessa eläimillä on kuitenkin lähes kaikki samat kentät. Voidaanko tätä jotenkin parantaa?

Totta kai! Teemme ensimmäisen muutoksen tiedostoon app/graphql/types/zoo_type.rb:

moduuli Tyypit
  class ZooType < Types::BaseObject
    field :name, String, null: false
    field :city, String, null: false
    field :animals, [Types::AnimalInterface], null: false
  end
end

Emme enää tarvitse aiemmin määrittelemäämme liittoa. Muutamme Types::AnimalType osoitteeseen Types::AnimalInterface.

Seuraava askel on lisätä funktio, joka palauttaa tyypin, joka on peräisin Tyypit :: AnimalInterface ja lisää myös luettelo orphan_types-tyypeistä, eli tyypeistä, joita ei koskaan käytetä suoraan:

moduuli Tyypit
  moduuli AnimalInterface
    include Types::BaseInterface

   field :name, String, null: false
   field :age, Kokonaisluku, null: false

   definition_methods do
      def resolve_type(obj, ctx)
        if obj.is_a?(Elephant)
          ElephantType
        elsif obj.is_a?(Cat)
          CatType
        elsif obj.is_a?(Dog)
          DogType
        end
      end
    end
    orphan_types Types::ElephantType, Types::CatType, Types::DogType
  end
end

Tämän yksinkertaisen menettelyn ansiosta kysely on vähemmän monimutkainen:

{
  allZoos : {
   zoo: {
      name
      kaupunki
      eläimet: {
        __typename
        name
        ikä

       ... on ElephantType {
          trunkLength

       }
       ... on CatType {
          hairType

       }
       ... on DogType {
          breed

        }
      }
    }
  }
}

Yhteenveto

GraphQL on todella hyvä ratkaisu. Jos et vielä tiedä sitä, kokeile sitä. Luota minuun, se on sen arvoista. Se tekee loistavaa työtä ratkaistessaan ongelmia, joita esiintyy esimerkiksi REST API:issa. Kuten edellä osoitin, polymorfismi ei ole todellinen este sille. Esittelin kaksi menetelmää sen ratkaisemiseksi.
Muistutus:

Lue lisää

GraphQL Ruby. Entä suorituskyky?

Kiskot ja muut liikennevälineet

Rails-kehitys TMUX, Vim, Fzf + Ripgrep -ohjelmilla

fiFinnish