Зміна способу створення об'єктів Ruby

5 хв. читання

Одна з особливостей, яка робить Ruby чудовою мовою програмування, полягає в тому, що ми можемо практично будь-що кастомізувати під наші потреби. Це корисно й небезпечно водночас. Дуже легко собі нашкодити. Проте при обережному використанні, можна отримати доволі потужні рішення.

Основи створення нових об'єктів з класів

Подивімось, як створювати об'єкти в Ruby. Щоб створити новий об'єкт (або екземпляр), ми викликаємо new на класі. На відміну від інших мов, new не є ключовим словом самої мови. Це метод, що викликається так само, як і будь-які інші.

class Dog
end

object = Dog.new

Щоб налаштувати щойно створений об'єкт, можна передати аргументи методу new. Що б не було передано як аргумент, воно буде передано ініціалізатору.

class Dog
  def initialize(name)
    @name = name
  end
end

object = Dog.new('Good boy')

Знову ж таки, на відміну від інших мов, ініціалізатор в Ruby — всього лише метод, а не якийсь спеціальний синтаксис або ключове слово.

Беручи це до уваги, чи можливо поекспериментувати з цими методами точно так само, як це можливо з іншими методами Ruby? Звичайно!

Зміна поведінки одного об'єкта

Скажімо, нам потрібно впевнитись, що всі об'єкти конкретного класу завжди друкуватимуть логи, навіть якщо метод перевизначений у підкласах. Один зі способів це зробити — додати модуль у singleton об'єкта.

module Logging
  def make_noise
    puts "Started making noise"
    super
    puts "Finished making noise"
  end
end

class Bird
  def make_noise
    puts "Chirp, chirp!"
  end
end

object = Bird.new
object.singleton_class.include(Logging)
object.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise

У цьому прикладі об'єкт Bird створений за допомогою Bird.new, а модуль Logging включений в результівний об'єкт за допомогою його класу синглтона.


Що таке singleton?

Ruby дозволяє використовувати методи, що є унікальними для одного об'єкта. Для цього Ruby додає анонімний клас між об'єктом та його фактичним класом. Коли методи викликаються, ті, що визначені в синглтоні, мають перевагу над методами фактичного класу. Ці синглтони є унікальними для кожного об'єкта, тому додавання методів до них не впливає на інші об'єкти фактичного класу. Для детальної інформації про класи й об'єкти дивіться в посібнику з програмування на Ruby.


Змінювати синглтон кожного об'єкта, коли він створюється, трохи незручно. Тому перейдімо до включення класу Logging в ініціалізатор, щоб додати його до кожного створеного об'єкта.

module Logging
  def make_noise
    puts "Started making noise"
    super
    puts "Finished making noise"
  end
end

class Bird
  def initialize
    singleton_class.include(Logging)
  end

  def make_noise
    puts "Chirp, chirp!"
  end
end

object = Bird.new
object.make_noise
# Started making noise
# Chirp, chirp!
# Finished making noise

Хоча все працює добре, якщо ми створимо підклас Bird, наприклад Duck, його ініціалізатор повинен викликати super, щоб зберегти поведінку Logging. Хоча можна стверджувати, що це гарна ідея — завжди правильно викликати super при кожному перевизначенні методу, спробуймо знайти спосіб, який цього не потребуватиме.

Якщо ми не викличемо super з підкласу, то загубимо включення класу Logger:

class Duck < Bird
  def initialize(name)
    @name = name
  end

  def make_noise
    puts "#{@name}: Quack, quack!"
  end
end

object = Duck.new('Felix')
object.make_noise
# Felix: Quack, quack!

Натомість, перепишімо Bird.new. Як вже згадувалось раніше, new — лише метод, реалізований у класах. Тому ми можемо перевизначити його, викликати super й змінити щойно створений об'єкт під наші потреби.

class Bird
  def self.new(*arguments, &block)
    instance = super
    instance.singleton_class.include(Logging)
    instance
  end
end

object = Duck.new('Felix')
object.make_noise
# Started making noise
# Felix: Quack, quack!
# Finished making noise

Але що трапиться, коли ми викличемо make_noise в ініціалізаторі? На жаль, оскільки синглтон ще не включає модуль Logging, ми не отримаємо бажаний вивід.

На щастя, є рішення: можна створити з нуля поведінку за замовчуванням для .new за допомогою allocate.

class Bird
  def self.new(*arguments, &block)
    instance = allocate
    instance.singleton_class.include(Logging)
    instance.send(:initialize, *arguments, &block)
    instance
  end
end

Виклик allocate повертає новий, неініціалізований об'єкт класу. Тому після цього ми можемо включити додаткову поведінку, і тільки тоді викликати метод initialize на цьому об'єкті (оскільки initialize є приватним за замовчуванням, нам для цього потрібно вдатися до використання send).


Правда про Class#allocate

На відміну від інших методів, allocate перевизначити неможливо. Ruby не використовує стандартний спосіб направлення для allocate всередині. В результаті, просто перевизначення allocate без перевизначення new не спрацює. Однак, якщо ми викликаємо allocate напряму, Ruby викличе перевизначений метод. Дізнатися більше про Class#new та Class#allocate можна в документації Ruby.


Навіщо це все потрібно?

Як і з багатьма речами, зміна стандартного способу створення об'єктів з класів у Ruby може бути небезпечною, і щось може піти не так.

Тим не менш, існують успішні приклади зміни способу створення об'єктів. Наприклад, ActiveRecord використовує allocate з іншим методом init_from_db для зміни процесу ініціалізації при створенні об'єктів з БД, на відміну від створення об'єктів, що не зберігаються. Він також використовує allocate для конвертації записів між різними типами наслідування з однією таблицею за допомогою becomes.

Найголовніше, граючись зі створенням об'єктів, ви отримуєте більш чітке представлення про те, як працює Ruby, що дозволяє вам знаходити альтернативні рішення для вирішення тих чи інших проблем.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 6.2K
Приєднався: 7 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація