Введение в полиморфизм в Ruby on Rails

Если вы когда-либо тратили время на создание объектно-ориентированной программы (ООП), вы, скорее всего, использовали полиморфизм в своем приложении или, по крайней мере, слышали этот термин.

Это то слово, которое вы ожидаете увидеть в учебнике по естественным наукам или информатике. Возможно, вы потратили время на изучение полиморфизма и даже реализовали его в своем приложении без четкого понимания концепции.

Эта статья даст вам более глубокое понимание полиморфизма, в частности, в Ruby on Rails. Для этого мы рассмотрим:

  • Использование полиморфизма в реальном мире
  • Полиморфизм в программировании на основе ООП
  • Как вы можете включить его в свое приложение Rails, чтобы помочь поддерживать высококачественный код.

Давайте начнем!

Полиморфизм в реальном мире

Существует несколько способов определения полиморфизма в различных контекстах. Полезным определением, независимо от контекста, является «способность объекта отображать более одной формы». На самом деле, если разобрать само слово, то poly означает «много», а morph — «форма».

В реальном мире основным примером этого определения может быть женщина, которая одновременно является полицейским, сестрой, чьим-то ребенком, чьей-то матерью и т.д. Каждая роль определяет ее поведение, вносит свой вклад в ее поведение. Каждая роль определяет ее поведение и вносит свой вклад в то, какой личностью она является.

Полиморфизм и генетика

За пределами компьютерного программирования полиморфизм — это термин, обычно ассоциирующийся с биологией и генетикой. В данном контексте полиморфизм более конкретно определяется как генетические вариации, которые приводят к появлению нескольких различных форм или типов особей в рамках одного вида.

Вспомните ягуаров. Ягуары могут иметь несколько вариантов генов, что может влиять на окраску их меха. У большинства ягуаров окрас таунитовый с черными кругами. Однако у них могут быть более светлые или более темные круги из-за измененного гена, а некоторые могут иметь черную окраску меха.

Различная пигментация в пределах одного вида птиц — еще один пример полиморфизма. Рассмотрим гульдиева вьюрка, который имеет очевидные различия в окраске между особями.

Мономорфизм против полиморфизма

Если мы обратим внимание на мономорфизм, то сможем лучше понять полиморфизм. Придерживаясь биологии, мономорфизм можно определить как «вид с одной формой», который сохраняет эту форму на разных этапах своего развития.

Пингвины являются мономорфными. Даже экспертам трудно различить полы. Генные различия у пингвинов минимальны. Поэтому по физическим признакам пингвины практически неотличимы, особенно по размеру и черно-белой окраске.

Поведенческие признаки часто являются самым простым способом различить пол у мономорфных видов.

Давайте обратимся к полиморфизму в программировании, а именно.

Полиморфизм в ООП

Если мы рассмотрим наше первоначальное определение полиморфизма — способность объекта иметь более одной формы — мы можем легко связать его с ООП.

В ООП мы можем использовать один и тот же метод для получения различных результатов, передавая отдельные объекты. Для этого мы можем использовать условия. Однако это может создать громоздкий код и отклонить нас от принципов DRY. Полиморфизм необходим для создания чистых и логичных приложений ООП.

Давайте рассмотрим два примера того, как полиморфизм может быть реализован в таком ООП-языке, как Ruby: через наследование и duck-типирование.

Полиморфизм и наследование в Ruby

Наследование — это когда дочерний класс наследует свойства родительского класса.

Ниже приведен пример того, как мы можем реализовать полиморфизм с помощью наследования:

class Instrument
  def instrument_example
    puts "Saxophone"
  end
end

class Stringed < Instrument
  def instrument_example
    puts "Guitar"
  end
end

class Percussion < Instrument
  def instrument_example
    puts "Drums"
  end
end

all_instruments = [Instrument.new, Stringed.new, Percussion.new]

all_instruments.each do |instrument|
  instrument.instrument_example
end

# Output

# Saxophone
# Guitar
# Drums
Вход в полноэкранный режим Выход из полноэкранного режима

В приведенном выше коде два дочерних класса — Stringed и Percussion — наследуются от родительского класса Instrument. Этот пример является полиморфным, так как мы вызываем метод: instrument_example — и он выводит несколько форм: Saxophone, Guitar, и Drums.

Этот пример достижения полиморфизма через наследование является, по сути, переопределением метода, но помогает получить более четкое представление о полиморфизме в языке ООП.

Duck-Typing и полиморфизм в Ruby

Более практичным примером полиморфизма в ООП является утиная типизация, как показано ниже.

class Guitar
  def brand
    'Gibson'
  end
end

  class Drums
    def brand
      'Pearl'
    end
  end

  class Bass
    def brand
      'Fender'
    end
  end

  class Keyboard
    def brand
      'Casio'
    end
  end

  all_instruments = [Guitar.new, Drums.new, Bass.new, Keyboard.new]

  all_instruments.each do |instrument|
    puts instrument.brand
  end
  # Output

  # Gibson
  # Pearl
  # Fender
  # Casio
Вход в полноэкранный режим Выход из полноэкранного режима

Хотя каждый метод класса имеет имя brand, мы не переопределяем метод (в отличие от полиморфного наследования). Вместо наследования от родительского класса, здесь мы имеем четыре независимых класса, каждый со своим собственным методом. Утиная типизация полезна, так как мы можем просто перебирать классы, чтобы получить результат каждого метода (в отличие от вызова каждого метода по отдельности).

Опять же, утиная типизация является полиморфной, поскольку мы вызываем метод — brand — и генерируем вывод, который принимает несколько форм: Gibson, Pearl, Fender, и Casio. Конечно, утиная типизация и полиморфизм не являются существенными для получения такого результата. Однако они очень полезны для реализации чистого и логичного кода.

Полиморфизм в Ruby on Rails

Полиморфизм хорошо работает в Ruby on Rails как ассоциация Active Record. Если модели по сути делают одно и то же, мы можем превратить их в одну единственную модель, чтобы создать полиморфные отношения.

Придерживаясь темы инструментов, давайте рассмотрим приложение, в котором пользователи могут размещать, комментировать и рецензировать инструменты. Изучите приведенную ниже диаграмму «сущность-связь» (ERD):

В этом примере мы имеем ERD для приложения, в котором пользователь может разместить инструмент с его деталями. Пользователь также может оставить комментарий о размещенном инструменте.

Другие пользователи могут дать оценку инструменту и оценить комментарии, чтобы определить их полезность или достоверность. Эти ассоциации Active Record прекрасно работают и служат целям нашего приложения.

Что если мы захотим добавить другие ассоциации в наше приложение? Нам нужно будет добавить и повторить дублирующие ассоциации.

Например, если бы мы хотели добавить модель user_rating для оценки благонадежности пользователя, нам пришлось бы создать отдельную таблицу с собственными ассоциациями. Это означало бы добавление нового отношения между моделями user и user_rating. Тогда ERD будет выглядеть примерно так:

Теперь у нас есть три модели, по сути дела делающие одно и то же: оценка объекта, но в разных контекстах. Эти ассоциации созрели для полиморфной ассоциации.

Давайте посмотрим на ERD с полиморфными моделями рейтинга:

Поскольку у нас есть колонка rating, я назвал модель reviews, а не ratings, чтобы избежать путаницы. Здесь модели без отзывов по-прежнему имеют ассоциации с другими моделями, но отдельные рейтинговые модели были объединены в одну модель отзывов.

reviewable_type и reviewable_id теперь играют ту же роль, что и отдельные рейтинговые модели, представляя, с какой моделью связан отзыв.

В столбце reviewable_type хранится имя класса модели (user, instrument_post или comment), а в reviewable_id — соответствующий ID этой модели.

Теперь мы можем использовать эти два столбца для связи целого числа рейтинга с конкретным пользователем, постом или комментарием с помощью запросов Active Record и/или условных операторов. Внешний ключ user_id остается в модели рецензии, поскольку он позволяет нам отслеживать, какой пользователь оставил рецензию.

Сейчас термин ‘-able’ в нашей полиморфной модели может показаться странным, но его назначение скоро станет понятным, когда мы сделаем немного магии Rails.

Модель отзывов считается полиморфной, поскольку у нас есть одна модель или объект, который может представлять и принимать несколько форм: пользователь, комментарий и инструмент post reviews.

Реализация полиморфизма в Ruby on Rails

Пора реализовать полиморфизм в Rails-приложении! Если мы будем действовать так, как будто мы уже создали модели user, instrument_post и comment, мы можем приступить к внедрению нашей полиморфной модели: reviews.

Для начала создайте таблицу и сгенерируйте модель из терминала, как показано ниже:

rails g model Review user:belongs_to reviewable:references{polymorphic}
Войти в полноэкранный режим Выйти из полноэкранного режима

Это создаст файл миграции:

class CreateReviews < ActiveRecord::Migration[7.0]
  def change
    create_table :reviews do |t|
      t.belongs_to :user, null: false, foreign_key: true
      t.references :reviewable, polymorphic: true, null: false
      t.integer :rating

      t.timestamps
    end
  end
end
Войти в полноэкранный режим Выйти из полноэкранного режима

Файл schema.rb обновляется после выполнения миграции. Опция polymorphic преобразует столбец reviewable в столбцы reviewable_type и reviewable_id:

  create_table "reviews", force: :cascade do |t|
    t.integer "user_id", null: false
    t.string "reviewable_type", null: false
    t.integer "reviewable_id", null: false
    t.integer "rating"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["reviewable_type", "reviewable_id"], name: "index_reviews_on_reviewable"
    t.index ["user_id"], name: "index_reviews_on_user_id"
  end
Войти в полноэкранный режим Выход из полноэкранного режима
# app/models/review.rb

class Review < ApplicationRecord
  belongs_to :user
  belongs_to :reviewable, polymorphic: true
end
Войти в полноэкранный режим Выход из полноэкранного режима

Помните, выше мы упоминали термин «-able»? Это соглашение Rails для обозначения нашей полиморфной ассоциации, дающее нам возможность сделать пользователя, инструментальный пост и комментарий «просматриваемыми».

Чтобы эта магия Rails сработала, нам нужно убедиться, что другие модели правильно ассоциированы с нашей полиморфной моделью.

# app/models/user.rb

class User < ApplicationRecord
  has_many :instrument_posts
  has_many :comments
  # alias association for user who submitted the review
  has_many :submitted_reviews, class_name: "Review", foreign_key: :user_id
  # association for user, instrument_post and comment that has the review
  has_many :reviews, as: :reviewable
end
Вход в полноэкранный режим Выход из полноэкранного режима
# app/models/instrument_post.rb

class InstrumentPost < ApplicationRecord
  belongs_to :user
  has_many :reviews, as: :reviewable
end
Войти в полноэкранный режим Выход из полноэкранного режима
# app/models/comment.rb

class Comment < ApplicationRecord
  belongs_to :user
  has_many :reviews, as: :reviewable
end
Войти в полноэкранный режим Выход из полноэкранного режима

Модели user, instrument_post и comment теперь могут быть просмотрены и оценены.

Если мы уже создали как минимум двух пользователей, комментарий и пост инструмента, мы можем создать и получить доступ к отзывам различными способами с помощью активных запросов записей, как показано ниже:

# A user (User 2) leaving a review for another user (User 1)
# The reviewable_id is the id of the user to which the review is given
# The user_id is the id of the user who created the user review
user_1 = User.first
user_1.reviews.create(user_id: 2, rating: 2)
Review.where(reviewable_type: "User").first # id: 1, user_id: 2, reviewable_type: "User", reviewable_id: 1, rating: 2
# or
user_1.reviews.first # id: 1, user_id: 2, reviewable_type: "User", reviewable_id: 1, rating: 2
Войти в полноэкранный режим Выйти из полноэкранного режима
# User 2 reviewing a instrument posted by User 1
# The reviewable_id is the id of the instrument_post
# The user_id is the id of the user who created the instrument_post review
post = InstrumentPost.first
post.reviews.create(user_id: 2, rating: 4)
post.reviews.first # id: 2, user_id: 2, reviewable_type: "InstrumentPost", reviewable_id: 1, rating: 4
post.reviews.first.reviewable_type # "InstrumentPost"
Войти в полноэкранный режим Выйти из полноэкранного режима
# User 2 reviewing a comment created by User 1
# The reviewable_id is the id of the comment
# The user_id is the id of the user who created the comment review
comment = Comment.first
comment.reviews.create(user_id: 2, rating: 5)
comment.reviews.first # id: 3, user_id: 2, reviewable_type: "Comment", reviewable_id: 1, rating: 5
comment.reviews.first.rating # 5

# We can then find the user that created the reviewed comment by associating the value of the reviewable_id to the comment id
Comment.where(id: 1) # id: 1, user_id: 1, content: "Comment created by user id 1"
# In this scenario the comment id and user_id just happen to be the same
Вход в полноэкранный режим Выход из полноэкранного режима
user_1 = User.first
user_2 = User.last

# Reviews User 1 has given (none)
user_1.submitted_reviews # []

# Reviews User 2 has given
user_2.submitted_reviews
# id: 1, user_id: 2, reviewable_type: "User", reviewable_id: 1, rating: 2
# id: 2, user_id: 2, reviewable_type: "InstrumentPost", reviewable_id: 1, rating: 4
# id: 3, user_id: 2, reviewable_type: "Comment", reviewable_id: 1, rating: 5

# Counting the total amount of reviews User 2 has given
user_2.submitted_reviews.count # 3
Войти в полноэкранный режим Выход из полноэкранного режима

Существует множество других способов взаимодействия с моделью reviews, в зависимости от того, какие данные вам необходимо отобразить. Важно разделить, как родительские модели могут создавать обзор. Пока обзор связан с пользователем и моделью reviewable, Active Record автоматически связывает reviewable_id и reviewable_type с соответствующей моделью.

Без полиморфизма в наших примерах Rails было бы гораздо больше таблиц, ненужных дублирующих столбцов, ассоциаций belongs_to и has_many в наших моделях. Полиморфизм уменьшил потребность в объединении таблиц, что позволило упростить и ускорить запросы и ассоциации Active Record.

Подведение итогов: Используйте полиморфизм для чистого и логичного кода Ruby

В этом посте мы рассмотрели полиморфизм в двух различных средах: биологии и программировании на Ruby. В обоих случаях полиморфизм — это способность объекта отображать более одной формы.

Мы рассмотрели, как реализовать полиморфизм в Ruby через наследование и duck-typing, прежде чем погрузиться в использование полиморфизма конкретно в Ruby on Rails.

Полиморфизм может помочь вам писать чистый и логичный код. Моя цель — помочь вам добавить эту важную концепцию ООП в ваш арсенал инструментов для ваших текущих, будущих и, возможно, даже прошлых приложений.

Счастливого кодинга!

P.S. Если вы хотите читать посты Ruby Magic, как только они выходят из печати, подпишитесь на нашу рассылку Ruby Magic и не пропустите ни одного поста!

Оцените статью
Procodings.ru
Добавить комментарий