미래 지향적인 웹 앱 구축: The Codest의 전문가 팀이 제공하는 인사이트
The Codest가 최첨단 기술로 확장 가능한 대화형 웹 애플리케이션을 제작하고 모든 플랫폼에서 원활한 사용자 경험을 제공하는 데 탁월한 성능을 발휘하는 방법을 알아보세요. Adobe의 전문성이 어떻게 디지털 혁신과 비즈니스를 촉진하는지 알아보세요...
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 is a key component of 객체 지향 프로그래밍. 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 루비 개발자 can obtain polymorphism in three ways:
상속 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.
예시:
class Document
attr_reader :name
end
class PDFDocument < Document
def extension
:pdf
end
end
class ODTDocument < Document
def extension
:odt
end
end
Modules in Ruby have many uses. One of them is mixins (read more about mixins in 궁극의 분석: 루비 대 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.
예시:
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
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
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": "[email protected]"
},
{
"id": 2,
"login": "user2",
"email": "[email protected]"
},
]
}
}
This is probably all we need to know at the moment. So, let’s get to the point.
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.
Below I will present the steps needed to achieve the goal.
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.
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 코드 above?
Types::BaseUnion
.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?
{
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"
},
]
}
]
}
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
에 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
}
}
}
}
}
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:
자세히 보기