Polimorfizm

Polimorfizm jest kluczowym składnikiem programowanie obiektowe. Upraszczając, opiera się ona na fakcie, że obiekty różnych klas mają dostęp do tego samego interfejsu, co oznacza, że od każdego z nich możemy oczekiwać tej samej funkcjonalności, ale niekoniecznie zaimplementowanej w ten sam sposób. W Ruby deweloperzy może uzyskać polimorfizm na trzy sposoby:

Dziedziczenie

Dziedziczenie polega na utworzeniu klasy nadrzędnej i klas podrzędnych (tj. dziedziczących z klasy nadrzędnej). Podklasy otrzymują funkcjonalność klasy nadrzędnej, a także umożliwiają zmianę i dodanie funkcjonalności.

Przykład:

class Document
  attr_reader :name
koniec

class PDFDocument < Dokument
  def extension
    pdf
  end
end

class ODTDocument < Document
  def extension
    odt
  end
end

Moduły

Moduły w Ruby mają wiele zastosowań. Jednym z nich są mixiny (przeczytaj więcej o mixinach w Ostateczny podział: Ruby vs. Python). Mixiny w Rubim mogą być używane podobnie do interfejsów w innych językach programowania (np. w Java) można na przykład zdefiniować w nich metody wspólne dla obiektów, które będą zawierały dany mixin. Dobrą praktyką jest umieszczanie w modułach metod tylko do odczytu, czyli takich, które nie będą modyfikować stanu tego obiektu.

Przykład:

moduł Taxable
  def tax

     cena * 0.23
  koniec
koniec

class Samochód
  include Taxable
 attr_reader :price
end

class Book
  include Taxable

 attr_reader :price
koniec

Pisanie na klawiaturze

Jest to jedna z kluczowych cech języków dynamicznie typowanych. Nazwa pochodzi od słynnego testu: jeśli wygląda jak kaczka, pływa jak kaczka i kwacze jak kaczka, to prawdopodobnie jest kaczką. Test programista nie musi interesować się, do jakiej klasy należy dany obiekt. Ważne są metody, które można wywołać na tym obiekcie.

Korzystając z klas zdefiniowanych w powyższym przykładzie:

class Samochód
  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 to stosunkowo nowy język zapytań dla interfejsów API. Jego zalety obejmują fakt, że ma bardzo prostą składnię, a ponadto klient decyduje, co dokładnie chce uzyskać, ponieważ każdy klient otrzymuje dokładnie to, czego chce i nic więcej.

Przykładowe zapytanie w GraphQL:

{
  allUsers {
     users {
        id
        login
        email

       }
     }
   }

Przykładowa odpowiedź:

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

To chyba wszystko, co powinniśmy wiedzieć w tym momencie. Przejdźmy więc do rzeczy.

Przedstawienie problemu

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!

polimorfizm ruby i grapql - zwierzęta

Załóżmy, że mamy aplikację backendową napisaną w języku Ruby on Rails. Jest on już przystosowany do obsługi powyższego schematu. Załóżmy również, że mamy już GraphQL skonfigurowany. Chcemy umożliwić klientowi złożenie zapytania w ramach następującej struktury:

{
 allZoos : {
    zoo: {
      nazwa
      miasto
      zwierzęta: {
        ...
      }
    }
  }
}

Co należy wstawić zamiast trzech kropek, aby uzyskać brakujące informacje - dowiemy się później.

Wdrożenie

Poniżej przedstawię kroki potrzebne do osiągnięcia tego celu.

Dodawanie zapytania do QueryType

Najpierw należy zdefiniować, co dokładnie oznacza zapytanie allZoos. Aby to zrobić, musimy odwiedzić plikapp/graphql/types/query_type.rb i zdefiniuj zapytanie:

   moduł Typy
      class QueryType < Types::BaseObject
       field :all_zoos, [Types::ZooType], null: false

       def all_zoos
          Zoo.all
       end
    end
 end

Zapytanie jest już zdefiniowane. Teraz nadszedł czas, aby zdefiniować typy zwracane.

Definicja typów

Pierwszym wymaganym typem będzie ZooType. Zdefiniujmy go w pliku app/graphql/types/ zoo_type.rb:

moduł Typy
  class ZooType < Types::BaseObject
    pole :name, String, null: false
    field :city, String, null: false
    field :animals, [Types::AnimalType], null: false
  end
end

Teraz nadszedł czas na zdefiniowanie typu AnimalType:

moduł Typy
  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

Co widzimy w kod powyżej?

  1. AnimalType dziedziczy po Types::BaseUnion.
  2. Musimy wymienić wszystkie typy, które mogą tworzyć dany związek.
    3.zastępujemy funkcję self.resolve_object(obj, ctx),która musi zwracać typ danego obiektu.

Następnym krokiem jest zdefiniowanie typów zwierząt. Wiemy jednak, że niektóre pola są wspólne dla wszystkich zwierząt. Uwzględnijmy je w typie AnimalInterface:

moduł Typy
  moduł AnimalInterface
    include Types::BaseInterface

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

Mając ten interfejs, możemy przystąpić do definiowania typów konkretnych zwierząt:

moduł Typy
  class ElephantType < Types::BaseObject
    implementuje Types::AnimalInterface

    field :trunk_length, Float, null: false
  end
end

moduł Typy
  class CatType < Types::BaseObject
   implementuje Types::AnimalInterface

   pole :hair_type, String, null: false
  end
end

moduł Typy
  class DogType < Types::BaseObject
    implementuje Types::AnimalInterface

     field :breed, String, null: false
  end
end

To jest to! Gotowe! Ostatnie pytanie: jak możemy wykorzystać to, co zrobiliśmy od strony klienta?

Tworzenie zapytania

{
 allZoos : {
   zoo: {
      nazwa
      miasto
      zwierzęta: {
        __typename

        ... on ElephantType {
          nazwa
          wiek
          trunkLength
        }

         ... on CatType {
          nazwa
          wiek
          hairType
         }
         ... on DogType {
          nazwa
          wiek
          rasa
         }
       }
     }
   }
 }

Możemy tutaj użyć dodatkowego pola __typename, które zwróci dokładny typ danego elementu (np. CatType). Jak będzie wyglądać przykładowa odpowiedź?

{
  "allZoos": [

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

Analiza

Widoczna jest jedna wada tego podejścia. W zapytaniu musimy wpisać nazwę i wiek w każdym typie, mimo że wiemy, że wszystkie zwierzęta mają te pola. Nie jest to uciążliwe, gdy kolekcja zawiera zupełnie różne obiekty. W tym przypadku jednak zwierzęta mają prawie wszystkie pola wspólne. Czy można to jakoś poprawić?

Oczywiście! Dokonujemy pierwszej zmiany w pliku app/graphql/types/zoo_type.rb:

moduł Typy
  class ZooType < Types::BaseObject
    pole :name, String, null: false
    field :city, String, null: false
    field :animals, [Types::AnimalInterface], null: false
  end
end

Nie potrzebujemy już związku, który zdefiniowaliśmy wcześniej. Zmieniamy się Types::AnimalType do Types::AnimalInterface.

Następnym krokiem jest dodanie funkcji zwracającej typ z pliku Typy :: AnimalInterface a także dodać listę orphan_types, czyli typów, które nigdy nie są bezpośrednio używane:

moduł Typy
  moduł AnimalInterface
    include Types::BaseInterface

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

Dzięki temu prostemu zabiegowi zapytanie ma mniej skomplikowaną formę:

{
  allZoos : {
   zoo: {
      nazwa
      miasto
      zwierzęta: {
        __typename
        nazwa
        wiek

       ... on ElephantType {
          trunkLength

       }
       ... on CatType {
          hairType

       }
       ... on DogType {
          rasa

        }
      }
    }
  }
}

Podsumowanie

GraphQL to naprawdę świetne rozwiązanie. Jeśli jeszcze go nie znasz, spróbuj. Zaufaj mi, warto. Świetnie radzi sobie z rozwiązywaniem problemów pojawiających się np. w REST API. Jak pokazałem powyżej, polimorfizm nie jest dla niego prawdziwą przeszkodą. Przedstawiłem dwie metody radzenia sobie z tym problemem.
Przypomnienie:

Czytaj więcej

GraphQL Ruby. Co z wydajnością?

Szyny i inne środki transportu

Rails Development z TMUX, Vim, Fzf + Ripgrep

pl_PLPolish