Едно от нещата, които правят 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 г.