Одна з особливостей, яка робить 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, що дозволяє вам знаходити альтернативні рішення для вирішення тих чи інших проблем.
Ще немає коментарів