Polymorfisme

Polymorfisme is een belangrijk onderdeel van objectgeoriënteerd programmeren. Om de zaken te vereenvoudigen, is het gebaseerd op het feit dat objecten van verschillende klassen toegang hebben tot dezelfde interface, wat betekent dat we van elk van hen dezelfde functionaliteit kunnen verwachten, maar niet noodzakelijk op dezelfde manier geïmplementeerd. In Ruby ontwikkelaars kunnen verkrijgen polymorfisme op drie manieren:

Erfenis

Erfenis bestaat uit het maken van een ouderklasse en kindklassen (d.w.z. die overerven van de ouderklasse). Subklassen krijgen de functionaliteit van de ouderklasse en stellen je ook in staat om functionaliteit te wijzigen of toe te voegen.

Voorbeeld:

klasse Document
  attr_lezer :naam
einde

klasse PDFDocument < Document
  def uitbreiding
    :pdf
  einde
einde

klasse ODTDocument < Document
  Uitbreiding
    :odt
  einde
einde

Modules

Modules in Ruby hebben veel toepassingen. Een daarvan is mixins (lees meer over mixins in De ultieme onderverdeling: Ruby vs. Python). Mixins in Ruby kunnen op dezelfde manier gebruikt worden als interfaces in andere programmeertalen (bijvoorbeeld in Java), bijvoorbeeld, kun je daarin methoden definiëren die gemeenschappelijk zijn voor objecten die een bepaalde mixin zullen bevatten. Het is een goede gewoonte om alleen-lezen methoden in modules op te nemen, dus methoden die de toestand van dit object niet wijzigen.

Voorbeeld:

module Belastbaar
  def. belasting

     prijs * 0,23
  einde
einde

Klasse Auto
  inclusief belastbaar
 attr_lezer :prijs
einde

Klasse Boek
  include belastingplichtige

 attr_lezer :prijs
einde

Typen met eend

Dit is een van de belangrijkste eigenschappen van dynamisch getypeerde talen. De naam komt van de beroemde test als het eruit ziet als een eend, zwemt als een eend en kwaakt als een eend, dan is het waarschijnlijk een eend. De programmeur hoeft niet geïnteresseerd te zijn tot welke klasse het gegeven object behoort. Wat belangrijk is, zijn de methoden die op dit object kunnen worden aangeroepen.

Gebruik de klassen die in het bovenstaande voorbeeld zijn gedefinieerd:

Klasse Auto
  attr_lezer :prijs

 def initialiseer(prijs)
    @prijs = prijs
   einde
einde

Klasse Boek
  attr_lezer :prijs

 def initialiseer(prijs)
    @prijs = prijs
  einde
einde

auto = auto.nieuw(20.0)
boek = boek.nieuw(10.0)

[auto, boek].map(&:prijs

GrapQL

GraphQL is een relatief nieuwe querytaal voor API's. De voordelen zijn onder andere dat het een zeer eenvoudige syntaxis heeft en bovendien bepaalt de klant wat hij precies wil krijgen, omdat elke klant precies krijgt wat hij wil en niets anders.

Voorbeeldvraag in GraphQL:

{
  allUsers {
     users {
        id
        login
        e-mail

       }
     }
   }

Voorbeeldantwoord:

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

Dit is waarschijnlijk alles wat we op dit moment moeten weten. Laten we ter zake komen.

Presentatie van het probleem

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 en grapql polymorfisme - dieren

Stel dat we een back-end applicatie hebben die geschreven is in Ruby on Rails. Het is al aangepast aan het bovenstaande schema. Laten we ook aannemen dat we al GraphQL geconfigureerd. We willen de klant in staat stellen een aanvraag te doen binnen de volgende structuur:

{
 allZoos : {
    zoo: {
      naam
      stad
      dieren: {
        ...
      }
    }
  }
}

Wat er in plaats van de drie puntjes moet worden gezet om de ontbrekende informatie te krijgen - daar komen we later wel achter.

Implementatie

Hieronder presenteer ik de stappen die nodig zijn om het doel te bereiken.

Een query toevoegen aan QueryType

Eerst moet je definiëren wat de query allZoos precies betekent. Hiervoor moeten we naar het bestandapp/graphql/types/query_type.rb en definieer de query:

   module Types
      klasse QueryType < Types::BaseObject
       veld :all_zoos, [Types::ZooType], null: false

       def alle_zoös
          Dierentuin.alle
       einde
    einde
 einde

De query is al gedefinieerd. Nu is het tijd om de retoursoorten te definiëren.

Definitie van soorten

Het eerste type dat nodig is, is ZooType. Laten we het definiëren in het bestand app/graphql/types/ zoo_type.rb:

module Types
  klasse Dierentype < Types::BaseObject
    veld :name, String, null: false
    veld :city, String, nul: vals
    veld :animals, [Types::AnimalType], nul: vals
  einde
einde

Nu is het tijd om het type AnimalType te definiëren:

module Typen
  Klasse DierType < Types::BaseUnion
   mogelijke_typen OlifantType, KatType, HondType

     def self.resolve_type(obj, ctx)
       als obj.is_een?(Olifant)
          OlifantType
       als obj.is_een?(Kat)
         CatType
       anders indien obj.is_een?(Hond)
        HondType
      eind
    einde
  einde
einde

Wat zien we in de code boven?

  1. Het AnimalType erft van Types::BaseUnion.
  2. We moeten een lijst maken van alle typen die een bepaalde unie kunnen vormen.
    3.We overschrijven de functie self.resolve_object(obj, ctx),die het type van een gegeven object moet teruggeven.

De volgende stap is het definiëren van de soorten dieren. We weten echter dat sommige velden gemeenschappelijk zijn voor alle dieren. Laten we ze opnemen in het type AnimalInterface:

module Typen
  module Dierlijke interface
    include Types::BaseInterface

    veld :name, String, null: false
    veld :leeftijd, geheel getal, nul: vals
  einde
einde

Met deze interface kunnen we de soorten specifieke dieren definiëren:

module Types
  Klasse OlifantType < Types::BaseObject
    implementeert Types::AnimalInterface

    veld :slurf_lengte, Vlotter, nul: vals
  einde
einde

module Types
  Klasse Kattype < Types::BaseObject
   implementeert Types::AnimalInterface

   veld :hair_type, String, null: false
  einde
einde

module Types
  Klasse HondType < Types::BaseObject
    implementeert Types::AnimalInterface

     veld :ras, String, nul: vals
  einde
einde

Dat is het! Klaar! Nog een laatste vraag: hoe kunnen we gebruiken wat we aan de kant van de klant hebben gedaan?

De query bouwen

{
 allZoos : {
   zoo: {
      naam
      stad
      dieren: {
        __typenaam

        ... op ElephantType {
          naam
          leeftijd
          trunkLength
        }

         ... op CatType {
          naam
          age
          hairType
         }
         ... on DogType {
          naam
          age
          ras
         }
       }
     }
   }
 }

We kunnen hier een extra __typename veld gebruiken, dat het exacte type van een gegeven element teruggeeft (bijvoorbeeld CatType). Hoe ziet een voorbeeldantwoord eruit?

{
  "allZoos": [

   {
      "naam": "Natura Artis Magistra",
      "stad": "Amsterdam",
      "dieren": [
        {
          "__typenaam": "ElephantType".
          "naam "Franco",
          "age": 28,
          "trunkLength": 9.27
         },
         {
         "__typename": "DogType
         "naam": "Jack",
         "age": 9,
         "ras": "Jack Russell Terrier"
        },
      ]
    }
  ]
} 

Analyse

Eén nadeel van deze aanpak is duidelijk. In de query moeten we bij elk type de naam en leeftijd invoeren, ook al weten we dat alle dieren deze velden hebben. Dit is niet hinderlijk als de verzameling compleet verschillende objecten bevat. In dit geval delen de dieren echter bijna alle velden. Kan dit op de een of andere manier worden verbeterd?

Natuurlijk! We maken de eerste wijziging in het bestand app/graphql/types/zoo_type.rb:

module Types
  klasse Dierentype < Types::BaseObject
    veld :name, String, null: false
    veld :city, String, null: false
    veld :animals, [Types::AnimalInterface], nul: vals
  einde
einde

We hebben de unie die we eerder hebben gedefinieerd niet langer nodig. We veranderen Types::AnimalType naar Types::AnimalInterface.

De volgende stap is het toevoegen van een functie die een type teruggeeft van Typen :: AnimalInterface en voeg ook een lijst met orphan_types toe, dus types die nooit direct gebruikt worden:

module Typen
  module Dierlijke interface
    include Types::BaseInterface

   veld :name, String, null: false
   veld :leeftijd, geheel getal, nul: vals

   definitie_methoden doen
      def resolve_type(obj, ctx)
        als obj.is_een?(Olifant)
          OlifantType
        als obj.is_een?(Kat)
          CatType
        als obj.is_een?(Hond)
          HondType
        eind
      einde
    einde
    orphan_types Types::ElephantType, Types::CatType, Types::DogType
  eind
einde

Dankzij deze eenvoudige procedure heeft de query een minder complexe vorm:

{
  allZoos : {
   zoo: {
      naam
      stad
      dieren: {
        __typenaam
        naam
        leeftijd

       ... op ElephantType {
          slurflengte

       }
       ... op CatType {
          hairType

       }
       ... op DogType {
          ras

        }
      }
    }
  }
}

Samenvatting

GraphQL is echt een geweldige oplossing. Als je het nog niet kent, probeer het dan. Geloof me, het is de moeite waard. Het werkt geweldig bij het oplossen van problemen die zich voordoen in bijvoorbeeld REST API's. Zoals ik hierboven heb laten zien, polymorfisme is niet echt een obstakel. Ik heb twee methoden gepresenteerd om het aan te pakken.
Herinnering:

Meer lezen

GraphQL Ruby. Hoe zit het met de prestaties?

Rails en andere transportmiddelen

Rails ontwikkelen met TMUX, Vim, Fzf + Ripgrep

nl_NLDutch