Все мы знаем Sinatra как легкую альтернативу Rails. Я считаю, что использовать это настоящее удовольствие. Вспомогательные методы Sinatra, поддержка шаблонов и маршрутизация обеспечивают достаточную скорость для быстрого запуска простого веб-сайта, но затем сразу же убираются с вашего пути. Спустя годы после появления Sinatra остается одной из самых популярных веб-фреймворков Ruby.
На этой неделе я решил заглянуть внутрь Синатры, чтобы узнать, чему я могу научиться из написанного. Я ожидал найти сложный, хорошо написанный код, который эффективно реализует API Синатры. То, чего я не ожидал увидеть, это код, написанный с реальным чувством стиля и полировки … Внутренние элементы Sinatra соответствуют названию!
Сегодня я покажу вам три примера этого: как Sinatra вызывает ваш код с помощью throw и catch , как Sinatra использует Test :: Unit очень читабельным и СУХИМЫМ способом, и как он использует метапрограммирование элегантным способом, который облегчает его код клиента для использования. Читайте дальше, чтобы узнать больше…
Использование throw / catch для управления потоком программы
Если вы когда-либо использовали или читали о Sinatra, вы помните, что вы предоставляете код для обработки HTTP-запросов с использованием серии блоков и шаблонов маршрутов. Вот основной пример с сайта Синатры :
get '/hi' do "Hello World!" end
Глядя на внутренности Синатры, первое, что я хотел выяснить, было то, как он называл эти блоки кода. Я ожидал увидеть какой-то цикл, проверяющий, соответствует ли текущий путь HTTP-запроса шаблону каждого маршрута, и действительно, вы можете найти этот цикл в Sinatra :: Base.route! метод . Но я не ожидал увидеть, как Синатра реализовал фактический вызов клиентского блока кода маршрута. Это происходит в методе route_eval в Sinatra :: Base :
# Run a route block and throw :halt with the result. def route_eval throw :halt, yield end
А? Что тут происходит? Оператор yield имеет смысл: поскольку клиент предоставляет код маршрута в виде блока, Синатре нужно уступить ему. Но что делает оператор throw ? А что значит : остановка ? Мой блок маршрута как-то возвращает ошибку или исключение? И куда это брошено?
Прежде чем понять, что делает бросок здесь, мы должны рассмотреть, как бросок и ловить работают в Ruby, и как они отличаются от рейза и спасения . Сегодня я не буду тратить время на объяснение того, что с тех пор, как Авди Гримм написал фантастическую статью об этом вопросе прошлым летом: бросить, поймать, поднять, спасти … Я так растерялся! Он даже использовал Синатру в качестве одного из своих примеров. Короче говоря, Авди объяснил, что, несмотря на разные имена, нужно использовать Raise / Rescue для обработки исключений, аналогично try / throw / catchв C ++, Java и других языках. В Ruby, с другой стороны, throw и catch предназначены для использования в качестве еще одной структуры управления программой.
Давайте посмотрим, как Синатра использует throw и catch :
Использование throw / catch для управления потоком программы
Если вы когда-либо использовали или читали о Sinatra, вы помните, что вы предоставляете код для обработки HTTP-запросов с использованием серии блоков и шаблонов маршрутов. Вот основной пример с сайта Синатры :
get '/hi' do "Hello World!" end
Глядя на внутренности Синатры, первое, что я хотел выяснить, было то, как он называл эти блоки кода. Я ожидал увидеть какой-то цикл, проверяющий, соответствует ли текущий путь HTTP-запроса шаблону каждого маршрута, и действительно, вы можете найти этот цикл в Sinatra :: Base.route! метод . Но я не ожидал увидеть, как Синатра реализовал фактический вызов клиентского блока кода маршрута. Это происходит в методе route_eval в Sinatra :: Base :
# Run a route block and throw :halt with the result. def route_eval throw :halt, yield end
А? Что тут происходит? Оператор yield имеет смысл: поскольку клиент предоставляет код маршрута в виде блока, Синатре нужно уступить ему. Но что делает оператор throw ? А что значит : остановка ? Мой блок маршрута как-то возвращает ошибку или исключение? И куда это брошено?
Прежде чем понять, что делает бросок здесь, мы должны рассмотреть, как бросок и ловить работают в Ruby, и как они отличаются от рейза и спасения . Сегодня я не буду тратить время на объяснение того, что с тех пор, как Авди Гримм написал фантастическую статью об этом вопросе прошлым летом: бросить, поймать, поднять, спасти … Я так растерялся! Он даже использовал Синатру в качестве одного из своих примеров. Короче говоря, Авди объяснил, что, несмотря на разные имена, нужно использовать Raise / Rescue для обработки исключений, аналогично try / throw / catchв C ++, Java и других языках. В Ruby, с другой стороны, throw и catch предназначены для использования в качестве еще одной структуры управления программой.
Давайте посмотрим, как Синатра использует throw и catch :
Здесь вы можете видеть, что после того, как один из ваших блоков кода маршрута возвращает значение в route_eval , Sinatra переходит обратно вверх по стеку вызовов к методу с именем invoke , где он фактически начал обработку текущего запроса ранее:
# Run the block with 'throw :halt' support and apply result to the response. def invoke res = catch(:halt) { yield } ... etc ... end
Ruby устанавливает возвращаемое значение блока catch для второго аргумента, передаваемого в throw — в этом случае возвращаемое значение блока кода маршрута или res на диаграмме.
Это просто простейший пример throw в Sinatra — оказывается, что многие из вспомогательных методов, таких как last_modified , redirect , error и т. Д., Все используют throw для возврата к вызову аналогичным образом, обеспечивая соответствующее возвращаемое значение. Вот еще один пример, показывающий, как работает вспомогательный метод Sinatra redirect :
Большим преимуществом здесь является то, что когда клиентский код решает вызвать перенаправление , Sinatra избегает необходимости выполнять все после вызова перенаправления («… много кода здесь…») — или необходимости того, чтобы сам код клиента использовал оператор if / else, чтобы избежать его выполнения. Синатра взялся за то, что должно быть обычной, рутинной задачей программирования на Ruby — вызывая блок кода, — и сделал это стильно и элегантно. Результат — более быстрый и чистый код, как для внутренних дел Синатры, так и для вас!
Читаемый и поддерживаемый набор тестов
Что-то еще во внутренностях Синатры, которое привлекло мое внимание, было то, как он использовал Test :: Unit. Многие разработчики Ruby сегодня предпочитают использовать RSpec или Minitest вместо Test :: Unit, чтобы получить более мощный и читаемый DSL для модульных тестов и BDD. Но Sinatra, как и основная команда Rails, использует простой старый Test :: Unit для своего набора тестов.
Но что за минута … давайте посмотрим на некоторые тесты Синатры:
class BaseTest < Test::Unit::TestCase ... etc ... describe 'Sinatra::Base subclasses' do class TestApp < Sinatra::Base get '/' do 'Hello World' end end it 'include Rack::Utils' do assert TestApp.included_modules.include?(Rack::Utils) end it 'processes requests with #call' do assert TestApp.respond_to?(:call) request = Rack::MockRequest.new(TestApp) response = request.get('/') assert response.ok? assert_equal 'Hello World', response.body end
Это совсем не похоже на Test :: Unit! Вместо этого кажется, что Синатра использует RSpec — почему я вижу описание и его ключевые слова здесь? Что ж, получается, что Синатра использовал небольшую библиотеку под названием « соревнование» , которая добавляет поддержку блоков описания / контекста в Test :: Unit, как вы видели бы в RSpec или Shoulda. Синатра также определил ключевое слово it как псевдоним для теста :
class Test::Unit::TestCase include Rack::Test::Methods class << self alias_method :it, :test ... etc ...
Эти два изменения в Test :: Unit сделали набор тестов Sinatra намного более читабельным… более стильным! Но есть и смысл в этом стиле: например, обратите внимание на прекрасный способ, которым Sinatra создал целое тестовое приложение прямо внутри блока описания:
describe 'Sinatra::Base subclasses' do class TestApp < Sinatra::Base get '/' do 'Hello World' end end
Теперь последующие тесты могут ссылаться на это тестовое приложение и посмотреть, правильно ли Синатра обрабатывает вещи; например:
it 'processes requests with #call' do assert TestApp.respond_to?(:call) request = Rack::MockRequest.new(TestApp) response = request.get('/') assert response.ok? assert_equal 'Hello World', response.body end
Вау — это так просто и легко читать. Здесь Синатра также использует превосходную библиотеку Rack :: Test Брайана Хелмкампа , которая предоставляет объект Rack :: MockRequest .
Я также был впечатлен тем, как набор тестов Синатры был очень СУХИМ — вот еще несколько тестов из файла erb_test.rb:
it 'renders inline ERB strings' do erb_app { erb '<%= 1 + 1 %>' } assert ok? assert_equal '2', body end it 'renders .erb files in views path' do erb_app { erb :hello } assert ok? assert_equal "Hello World\n", body end
Эти два теста и многие другие тесты в одном и том же файле используют метод erb_app для создания тестового приложения Sinatra и перехода к предоставленному блоку в контексте этого приложения. Синатра достигает этого, используя вспомогательный метод в верхней части erb_test.rb:
def erb_app(&block) mock_app { set :views, File.dirname(__FILE__) + '/views' get '/', &block } get '/' end
И Синатра определяет mock_app в файле helper.rb:
# Sets up a Sinatra::Base subclass defined with the block # given. Used in setup or individual spec methods to establish # the application. def mock_app(base=Sinatra::Base, &block) @app = Sinatra.new(base, &block) end
Sinatra DRY-ed разработала свой набор тестов, в основном используя эти два вспомогательных метода, наряду с другими, похожими методами. По этой причине набор тестов Sinatra не только легче читать, но и легче поддерживать. В моих личных проектах я часто не применяю столько же любви и внимания к своему тестовому коду, сколько к своему производственному коду — здесь определенно есть урок!
Метапрограммирование со стилем
Одна из особенностей Sinatra, с которой вы, возможно, не знакомы, — это регистрация расширений стандартной DSL Sinatra. Вот пример, который они используют в своей документации по написанию расширений :
require 'sinatra/base' module Sinatra module LinkBlocker def block_links_from(host) before { halt 403, "Go Away!" if request.referer.match(host) } end end register LinkBlocker end
После того, как вы зарегистрировали такой модуль расширения, вы можете использовать его в своем приложении Sinatra следующим образом:
require 'sinatra' require 'sinatra/linkblocker' block_links_from 'digg.com' get '/' do "Hello World" end
Что меня заинтересовало в этом, так это то, как Синатра реализовал метод регистрации внутри. Давайте посмотрим на это:
# Register an extension. Alternatively take a block from which an # extension will be created and registered on the fly. def register(*extensions, &block) extensions << Module.new(&block) if block_given? @extensions += extensions extensions.each do |extension| extend extension extension.registered(self) if extension.respond_to?(:registered) end end
Хотя поначалу это может быть немного сложно понять, на самом деле это довольно просто. Код клиента (ваше приложение Sinatra) передается в виде расширения, например, при вызове регистрации LinkBlocker, который мы видели выше. Затем этот модуль расширения добавляется в массив с именем @extensions, а затем Sinatra перебирает массив и расширяет себя с каждым расширением.
Это звучит довольно обыденно и просто — я бы использовал гораздо более простой код, чем этот, чтобы сделать то же самое! Что еще здесь происходит?
Как и большая часть кода внутри Sinatra, метод register не просто реализован простым способом — он выполнен с настоящим чувством стиля! Давайте внимательнее посмотрим на некоторые детали здесь:
- Во-первых, использование * расширений вместо просто расширений . Это позволяет клиентскому коду передавать одно расширение или список расширений по желанию. Это очень распространенная идиома Ruby, и Синатра хорошо использует ее здесь.
- Далее обратите внимание на строку кода, содержащую вызов Module.new . Это более тонко и интересно. Здесь Sinatra позволяет клиентскому коду передаваться в блоке вместо или в дополнение к фактическим модулям. Если блок задается клиентским кодом ( block_given? ), То Синатра прямо на месте создает новый анонимный модуль! Затем он добавляет анонимный модуль в массив расширений . Это дает клиентскому коду гибкость для передачи в реальных модулях и / или анонимных блоках кода.
- Наконец, обратите внимание на строку в конце, которая вызывает response_to? , О чем это все? Что ж, здесь Синатра проверяет, содержит ли новый модуль расширения метод с именем зарегистрирован . Если это так, он вызывает это после применения расширения. Это дает клиентскому коду возможность реализовать зарегистрированный метод, если ему необходимо предпринять какие-либо действия на этом этапе.
Обращая внимание на эти мелкие детали метапрограммирования, Sinatra добавил большую гибкость в метод register и упростил работу клиентского кода. Подобные примеры вы можете найти в других метапрограммированиях, используемых в других местах кодовой базы Синатры.
Вывод
Все это довольно незначительные детали, но, на мой взгляд, они поднимают внутренний код Синатры с чего-то нормального и обычного до более высокого уровня — чтобы он был чем-то стильным и исключительным. Разработчики Sinatra не были удовлетворены созданием еще одного веб-фреймворка. Они хотели создать что-то особенное, что люди хотели бы использовать. И способ, которым они достигли этого, состоял в том, чтобы обратить внимание на маленький дизайн и детали кода, которые большинство разработчиков Ruby — как я — вероятно, проигнорировали бы.