Ruby — один из самых популярных языков, используемых в сети. Здесь мы проводим сессию на Nettuts +, которая познакомит вас с Ruby, а также с отличными фреймворками и инструментами, которые сопровождают разработку на Ruby. В этом эпизоде мы рассмотрим слишком крутой, чтобы быть правдивым, способ, которым объекты Ruby работают с методами, которые не существуют.
Видеоурок?
Проблема (и решение)
Допустим, вы работаете с объектом Ruby. И давайте также скажем, что вы не совсем знакомы с этим объектом. И давайте также скажем, что вы вызываете метод, который не существует на объекте.
1
2
3
|
o = Object.new
o.some_method
# NoMethodError: undefined method `some_method’ for #<Object:0x00000100939828>
|
Это менее чем желательно, поэтому у Ruby есть замечательный способ позволить нам избавиться от этого. Проверь это:
1
2
3
4
5
6
7
8
9
|
class OurClass
def method_missing (method_name)
puts «there’s no method called ‘#{method_name}'»
end
end
o = OurClass.new
o.some_method
# => there’s no method called ‘some_method’
|
Мы можем создать метод под названием method_missing
в нашем классе. Если объект, для которого мы вызываем метод, не имеет метода (и не наследует метод от другого класса или модуля), Ruby даст нам еще один шанс сделать что-то полезное: если у класса есть метод method_missing
мы method_missing
информацию о методе cal методу method_missing
и позволим ему разобраться в беспорядке.
Ну, это здорово; мы больше не получаем сообщение об ошибке.
Лучшее использование
Но остановитесь и подумайте об этом на секунду. Прежде всего: нет, мы больше не получаем сообщение об ошибке, но мы не получаем что-то полезное. Трудно сказать, что было бы полезно в этом случае, потому что имя метода ничего не подсказывает. Во-вторых, это довольно мощный инструмент, потому что он позволяет вам практически передавать любой метод объекту и получать разумный результат.
Давайте сделаем то, что имеет больше смысла; начнем с этого:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
class TutsSite
attr_accessor :name, :tutorials
def initialize name = «», tuts = []
@name = name
@tutorials = tuts
end
def get_tuts_about_javascript
@tutorials.select do |tut|
tut[:tags].include?
end
end
def get_tuts_by_jeffrey_way
@tutorials.select do |tut|
tut[:author] == «Jeffrey Way»
end
end
end
|
Здесь вы видите небольшой класс для учебного сайта. При создании нового объекта веб-сайта мы передаем ему имя и массив учебников. Мы ожидаем, что учебники будут хэши в следующей форме:
1
2
3
4
5
|
{ title: «Some title», author: «the author», tags: [«array», «of», «tags»] # Ruby 1.9
# OR
{ :title => «Some title», :author => «the author», :tags => [«array», «of», «tags»] # Ruby 1.8
|
Мы ожидаем символы в качестве ключей; обратите внимание, что если вы не используете Ruby 1.9, вам придется использовать нижний формат для ваших хэшей (оба работают в 1.9)
Затем у нас есть две вспомогательные функции, которые позволяют нам получить только туториал с тегом JavaScript или только туториалы Джеффри Уэя. Они полезны для фильтрации учебных пособий … но они не дают нам слишком много вариантов. Конечно, мы могли бы создать методы с именами get_tuts_with_tag
и get_tuts_by_author
которые принимают параметры с тегом или именем автора. Однако мы собираемся пойти другим путем: method_missing
.
Как мы уже видели, method_missing
получает имя метода в качестве параметра. Я не упомянул, что это символ. Также доступны параметры, которые передаются методу и блоку (если он был задан). Обратите внимание, что параметры передаются в качестве отдельных параметров в method_missing
, поэтому обычным соглашением является использование оператора splat для сбора их всех в массив:
1
2
3
|
def method_missing name, *args, &block
end
|
Таким образом, поскольку мы можем получить имя метода, который пытался выполнить, мы можем проанализировать это имя и сделать что-то интеллектуальное с ним. Например, если пользователь вызывает что-то вроде этого:
1
2
3
4
5
6
7
|
nettuts.get_tuts_by_jeffrey_way
nettuts.get_tuts_about_html
nettuts.get_tuts_about_canvas_by_rob_hawkes
nettuts.get_tuts_by_jeremy_mcpeak_about_asp_net
|
Итак, давайте вернемся к этому; отбросьте те более ранние методы и замените это этим:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
def method_missing name, *args, &block
tuts = @tutorials.dup
name = name.to_s.downcase
if (md = /^get_tuts_(by_|about_)(\w*?)((_by_|_about_)(\w*))?$/.match name)
if md[1] == ‘by_’
tuts.select!
tuts.select!
elsif md[1] == ‘about_’
tuts.select!
tuts.select!
end
else
tuts = «This object doesn’t support the object ‘#{name}'»
end
tuts
end
|
Не волнуйтесь, мы пройдем через все это сейчас. Мы начнем с дублирования массива @tutorials
; у каждого объекта Ruby есть метод dup
который его копирует; если бы мы этого не делали — а просто сказали tuts = @tutorial
мы бы работали с исходным массивом, чего мы не хотим делать; мы хотим сохранить этот массив как есть. Затем мы отфильтруем учебные хеши, которые нам не нужны.
Мы также должны получить имя метода; так как он передается в method_missing
как символ, мы конвертируем его в строку с to_s
а затем to_s
чтобы она была в нижнем регистре с downcase
.
Теперь мы должны проверить, что метод соответствует формату, который мы хотим; в конце концов, возможно, что кто-то мог передать что-то еще методу. Итак, давайте разберем это имя метода. Если это соответствует, мы разработаем магию; в противном случае мы возвращаем сообщение об ошибке по умолчанию:
1
2
3
4
5
|
if (md = /^get_tuts_(by_|about_)(\w*?)((_by_|_about_)(\w*))?$/.match name)
#coming
else
tuts = «This object doesn’t support the method ‘#{name}'»
end
|
Это выглядит довольно устрашающе, но вы должны это понять: в основном, мы ищем «get_tuts_», за которым следует «by_» или «about_»; затем у нас есть имя автора или тег, за которым следует «_by_» или «_about_» и автор или тег. Если это соответствует, мы MatchData
объект MatchData
в md
; в противном случае мы вернем nil
; в этом случае мы установим tuts
сообщения об ошибке. Мы делаем это так, чтобы в любом случае мы могли вернуть tuts
.
Таким образом, регулярное выражение соответствует, мы получим объект MatchData
. Если имя метода было get_tuts_by_andrew_burgess_about_html
, у вас есть следующие индексы:
1
2
3
4
5
6
|
0. get_tuts_by_andrew_burgess_about_html
1. by_
2. andrew_burgess
3. _about_html
4. _about_
5. html
|
Отмечу, что если одна из необязательных групп не заполнена, ее индекс имеет значение nil
.
Итак, данные, которые мы хотим, находятся в индексах 2 и 5; помните, что мы могли получить только тег, только автора или оба (в любом порядке). Итак, затем мы должны отфильтровать слова, которые не соответствуют нашим критериям. Мы можем сделать это с помощью метода select
массива. Он передает каждый элемент в блок, один за другим. Если блок возвращает true
, элемент сохраняется; если он возвращает false
, элемент выбрасывается из массива. Давайте начнем с этого:
1
2
3
|
if md[1] == ‘by_’
tuts.select!
tuts.select!
|
Если md[1]
«by_», мы знаем, что автор пришел первым. Поэтому внутри блока первого вызова select
мы получаем имя автора tut
хеша (в downcase
) и сравниваем его с md[2]
. Я использую метод глобальной замены — gsub
— для замены всех подчеркиваний одним пробелом. Если строки сравниваются как true, элемент сохраняется; в противном случае это не так. Во втором вызове select
мы проверяем тег (хранящийся в md[5]
) в массиве tut[:tags]
. Массив include?
Метод вернет true
если элемент находится в массиве. Обратите внимание на модификатор в конце этой строки: мы делаем это только в том случае, если четвертым индексом является строка «_about_».
Обратите внимание, что мы на самом деле используем метод select
массива: мы используем select!
(с ударом / восклицательным знаком). Это не возвращает новый массив только с выбранными элементами; это работает с фактическим массивом tuts
в памяти.
Теперь, когда вы понимаете это, у вас не должно быть проблем со следующими строками:
1
2
3
4
|
elsif md[1] == ‘about_’
tuts.select!
tuts.select!
end
|
Эти строки выполняют те же действия, что и выше, но они предназначены для имен методов в обратной ситуации: первый тег, необязательный второй автор
В конце метода мы возвращаем tuts
; это либо фильтрованный массив, либо сообщение об ошибке.
Теперь давайте проверим это:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
tuts = [
{ title: «How to transition an Image from B&W to Color with Canvas», author: «Jeffrey Way», tags: [«javascript», «canvas»] },
{ title: «Node.js Step by Step: Blogging Application», author: «Christopher Roach», tags: [«javascript», «node»] },
{ title: «The 30 CSS Selectors you Must Memorize», author: «Jeffrey Way», tags: [«css», «selectors»] },
{ title: «Responsive Web Design: A Visual Guide», author: «Andrew Gormley», tags: [«html», «responsive design»] },
{ title: «Web Development from Scratch: Basic Layout», author: «Jeffrey Way», tags: [«html»] },
{ title: «Protect a CodeIgniter Application Against CSRF», author: «Ian Murray», tags: [«php», «codeigniter»] },
{ title: «Manage Cron Jobs with PHP», author: «Nikola Malich», tags: [«php», «cron jobs»] }
]
nettuts = TutsSite.new «Nettuts+», tuts
p nettuts.get_tuts_by_ian_murray
# [{:title=>»Protect a CodeIgniter Application Against CSRF», :author=>»Ian Murray», :tags=>[«php», «codeigniter»]}]
p nettuts.get_tuts_about_html
# [{:title=>»Responsive Web Design: A Visual Guide», :author=>»Andrew Gormley», :tags=>[«html», «responsive design»]}, {:title=>»Web Development from Scratch: Basic Layout», :author=>»Jeffrey Way», :tags=>[«html»]}]
p nettuts.get_tuts_by_jeffrey_way_about_canvas
# [{:title=>»How to transition an Image from B&W to Color with Canvas», :author=>»Jeffrey Way», :tags=>[«javascript», «canvas»]}]
p nettuts.get_tuts_about_php_by_nikola_malich
# [{:title=>»Manage Cron Jobs with PHP», :author=>»Nikola Malich», :tags=>[«php», «cron jobs»]}]
p nettuts.submit_an_article
# This object doesn’t support the method ‘submit_an_article'»
|
Я пишу результаты этих методов, так что вы можете запустить их в файле ruby в командной строке.
Предупреждение
Я должен отметить, что, хотя это довольно круто, это не обязательно правильное использование method_missing
. Это прежде всего для безопасности, чтобы спасти вас от ошибок. Однако соглашение не плохое: оно широко используется в классах ActiveRecord
которые являются большой частью Ruby on Rails.
Бонус
Вы, вероятно, не знали, что в JavaScript была похожая функция: это метод __noSuchMethod__
для объектов. Насколько я знаю, он поддерживается только в FireFox, но это интересная идея. Я переписал приведенный выше пример на JavaScript, и вы можете проверить его на этом JSBin .
Вывод
Это обертка на сегодня! У меня в рукаве есть интересные вещи с Руби, которые скоро придут к вам. Следите за Nettuts +, и если вы хотите что-то конкретное, дайте мне знать в комментариях!