Введение от автора
«У множества языков есть веб-фреймворки, но Rails есть только у Ruby».
Кирилл Мокевнин, основатель Hexlet
Перешедший из весовой категории «новенький и блестящий» в ранг «проверенный временем и надёжный», Ruby упорно не желает отправляться в рай для красивых языков программирования.
Он сочетает в себе структурный, объектный и функциональный стили программирования, обладает лаконичным синтаксисом и ясными грамматиками, построен по «принципу наименьшего удивления». Ruby позволяет воплощать в изящном и легко читаемом коде любые изыски алгоритмической и технологической мысли.
Огромное количество накопленного в книгах по Ruby-разработке теоретического и практического знания позволяет использовать язык во множестве решений — от преподавания программирования и теории вычислений в вузах до промышленного программирования веб-сервисов и ERP-систем. Фреймворк Ruby on Rails год за годом остаётся одним из флагманов веб-разработки. Он позволяет собирать системы разного уровня сложности — от личного блога до гетерогенных микросервисных платформ. Опираясь на свойства Ruby как языка, компоненты и инструменты Rails делают разработку сложных веб-сервисов технологичным и эффективным процессом, в котором уютно себя чувствует и разработчик, и менеджер.
Компания Evrone работает на рынке веб-разработки уже 13 лет. Ruby on Rails — одна из технологий, которую мы используем часто, поэтому накопили внушительный багаж знаний — от продвинутых приёмов программирования на Ruby до эффективного использования «магии» Rails. Чтобы делиться этими знаниями, мы запускали в тестовом режиме Evrone Academy — платформу для обучения программированию. Сейчас она на паузе, но мы хотим публично поделиться лекциями и знаниями с теми, кто хочет поближе познакомиться с Ruby уже сейчас, в привычной хабра-обстановке.
Этот курс рассчитан на тех, у кого уже есть базовые навыки и знания в области структурного и объектно-ориентированного программирования. Это ваш «второй курс» по Ruby on Rails.
Он начинается с обзора продвинутых свойств Ruby в трёх основных стилях программирования — структурно-императивном, объектно-ориентированном и функциональном. Затем мы перейдём к разработке на фреймворке Ruby on Rails — начнём с введения в его фундаментальные концепции, компоненты и их свойства, разберем инструменты и библиотеки, важные в повседневной практике, и пройдёмся по продвинутым техникам, высокотехнологичным приёмам и архитектурным паттернам.
Надеюсь, с нашим курсом вам не будет скучно.
Павел Аргентов
Ruby Lead и ментор в Evrone, автор курса
Текстовая версия
Различают несколько основных стилей программирования:
императивный
декларативный
функциональный
объектно-ориентированный
В Ruby можно использовать любой из них, потому что Ruby — чистый объектно-ориентированный язык, в которым любые объекты можно наделить любым поведением, нужным в конкретной программе. Сегодня мы поговорим про самый простой, императивный стиль программирования.
В этом стиле мы последовательно приказываем машине, что и в какой последовательности мы хотим делать. Например — «выведи что-то в консоль, а теперь сделай то-то». В этом случае машина последовательно исполняет инструкции, которые мы подготовили и описали. На примере видим инструкцию «выведи в консоль Hello World»:
puts 'Hello world!'
puts 'Hello world!'
puts 'И Вы, программист, тоже здравствуйте'
Инструкций может быть много, они могут помещаться на разных строках или на одной и той же, но тогда их надо разделять точкой с запятой:
puts 'Hello world!'; puts 'Прекрасная погодка нынче!'
Ruby — умный язык. Всегда, в том числе когда мы работаем в нём в самом простом стиле, он воспринимает текст программы как комбинацию выражений. Все приведенные ранее примеры — это выражения. Они отличаются от инструкций тем, что язык не только исполняет прямые указания, но и вычисляет. Каждое выражение порождает какие-то значения, возвращает какой-то результат. Проще всего это увидеть в интерактивной оболочке Ruby:
[1] pry(main)> puts 'Hello world!'
Hello world!
=> nil
Выполненное выражение напечатало нужное сообщение в консоль, а ещё вычислило значение nil
— пустой объект. Можете поэкспериментировать с интерактивным режимом на своей машине и посмотреть, как Ruby выполняет разные выражения.
Технически, всё в Ruby — это выражения, даже если они ведут себя как инструкции и возвращают пустой объект — nil
. Сейчас для нас это не особенно важно, но мы вернёмся к этому в следующих уроках, когда будем разбирать более сложный код.
Теперь поговорим о переменных. Переменную можно представить себе как контейнер, куда программа помещает какое-то значение и работает с ним: хранит, изменят, передаёт для выполнения различных операций. В следующем примере две переменные: a
и b
. То что написано слева от оператора присваивания — это имя переменной. Справа — значение переменной. Мы связали значение 42
с переменной a
и значение 21
с переменной b
:
a = 42
b = 21
Переменные могут участвовать в вычислениях. Например — «возьми значение переменной a
и сложи со значением переменной b
, а результат запиши в переменную c
»:
c = a + b
В переменных могут храниться значения сложных выражений, например, результат интерполяции строк. Интерполяция работает для строк в двойных кавычках. В примере мы взяли строку и подставили в нужное место значение переменной а
:
str = "Ответ на Главный вопрос жизни, вселенной и всего такого: #{a}"
Переменные можно использовать для передачи значений в методы. Например, мы в метод puts
передаём значение переменной str
:
[4] pry(main)> puts str
Ответ на Главный вопрос жизни, вселенной и всего такого: 42
=> nil
Имена пишутся латиницей. Они регистрочувствительны. Две переменные a
и A
не одинаковы. Язык будет работать с ними как с двумя разными переменными. Советую не злоупотреблять переменными с одинаковым названием в разном регистре, чтобы не запутаться.
Имя переменной не может начинаться с цифры, но может начинаться со знака подчёркивания. Если имя переменной начинается с большой буквы, то Ruby будет считать такую переменную константой. Кроме переменных именами обладают модули, классы, методы. Если имя переменной состоит из несколько слов, эти слова принято разделять знаком _
. В именах констант для этого принято использовать «camel case», когда слова пишутся без пробелов, но все с большой буквы. Если все буквы имени заглавные, слова разделяют при помощи _
:
long_variable_name
_this_variable_i_will_not_use
LongClassNameForWhichTheColleaguesWillHateYou
CONSTANT_IS_STATIC
Иногда в коде можно увидеть, что в начале имён переменных используются специальные символы, уточняющие их особые свойства. Сейчас мы просто привыкаем к тому, как они выглядят, а чуть позже разберемся с этими особыми свойствами:
@foo # переменная экземпляра
@@bar # переменная класса
$baz # глобальная переменная
Переменные можно использовать повторно, несколько раз присваивая разные значения одному и тому же имени, но это считается плохой практикой. Опытные программисты пользуются этим свойством языка только в исключительных случаях, например для оптимизации алгоритмов.
Методами в Ruby называется то, что в других языках называют функциями. Отличие методов от функций в том, что методы всегда "прикрепляются" к какому-то объекту. В этом особенность Ruby как чистого объектного языка. В нём любое выражение обозначает либо объект, либо вызов методов объекта.
Для примера определим простую функцию с названием function
в интерактивном режиме и посмотрим, как она будет себя вести:
[5] pry(main)> def function(parameter)
[5] pry(main)* parameter * 3.1415926
[5] pry(main)* end
=> :function
Мы объявили некоторую функцию с названием function
, определили у неё один параметр, и умножаем этот параметр на короткую версию числа Пи. После передаём какой-то аргумент в эту функцию — пишем function
, передаём пятёрку и получаем некий результат на выходе:
[6] pry(main)> function(5)
=> 15.707963
В следующем примере мы берём переменную object
и связываем её с объектом self
, который сейчас обозначает «контекст исполнения программы». Мы говорим: вызови метод function
текущего контекста:
[7] pry(main)> object = self
=> main
[8] pry(main)> object.function(8)
=> 25.1327408
В Ruby также можно определить особые методы, которые ведут себя как самостоятельные объекты, но это мы рассмотрим в рамках отдельной лекции. А сейчас повторим, как объявить метод. Мы пишем имя метода, описываем параметры, пишем тело. метод можно вызвать, а результат его выполнения — передать в другой метод и т.д. При описании параметров и при вызове метода можно опускать круглые скобки, но и в этом случае нужно разделять параметры/аргументы запятыми:
def quadruple(parameter)
parameter * 4
end
quadruple(8)
# => 32
def multiply a, b
a * b
end
multiply 2, 2
# => 4
Методам можно задавать параметры по умолчанию. В следующем примере второй параметр метода multiply
примет значение 1, если его не передадут при вызове. В примере мы передали только четвёрку, но если бы мы передали 4-запятая-2 то второй параметр был бы двойкой, вместо единицы:
# параметр со значением по умолчанию
def multiply(x, multiplier = 1)
x * multiplier
end
multiply 4
# => 4
Также в Ruby существуют именованные параметры. Для них также возможно задание значений по умолчанию:
# именованный параметр
def greeting(name: 'Nobody')
"Aloha #{name}"
end
greeting
# => "Aloha Nobody"
greeting(name: "Arthur")
# => "Aloha Arthur"
Отдельно отметим отличие терминов «параметры» и «аргументы». То, что мы описываем в коде функции, принято называть параметрами. То, что передаём при вызове — аргументами.
В приведённых примерах нет оператора return
в конце метода. Это иллюстрация того, как Ruby работает с выражениями. Метод — частный случай составного выражения. В Ruby значением составного выражения является значение последнего подвыражения. Такое свойство позволяет программисту компактнее выражать свои мысли, а языку — однородно работать с возвратами из методов в ситуациях различной сложности.
Оператор return
использовать не запрещено, но использование его в конце метода — плохой стиль. Хороший стиль — использовать return
в так называемых защитных выражениях, для «раннего возврата» из метода:
def safe_divide(a, b)
return Nothing if b == 0
Some(a / b)
end
Если у оператора return
нет аргументов, возвращается nil
, то есть пустой объект. Метод с пустым телом в следующем примере тоже возвращает nil
как результат своего выполнения. Обратите внимание, что само объявление метода — это также выражение, значением которого является имя этого метода в особом виде — символ:
def fun
end
# => :fun
Поговорим об управляющих выражениях в Ruby. Кроме того, что они управляют последовательностью выполнения вычислений других выражений, они также вычисляются сами. К управляющим выражениям относятся ветвление, цикл, составные выражения, блоки и обработчики исключений. На двух примерах различные ветвления: в одну строку, простые if, if-else, выражения с модификаторами, тернарные операторы, кейс-выражения:
z = gets.to_i
# ветвление в одну строку
a = if z > 0 then z else 42 end
# простой if
if z == 4
puts 'FOUR'
end
# полный if-else
if z == 42
puts 'The Answer is correct!'
else
puts 'The Answer is incorrect'
end
# выражение с модификатором
puts("Negative number #{z}") if z < 0
# тернарный оператор
z > 0 ? :ok : :not_ok
# case-выражение
resolution = case z
when 100_500 then :foo
when 42 then :oops
when 1 then :smol
else :no_way
end
Ruby как и многие другие языки программирования, поддерживает циклы. Их можно объявить с помощью слов while
и for
. Попробуйте выполнить этот пример самостоятельно и порассуждать, как он работает:
n = 0
ns = []
while n < 100 do
ns << n if n.odd?
n += 1
end
is = 0
for i in 0..100_500
is += 25 if i % 5 == 0
end
Также в качестве управляющего выражения можно встретить составные выражения. В качестве границ составного выражения можно использовать круглые скобки либо begin
и end
, как в примере:
x = begin
b = 1
a = 2
b + a
end
# => 3
Особенность Ruby — возможность описывать повторяющиеся вычисления в виде блоков, которые являются свойством сложных объектов — итераторов. Блок — это одно или несколько выражений, заключённых между словами do и end или фигурными скобками:
[32] pry(main)> 5.times do |i|
[32] pry(main)* puts "This is iteration #{i}"
[32] pry(main)* end
This is iteration 0
This is iteration 1
This is iteration 2
This is iteration 3
This is iteration 4
=> 5
В блоках можно описывать параметры, в виде списка в вертикальных скобках. Блок — это одна из главных суперспособностей Ruby. Мы часто будем видеть блоки в разбираемых примерах.
В Ruby есть специальный механизм прерывания вычислений. Это исключения. Исключения — это особые объекты, несущие информацию о точке прерывания вычислений. Их можно активировать произвольно из любого участка кода. Также мы можем их перехватывать и обрабатывать.
Если исключение не «поймать», вся программа прервётся в точке его активации и выдаст сообщение об ошибке. В обработчиках исключений пишут действия, которые программе нужно выполнить для аккуратного завершения работы или обхода опасного участка:
# Необработанное исключение
[1] pry(main)> puts "I am your father!"
I am your father!
=> nil
[2] pry(main)> raise "NOOOOOOOOOOO!!!"
RuntimeError: NOOOOOOOOOOO!!!
На следующем примере видно активацию, перехват и обработку исключения.
# Исключение с обработчиком
def luke(vaders_phrase)
raise "NOOOOOO!!!" if vaders_phrase == "I am your father!"
puts "Luke keeps hanging on"
rescue => e
puts "Team saves Luke after #{e}"
end
[4] pry(main)> luke "Hang on!"
Luke keeps hanging on
[5] pry(main)> luke "I am your father!"
Team saves Luke after NOOOOOO!!!
Наш обзор императивных свойств языка Ruby мы закончим областями видимости переменных. Понимание областей видимости — это важный элемент рассуждения о программе — для программиста, мощный инструмент управления процессом вычислений — для машины.
Область видимости переменной — это регион программы, в котором возможно её использование. Ранее мы упоминали, существуют особые знаки, которые управляют областями видимости переменных:
@foo # переменная экземпляра: определяется в цикле жизни экземпляра
@@bar # переменная класса: определяется для всего класса
$baz # глобальная переменная: видна во всей программе
В первой строке — пример объявления переменной экземпляра класса, которая доступна только в рамках текущего, конкретного экземпляра класса. Она объявляется с помощью @
перед названием переменной. С помощью @@
объявляются переменные класса, которые доступны во всех экземплярах класса. С помощью $
перед названием переменных объявляются и используются глобальные переменные, которые доступны в любом участке вашего кода.
Глобальных переменных следует избегать. Они способны значительно затруднить понимание и выполнение программы. Если переменная может быть объявлена в одном месте, использоваться в другом, изменяться в третьем — будет очень сложно контролировать такую программу.
Переменные экземпляра класса являются автоматическими. Их можно использовать без объявления. Значением неинициализированной переменной экземпляра класса является nil
.
Если перед именем переменной нет специального знака, такая переменная является локальной. Область в пределах которой видна локальная переменная называется локальной областью видимости. Локальные области видимости создаются при определении классов, модулей, методов или блоков.
Управляющие выражения не создают локальные области видимости. В примере мы видим, метод scope_test
, внутри которого объявляется переменная а
; мы можем вызвать метод scope_test
и получить то, во что вычислилось последнее выражение этого метода, однако а
не будет видно «снаружи» и в этом месте мы получим исключение:
def scope_test
a = 100
end
scope_test
# => 100
a
# => NameError: undefined local variable or method `a' ...
В следующем примере, переменная d
задаётся внутри блока, внутри которого создается новая локальная область видимости. Обращение к d
вне блока также активирует исключение:
5.times do |i|
puts "Iteration #{i}"
d = i
end
d
# => NameError: undefined local variable or method `d' ...
Локальная область видимости распространяется на локальные области, вложенные в неё. Если во вложенных областях видимости присвоить значение переменной которая совпадает с именем переменной, определённой во внешней локальной области — произойдёт изменение внешней переменной. При определении блоков либо методов, параметры которых одноимённы переменным или методам во внешней локальной области видимости, происходит так называемое затенение переменной:
a = 34
5.times do |i|
a += i # мутируется внешняя переменная `a`
p [i, a]
end
a
# => 44
За редким исключением, затенение считается плохим стилем, так как является потенциальным источником ошибок. В примере наглядно видно, как работает затенение: есть переменная bool
, которая описана во внешней области видимости со значением false
, и есть какой-то блок, внутри которого есть параметр блока с таким же названием, bool
:
bool = false
bools.each do |bool| # формируется затенение
if func(bool) == 42
bool = true # мутируется внутренняя переменная блока
end
end
bool # => false : внешняя переменная не изменилась
В данном случае присвоение переменной значения true
, никак не влияет на переменную во внешнем контексте, соответственно bool
во внешней области видимости так и останется связанным со значением false
.
Вот второй пример: есть метод или класс с некоторым именем и метод, в котором определён параметр с таким же именем; этот параметр не будет относится к методу, который мы объявили выше:
def zero
0
end
def add3(zero) # затенение
zero + 3 # используется локальная переменная, а не внешний метод
end
zero
# => 0
add3(8)
# => 11
В императивном стиле программа воспринимается как «глупый» вычислитель, выполняющий последовательность инструкций в порядке ввода. На протяжении курса мы увидим, что Ruby гораздо умнее, и наши программы на нём могут быть совсем не глупыми.