Статьи

Изучение элегантности Синатры: легкая альтернатива рельсам

Все мы знаем 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 — как я — вероятно, проигнорировали бы.