Дислеймер

Это не статья про руби, это статья про любой язык. Пример кода на руби просьба прочитать как псевдокод, понять и забыть что он был на ruby

А нахуй он нам собственно нужен?

Собственно, основное отличие фреймворка от обычной библиотеки в том, что фреймворк определяет структуру программы, которая на нем написанна.

  • Это может быть похоже на mini-DSL в руби (например, фреймворк RSpec определяет синтаксис, использующийся для описания автотестов)
  • Это могут быть просто правила поиска констант (например, фреймворк ActiveSupport)
  • Вообще, фреймворком можно назвать любую библиотеку, которая размножается имплементацией интерфейсов в широком смысле (например, фреймворк ActiveRecord)
  • Фреймворк может отвечать за настройку приложения (например, Spring из мира java)
  • И все ожидали увидеть здесь, что фреймворк может заменять точку входа чем то более приличным (например, фреймворк ActionPack)

Есть еще фреймворки, в которых фреймворки собраны воедино, чтобы пользователи могли с удобством пользоваться библиотеками, затрагивающие несколько фреймворков одновременно (например, rails).

Вообще, правила хорошего тона подразумевают, что вы будете использовать rails / hanami (упаси господь) / trailblazer (тоже не надо) во всех случаях кроме тех, в которых они вам вредны. То есть, если вы делаете сервис, который общается с миром только через RabbitMQ, и при этом в качестве ORM используете Sequel то все равно лучше использовать rails, чем не использовать. Это может быть контринтиунтивно, особенно если вы знаете что rails (django, play) говно, но это реально решает ваши проблемы с использованием rails-библиотек и упрощает вхождение в проект для новичков. Иными словами, фреймворк задает архитектуру, а знакомая архитектура это здорово.

Но если вдруг у вас микросервисная архитектура и все прокеты устроены одинаково просто, то ничего не мешает отказаться от фреймворка вообще, главное чтобы у вас была одна и та же архитектура в пределах команды.

Это не железное правило, всегда можно сделать один эксперементальный проект на новой архитектуре, но если она окажется круче, остальные проекты придётся объявить legacy или рефакторить

И так, с архитектурой мы разобрались

Для пиздатой архитектуры можно использовать фреймворк, а можно сделать свои гаедлайны, которые, разумеется, хорошо бы отдраить и вынести в фреймворк.

Разумеется, вам потребуются фичи, без которых веб-приложение как то не очень. Перечислим их и начнем разбираться

Router / url builder

типичный пример — ActionDispatch

  • умеет превращать параметры запроса (из rack, например) в инструкции для обработки запросов (пары контроллер-акэшен)
  • умеет превращать инструкции (пары контролер-экшен с набором параметров) в url для ссылок

С первым пунктом всё понятно. Второй пункт нам интересен для

  • автогенерации доков на API
  • генерации ссылок (для модного rest с метаданными, серверных вьюх, почты в конце концов)

Иными словами, речь идёт о построении биективного оборажение урлов на какие то понятные нам структуры

К сожалению, в мире руби нет реализации подобного в библиотеках, которые не стыдно использовать отдельно (хотя, есть ActionDispatch, но он даже в тестах начинает глючить).

Так как мы строим биективное отображение, в лямбду это лучше не пихать, так что паттерн responsibility chain (foo || bar || baz) нам не особо подходит. Но можно его и усложнить

Action = Struct.new(:controller, :action, :params)

Route = Struct.new(:check_requst, :check_action,
                   :requst_to_action, :action_to_path) do

  def self.all(routes)
    routes.reduce(:or)
  end

  def or(other)
    Route.new(
      ->(x) { check_request(x) || other.check_request(x) },
      ->(x) { check_action(x) || other.check_action(x) },
      ->(x) { check_request(x) ? reqest_to_action(x) : other.reqest_to_action(x) },
      ->(x) { check_action(x) ? action_to_path(x) : other.action_to_path(x) }
    )
  end
end

def uri_match(method, uri, controller, action, params = {})
  params = Params.new(params) # implements === to match request params
  Route.new(
    ->(x) { x.method == method && pattern === x.uri && params === x.params },
    ->(x) { x.controller == controller && x.action == action },
    ->(x) { Action.new(controller, action, x.params) },
    ->(x) { build_path(uri, x.params) }
  )
end

Route.all [
  uri_match('GET', '/users', UsersController, :index),
  uri_match('GET', %r{\Ausers/\d+\z}, UsersController, :show),
  uri_match('POST', '/users', UsersController, :create),
  uri_match('PUT', %r{\Ausers/\d+\z}, UsersController, :update),
  uri_match('DELETE', %r{\Ausers/\d+\z}, UsersController, :destroy)
]

Некоторые детали реализации скрыты, но вроде все понятно.

Голос сбоку подсказывает, что надо бы еще добавить кошерный inspect, чтобы можно было смотреть роуты из консоли и херачить грепом

ORM

ORM состоит из двух частей, это query_builder и data_mapper. Жить без них можно, но грустно. Сами мы ORM делать не будет, возьмем AR (если мы хотим AS) и Sequel если нехотим или просто любим Sequel.

Можно и чистый SQL фигачить, но замучаемся без санитизации query_builderа, да и data_mapper лучше, чем просто хэш. Хотя data_mapper можно и выкинуть для перформанса

И всё

Нет, серьездно, остальное можно получить без фреймворков.

Окей возьмем RSpec для тестов, он крутой, но для веб-приложения нам больше ничего не нужно

Таким нехитрым образом

Мы научились делать полноценне rest-прилоржения для любого языка без фреймворка. А почту пусть микросервис собирает или SASS решение.

Значит ли это, что рельсы не нужны? А хуй там, нужны. Но не всегда же мы выбираем ruby / python / java / php. Хочу заметить, что другие языки это почти всегда enterprise решения, поскольку делать все описанное для прототипирования или небольших проектов это пиздец и не нужно