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.
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!
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
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?
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