Едно от нещата, които правят Ruby страхотен е, че можем да персонализираме почти всичко според нашите нужди. Това е едновременно полезно и опасно. Лесно е да се простреляме в крака, но когато се използва внимателно, това може да доведе до доста мощни решения.

В Ruby Magic смятаме, че полезно и опасно е отлична комбинация. Нека да разгледаме как 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? Разбира се, че е!

Модифициране на поведението на отделен обект

Да кажем, че искаме да гарантираме, че всички обекти от определен клас винаги ще отпечатват оператори на журнал, дори ако методът е заменен в подкласове. Един от начините да направите това е да добавите модул към единичния клас на обекта.

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 добавя анонимен клас между обекта и действителния му клас. Когато се извикват методи, дефинираните в класа singleton получават предимство пред методите в действителния клас. Тези единични класове са уникални за всеки обект, така че добавянето на методи към тях не засяга други обекти от действителния клас. Научете повече за класовете и обектите в ръководството за програмиране на Ruby.

Малко е тромаво да модифицирате класа singleton на всеки обект, когато се създава. Така че нека преместим включването на класа 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 в инициализатора? За съжаление, тъй като класът singleton все още не включва модула 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 и отваряте ума си за различни решения. Надяваме се статията да ви е харесала.

Ще се радваме да чуем за нещата, които внедрихте чрез промяна на начина по подразбиране на Ruby за създаване на обекти. Моля, не се колебайте да туитвате мислите си до @AppSignal.

Бенедикт Дейке е софтуерен инженер и технически директор на Userlist.io. От друга страна, той пише книга за изграждането на „SaaS приложения в Ruby on Rails“. Можете да се свържете с Бенедикт чрез Twitter.

Първоначално публикувано в blog.appsignal.com на 7 август 2018 г.