Polymorfisme

Polymorfisme er en nøkkelkomponent i objektorientert programmering. For å forenkle det hele er det basert på det faktum at objekter i ulike klasser har tilgang til det samme grensesnittet, noe som betyr at vi kan forvente samme funksjonalitet fra hver av dem, men ikke nødvendigvis implementert på samme måte. I Ruby utviklere kan oppnå polymorfisme på tre måter:

Arv

Arv består i å opprette en overordnet klasse og underordnede klasser (dvs. som arver fra den overordnede klassen). Underklasser får funksjonaliteten til den overordnede klassen, og gjør det også mulig å endre og legge til funksjonalitet.

Eksempel:

klasse Dokument
  attr_reader :navn
end

class PDFDocument < Dokument
  def extension
    :pdf
  end
end

class ODTDocument < Dokument
  def extension
    :odt
  end
end

Moduler

Moduler i Ruby har mange bruksområder. En av dem er mixins (les mer om mixins i Den ultimate sammenbruddet: Ruby vs. Python). Mixins i Ruby kan brukes på samme måte som grensesnitt i andre programmeringsspråk (f.eks. i Java), kan du for eksempel definere metoder som er felles for objekter som skal inneholde et gitt mixin. Det er lurt å inkludere skrivebeskyttede metoder i modulene, det vil si metoder som ikke endrer objektets tilstand.

Eksempel:

modul Taxable
  def skatt

     pris * 0,23
  end
end

klasse Bil
  include Taxable
 attr_reader :price
end

class Book
  include Taxable

 attr_reader :pris
end

Skriving på and

Dette er et av de viktigste kjennetegnene ved dynamisk typede språk. Navnet kommer fra den berømte testen "hvis det ser ut som en and, svømmer som en and og kvakker som en and, så er det sannsynligvis en and". Den programmerer trenger ikke å være interessert i hvilken klasse det gitte objektet tilhører. Det som er viktig, er hvilke metoder som kan kalles på dette objektet.

Bruk klassene som er definert i eksempelet ovenfor:

klasse Bil
  attr_reader :pris

 def initialize(pris)
    @price = pris
   end
end

klasse Bok
  attr_reader :pris

 def initialize(pris)
    @pris = pris
  end
end

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

[bil, bok].map(&:pris

GrapQL

GraphQL er et relativt nytt spørrespråk for API-er. Fordelene er blant annet at det har en svært enkel syntaks, og i tillegg bestemmer kunden selv hva han eller hun vil ha, ettersom hver kunde får akkurat det han eller hun vil ha og ikke noe annet.

Eksempel på spørring i GraphQL:

{
  allUsers {
     users {
        id
        innlogging
        e-post

       }
     }
   }

Eksempel på svar:

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

Dette er sannsynligvis alt vi trenger å vite for øyeblikket. Så, la oss komme til poenget.

Presentasjon av problemet

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 og grapql polymorfisme - dyr

Anta at vi har en backend-applikasjon skrevet i Ruby on Rails. Den er allerede tilpasset for å håndtere ordningen ovenfor. La oss også anta at vi allerede har GraphQL konfigurert. Vi ønsker å gjøre det mulig for klienten å gjøre en forespørsel innenfor følgende struktur:

{
 allZoos : {
    zoo: {
      navn
      by
      animals: {
        ...
      }
    }
  }
}

Hva som skal settes i stedet for de tre prikkene for å få den manglende informasjonen - det finner vi ut senere.

Implementering

Nedenfor vil jeg presentere trinnene som trengs for å nå målet.

Legge til en spørring i QueryType

Først må du definere hva spørringen allZoos egentlig betyr. For å gjøre dette må vi gå til filenapp/graphql/types/query_type.rb og definere spørringen:

   modul Types
      class QueryType < Types::BaseObject
       field :all_zoos, [Types::ZooType], null: false

       def all_zoos
          Zoo.all
       end
    end
 end

Spørringen er allerede definert. Nå er det på tide å definere returtypene.

Definisjon av typer

Den første typen som kreves, er ZooType. La oss definere den i filen app/graphql/types/ zoo_type.rb:

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

Nå er det på tide å definere typen AnimalType:

modul Typer
  class AnimalType < Types::BaseUnion
   possible_types ElephantType, CatType, DogType

     def self.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
end

Hva ser vi i kode over?

  1. AnimalType arver fra Types::BaseUnion.
  2. Vi må liste opp alle typer som kan utgjøre en gitt union.
    3. Vi overstyrer funksjonen self.resolve_object(obj, ctx),som må returnere typen til et gitt objekt.

Neste trinn er å definere dyretypene. Vi vet imidlertid at noen felt er felles for alle dyrene. La oss inkludere dem i typen AnimalInterface:

modul Typer
  modul AnimalInterface
    include Types::BaseInterface

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

Med dette grensesnittet kan vi gå videre til å definere typer av spesifikke dyr:

modul Typer
  class ElephantType < Types::BaseObject
    implementerer Types::AnimalInterface

    field :snabel_lengde, Float, null: false
  end
end

modul Typer
  class CatType < Types::BaseObject
   implementerer Types::AnimalInterface

   field :hair_type, String, null: false
  end
end

modul Typer
  class DogType < Types::BaseObject
    implementerer Types::AnimalInterface

     field :rase, String, null: false
  end
end

Sånn, ja! Nå er vi klare! Et siste spørsmål: Hvordan kan vi bruke det vi har gjort fra kundens side?

Bygge opp spørringen

{
 allZoos : {
   zoo: {
      navn
      by
      animals: {
        __typename

        ... on ElephantType {
          navn
          alder
          trunkLength
        }

         ... on CatType {
          navn
          alder
          hairType
         }
         ... on DogType {
          navn
          alder
          rase
         }
       }
     }
   }
 }

Vi kan bruke et ekstra __typename-felt her, som returnerer den nøyaktige typen av et gitt element (f.eks. CatType). Hvordan vil et eksempel på et svar se ut?

{
  "allZoos": [

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

Analyse

En ulempe med denne tilnærmingen er åpenbar. I spørringen må vi skrive inn navn og alder i hver type, selv om vi vet at alle dyr har disse feltene. Dette er ikke så plagsomt når samlingen inneholder helt forskjellige objekter. I dette tilfellet deler imidlertid dyrene nesten alle feltene. Kan det forbedres på en eller annen måte?

Selvsagt! Vi gjør den første endringen i filen app/graphql/types/zoo_type.rb:

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

Vi trenger ikke lenger den unionen vi har definert tidligere. Vi endrer Types::AnimalType til Types::AnimalInterface.

Neste steg er å legge til en funksjon som returnerer en type fra Typer :: AnimalInterface og også legge til en liste over orphan_types, altså typer som aldri brukes direkte:

modul Typer
  modul AnimalInterface
    include Types::BaseInterface

   field :name, String, null: false
   field :age, Heltall, 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

Takket være denne enkle prosedyren har spørringen en mindre kompleks form:

{
  allZoos : {
   zoo: {
      navn
      by
      animals: {
        __typenavn
        navn
        alder

       ... on ElephantType {
          trunkLength

       }
       ... på CatType {
          hairType

       }
       ... on DogType {
          rase

        }
      }
    }
  }
}

Sammendrag

GraphQL er en veldig bra løsning. Hvis du ikke vet det ennå, prøv det. Tro meg, det er verdt det. Den gjør en god jobb med å løse problemer som oppstår i for eksempel REST API-er. Som jeg viste ovenfor, polymorfisme er ikke et reelt hinder for det. Jeg presenterte to metoder for å takle det.
Påminnelse:

Les mer

GraphQL Ruby. Hva med ytelse?

Skinner og andre transportmidler

Rails-utvikling med TMUX, Vim, Fzf + Ripgrep

nb_NONorwegian