Πολυμορφισμός

Πολυμορφισμός αποτελεί βασικό συστατικό της αντικειμενοστραφής προγραμματισμός. Για να απλοποιήσουμε τα πράγματα, βασίζεται στο γεγονός ότι τα αντικείμενα διαφορετικών κλάσεων έχουν πρόσβαση στην ίδια διεπαφή, πράγμα που σημαίνει ότι μπορούμε να περιμένουμε από καθένα από αυτά την ίδια λειτουργικότητα, αλλά όχι απαραίτητα υλοποιημένη με τον ίδιο τρόπο. Στο Ruby προγραμματιστές μπορεί να λάβει πολυμορφισμός με τρεις τρόπους:

Κληρονομικότητα

Κληρονομικότητα συνίσταται στη δημιουργία μιας γονικής κλάσης και κλάσεων-παιδιών (δηλαδή, που κληρονομούν από τη γονική κλάση). Οι υποκλάσεις λαμβάνουν τη λειτουργικότητα της γονικής κλάσης και σας επιτρέπουν επίσης να αλλάξετε και να προσθέσετε μια λειτουργικότητα.

Παράδειγμα:

κλάση Document
  attr_reader :name
end

class PDFDocument < Document
  def extension
    :pdf
  end
end

class ODTDocument < Document
  def extension
    :odt
  end
end

Ενότητες

Ενότητες σε Ruby έχουν πολλές χρήσεις. Μία από αυτές είναι τα mixins (διαβάστε περισσότερα για τα mixins στο Η απόλυτη ανάλυση: Python). Τα mixins στη Ruby μπορούν να χρησιμοποιηθούν παρόμοια με τις διασυνδέσεις σε άλλες γλώσσες προγραμματισμού (π.χ. σε Java), για παράδειγμα, μπορείτε να ορίσετε σε αυτές μεθόδους κοινές για τα αντικείμενα που θα περιέχουν ένα συγκεκριμένο mixin. Αποτελεί καλή πρακτική να συμπεριλαμβάνετε μεθόδους μόνο για ανάγνωση στις μονάδες, δηλαδή μεθόδους που δεν θα τροποποιούν την κατάσταση αυτού του αντικειμένου.

Παράδειγμα:

ενότητα Φορολογητέα
  def tax

     τιμή * 0.23
  end
end

κλάση Αυτοκίνητο
  include Taxable
 attr_reader :price
end

class Book
  include Taxable

 attr_reader :price
end

Δακτυλογράφηση πάπιας

Αυτό είναι ένα από τα βασικά χαρακτηριστικά των δυναμικά τυποποιημένων γλωσσών. Το όνομα προέρχεται από το διάσημο τεστ αν μοιάζει με πάπια, κολυμπάει σαν πάπια και λαλεί σαν πάπια, τότε μάλλον είναι πάπια. Το προγραμματιστής δεν χρειάζεται να ενδιαφέρεται για το σε ποια κλάση ανήκει το συγκεκριμένο αντικείμενο. Αυτό που έχει σημασία είναι οι μέθοδοι που μπορούν να κληθούν σε αυτό το αντικείμενο.

Χρησιμοποιώντας τις κλάσεις που ορίζονται στο παραπάνω παράδειγμα:

κλάση Αυτοκίνητο
  attr_reader :price

 def initialize(price)
    @price = price
   end
end

class Book
  attr_reader :price

 def initialize(price)
    @price = price
  end
end

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

[car, book].map(&:price

GrapQL

GraphQL είναι μια σχετικά νέα γλώσσα ερωτημάτων για APIs. Στα πλεονεκτήματά της συγκαταλέγεται το γεγονός ότι έχει πολύ απλή σύνταξη και, επιπλέον, ο πελάτης αποφασίζει τι ακριβώς θέλει να πάρει, καθώς κάθε πελάτης παίρνει ακριβώς αυτό που θέλει και τίποτε άλλο.

Δείγμα ερωτήματος στο GraphQL:

{
  allUsers {
     users {
        id
        login
        email

       }
     }
   }

Δείγμα απάντησης:

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

Αυτό είναι μάλλον το μόνο που χρειάζεται να γνωρίζουμε προς το παρόν. Ας μπούμε λοιπόν στο θέμα.

Παρουσίαση του προβλήματος

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 και grapql - ζώα

Ας υποθέσουμε ότι έχουμε μια backend εφαρμογή γραμμένη σε Ruby on Rails. Έχει ήδη προσαρμοστεί για να χειριστεί το παραπάνω σχήμα. Ας υποθέσουμε επίσης ότι έχουμε ήδη GraphQL διαμορφωμένο. Θέλουμε να δώσουμε τη δυνατότητα στον πελάτη να κάνει μια έρευνα με την ακόλουθη δομή:

{
 allZoos : {
    zoo: {
      name
      city
      ζώα: {
        ...
      }
    }
  }
}

Τι θα πρέπει να βάλετε αντί για τις τρεις τελείες για να λάβετε τις πληροφορίες που λείπουν - θα το μάθουμε αργότερα.

Εφαρμογή

Παρακάτω θα παρουσιάσω τα βήματα που απαιτούνται για την επίτευξη του στόχου.

Προσθήκη ερώτησης στο QueryType

Πρώτον, πρέπει να ορίσετε τι ακριβώς σημαίνει το ερώτημα allZoos. Για να το κάνουμε αυτό, πρέπει να επισκεφτούμε το αρχείοapp/graphql/types/query_type.rb και ορίστε το ερώτημα:

   ενότητα Τύποι
      class QueryType < Types::BaseObject
       field :all_zoos, [Types::ZooType], null: false

       def all_zoos
          Zoo.all
       end
    end
 end

Το ερώτημα έχει ήδη οριστεί. Τώρα ήρθε η ώρα να ορίσουμε τους τύπους επιστροφής.

Ορισμός των τύπων

Ο πρώτος απαιτούμενος τύπος θα είναι ο ZooType. Ας τον ορίσουμε στο αρχείο app/graphql/types/ zoo_type.rb:

ενότητα Τύποι
  class ZooType < Types::BaseObject
    field :name, String, null: false
    field :city, String, null: false
    field :animals, [Types::AnimalType], null: false
  end
end

Τώρα ήρθε η ώρα να ορίσουμε τον τύπο AnimalType:

ενότητα Τύποι
  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

Τι βλέπουμε στο κωδικός παραπάνω;

  1. Το AnimalType κληρονομεί από το Τύποι::BaseUnion.
  2. Πρέπει να απαριθμήσουμε όλους τους τύπους που μπορούν να αποτελέσουν μια δεδομένη ένωση.
    3.Παρακάμπτουμε τη συνάρτηση self.resolve_object(obj, ctx),η οποία πρέπει να επιστρέφει τον τύπο ενός συγκεκριμένου αντικειμένου.

Το επόμενο βήμα είναι ο καθορισμός των τύπων των ζώων. Ωστόσο, γνωρίζουμε ότι ορισμένα πεδία είναι κοινά για όλα τα ζώα. Ας τα συμπεριλάβουμε στον τύπο AnimalInterface:

ενότητα Τύποι
  module AnimalInterface
    include Types::BaseInterface

    πεδίο :name, String, null: false
    πεδίο :age, Ακέραιος αριθμός, null: false
  end
end

Έχοντας αυτή τη διεπαφή, μπορούμε να προχωρήσουμε στον καθορισμό των τύπων των συγκεκριμένων ζώων:

ενότητα Τύποι
  class ElephantType < Types::BaseObject
    υλοποιεί Types::AnimalInterface

    πεδίο :trunk_length, Float, null: false
  end
end

module Τύποι
  class CatType < Types::BaseObject
   υλοποιεί Types::AnimalInterface

   πεδίο :hair_type, String, null: false
  end
end

module Τύποι
  class DogType < Types::BaseObject
    υλοποιεί Types::AnimalInterface

     πεδίο :breed, String, null: false
  end
end

Αυτό είναι! Έτοιμοι! Μια τελευταία ερώτηση: πώς μπορούμε να χρησιμοποιήσουμε αυτό που κάναμε από την πλευρά του πελάτη;

Δημιουργία του ερωτήματος

{
 allZoos : {
   zoo: {
      name
      city
      ζώα: {
        __typename

        ... on ElephantType {
          name
          age
          trunkLength
        }

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

Μπορούμε να χρησιμοποιήσουμε ένα πρόσθετο πεδίο __typename εδώ, το οποίο θα επιστρέψει τον ακριβή τύπο ενός συγκεκριμένου στοιχείου (π.χ. CatType). Πώς θα μοιάζει ένα δείγμα απάντησης;

{
  "allZoos": [

   {
      "name": "Natura Artis Magistra",
      "city": "Άμστερνταμ",
      "animals": [
        {
          "__typename": "ElephantType"
          "name": "Franco",
          "age": 28,
          "trunkLength": 9.27
         },
         {
         "__typename": "DogType"
         "name": "Jack",
         "age": 9,
         "breed": "Jack Russell Terrier"
        },
      ]
    }
  ]
} 

Ανάλυση

Ένα μειονέκτημα αυτής της προσέγγισης είναι προφανές. Στο ερώτημα, πρέπει να εισάγουμε το όνομα και την ηλικία σε κάθε τύπο, παρόλο που γνωρίζουμε ότι όλα τα ζώα έχουν αυτά τα πεδία. Αυτό δεν είναι ενοχλητικό όταν η συλλογή περιέχει εντελώς διαφορετικά αντικείμενα. Σε αυτή την περίπτωση, όμως, τα ζώα μοιράζονται σχεδόν όλα τα πεδία. Μπορεί να βελτιωθεί με κάποιο τρόπο;

Φυσικά! Κάνουμε την πρώτη αλλαγή στο αρχείο app/graphql/types/zoo_type.rb:

ενότητα Τύποι
  class ZooType < Types::BaseObject
    field :name, String, null: false
    field :city, String, null: false
    field :animals, [Types::AnimalInterface], null: false
  end
end

Δεν χρειαζόμαστε πλέον την ένωση που έχουμε ορίσει πριν. Αλλάζουμε Τύποι::AnimalType στο Τύποι::AnimalInterface.

Το επόμενο βήμα είναι να προσθέσουμε μια συνάρτηση που επιστρέφει έναν τύπο από το Τύποι :: AnimalInterface και προσθέστε επίσης μια λίστα με τύπους orphan_types, δηλαδή τύπους που δεν χρησιμοποιούνται ποτέ άμεσα:

ενότητα Τύποι
  module AnimalInterface
    include Types::BaseInterface

   πεδίο :name, String, null: false
   πεδίο :age, Ακέραιος αριθμός, 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

Χάρη σε αυτή την απλή διαδικασία, το ερώτημα έχει μια λιγότερο περίπλοκη μορφή:

{
  allZoos : {
   zoo: {
      name
      city
      ζώα: {
        __typename
        name
        age

       ... on ElephantType {
          trunkLength

       }
       ... on CatType {
          hairType

       }
       ... on DogType {
          breed

        }
      }
    }
  }
}

Περίληψη

GraphQL είναι μια πραγματικά εξαιρετική λύση. Αν δεν το ξέρετε ακόμα, δοκιμάστε το. Πιστέψτε με, αξίζει τον κόπο. Κάνει εξαιρετική δουλειά στην επίλυση προβλημάτων που εμφανίζονται, για παράδειγμα, στα REST APIs. Όπως έδειξα παραπάνω, πολυμορφισμός δεν αποτελεί πραγματικό εμπόδιο για αυτό. Παρουσίασα δύο μεθόδους για την αντιμετώπισή του.
Υπενθύμιση:

Διαβάστε περισσότερα

GraphQL Ruby. Τι γίνεται με τις επιδόσεις;

Σιδηρόδρομοι και άλλα μέσα μεταφοράς

Ανάπτυξη Rails με TMUX, Vim, Fzf + Ripgrep

elGreek