Software Development
Lukasz Brzeszcz
2022-01-13

Polymorphism in Ruby and GraphQL

In this article, I will present the use of polymorphism in GraphQL. Before I start, however, it is worth recalling what polymorphism and GraphQL are.

Polymorphism

Polymorphism is a key component of object-oriented programming. To simplify things, it is based on the fact that objects of different classes have access to the same interface, which means that we can expect from each of them the same functionality but not necessarily implemented in the same way. In Ruby developers can obtain polymorphism in three ways:

Inheritance

Inheritance consists in creating a parent class and child classes (i.e., inheriting from the parent class). Subclasses receive the functionality of the parent class and also allow you to change and add a functionality.

Example:

class Document
  attr_reader :name
end

class PDFDocument < Document
  def extension
    :pdf
  end
end

class ODTDocument < Document
  def extension
    :odt
  end
end

Modules

Modules in Ruby have many uses. One of them is mixins (read more about mixins in The Ultimate Breakdown: Ruby vs. Python). Mixins in Ruby can be used similarly to interfaces in other programming languages (e.g., in Java), for instance, you can define in them methods common to objects that will contain a given mixin. It is a good practice to include read-only methods in modules, so methods that will not modify the state of this object.

Example:

module Taxable
  def tax

     price * 0.23
  end
end

class Car
  include Taxable
 attr_reader :price
end

class Book
  include Taxable

 attr_reader :price
end

Duck typing

This is one of the key features of dynamically typed languages. The name comes from the famous test if it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck. The programmer does not have to be interested to which class the given object belongs. What is important are the methods that can be called on this object.

Using the classes defined in the example above:

class Car
  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 is a relatively new query language for APIs. Its advantages include the fact that it has a very simple syntax and, in addition, the client decides what exactly they want to get as each customer gets exactly what they want and nothing else.

Sample query in GraphQL:

{
  allUsers {
     users { 
        id
        login 
        email

       } 
     }
   }

Sample response:

{
  "allUsers": {
    "users": [ 
     {
        "id": 1,
        "login": "user1",
        "email": "user1@te.st"
      }, 
      {
        "id": 2,
        "login": "user2",
        "email": "user2@te.st"
      }, 
    ]
  }
}

This is probably all we need to know at the moment. So, let's get to the point.

Presentation of the problem

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 and grapql polymorphism - animals

Suppose we have a backend application written in Ruby on Rails. It is already adapted to handle the above scheme. Let's also assume that we already have GraphQL configured. We want to enable the client to make an inquiry within the following structure:

{
 allZoos : {
    zoo: {
      name
      city
      animals: {
        ... 
      }
    }
  }
}

What should be put instead of the three dots in order to obtain the missing information – we will find out later.

Implementation

Below I will present the steps needed to achieve the goal.

Adding a query to QueryType

First, you need to define what exactly the query allZoos means. To do this, we need to visit the fileapp/graphql/types/query_type.rb and define the query:

   module Types
      class QueryType < Types::BaseObject
       field :all_zoos, [Types::ZooType], null: false
   
       def all_zoos
          Zoo.all
       end
    end
 end

The query is already defined. Now it's time to define the return types.

Definition of types

The first type required will be ZooType. Let's define it in the file app/graphql/types/ zoo_type.rb:

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

Now it's time to define the type AnimalType:

module Types
  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

What do we see in the code above?

  1. The AnimalType inherits from Types::BaseUnion.
  2. We have to list all types that can make up a given union. 

3.We override the function self.resolve_object(obj, ctx),which must return the type of a given object.

The next step is to define the types of animals. However, we know that some fields are common to all animals. Let's include them in type AnimalInterface:

module Types
  module AnimalInterface
    include Types::BaseInterface

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

Having this interface, we can proceed to define the types of specific animals:

module Types
  class ElephantType < Types::BaseObject
    implements Types::AnimalInterface

    field :trunk_length, Float, null: false
  end
end

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

   field :hair_type, String, null: false
  end
end

module Types
  class DogType < Types::BaseObject 
    implements Types::AnimalInterface

     field :breed, String, null: false
  end
end

That's it! Ready! One last question: how can we use what we have done from the client’s side?

Building the query

{
 allZoos : {
   zoo: {
      name
      city
      animals: {
        __typename

        ... on ElephantType {
          name
          age
          trunkLength
        }

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

We can use an additional __typename field here, which will return the exact type of a given element (e.g., CatType). What will a sample answer look like?

{
  "allZoos": [

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

Analysis

One drawback of this approach is apparent. In the query, we must enter the name and age in each type, even though we know that all animals have these fields. This is not bothersome when the collection contains completely different objects. In this case, however, the animals share almost all fields. Can it be improved somehow?

Of course! We make the first change in the file app/graphql/types/zoo_type.rb:

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

We no longer need the union we have defined before. We change Types::AnimalType to Types::AnimalInterface.

The next step is to add a function that returns a type from Types :: AnimalInterface and also add a list of orphan_types, so types that are never directly used:

module Types
  module 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

Thanks to this simple procedure, the query has a less complex form:

{
  allZoos : {
   zoo: {
      name
      city
      animals: {
        __typename
        name
        age

       ... on ElephantType {
          trunkLength

       }
       ... on CatType {
          hairType

       }
       ... on DogType {
          breed

        } 
      }
    }
  }
}

Summary

GraphQL is a really great solution. If you don't know it yet, try it. Trust me, it's worth it. It does a great job in solving problems appearing in, for example, REST APIs. As I showed above, polymorphism is not a real obstacle for it. I presented two methods to tackle it. Reminder:

  • If you operate on a list of objects with a common base or a common interface – use the interfaces,
  • If you operate on a list of objects with a different structure, use a different interface – use the union.

Ruby Developer Offer

Read More

GraphQL Ruby. What about performance?

Rails and Other Means of Transport

Rails Development with TMUX, Vim, Fzf + Ripgrep