Чем Phoenix отличается от Rails

На первый взгляд, Phoenix по некоторым признакам схож с Rails. Это побуждает разработчиков начинать его осваивать и сразу же создавать свои проекты для того, чтобы в совершенстве им овладеть. Для достижения желаемого результата потребуется немного больше, чем простое знание общих принципов этих двух фреймворков. Однако Ruby-разработчики, приворожённые внешним сходством Phoenix и Rails, с удовольствием погружаются в новую среду и делают это довольно успешно. К сожалению, это приводит к большим заблуждениям о единообразии Phoenix и Rails, а также к упущению значительных различий их идеологий.

В сообществе Ruby принято считать, что есть Rails-разработчики, а есть Ruby-разработчики. С Phoenix такого раскола не предвидится. Хотя Phoenix, безусловно, имеет свой механизм абстракций, написание Phoenix-приложения — это написание приложения на Elixir. Тестирование Phoenix-кода — это тестирование Elixir-функций. В данной статье приводится обоснование этих принципов путём выявления сходств и различий фреймворков Phoenix и Rails.

Сходства

Большую часть поклонников Phoenix составляют Rails-разработчики, поэтому озвучим несколько основных особенностей, которые принёс с собой фреймворк Rails.

  • Нацеленность на высокие показатели скорости разработки как на клиентской, так и на серверной стороне.

  • Стандартная структура файлов и каталогов, хотя Phoenix попросту берёт за основу структуру Elixir-приложений.

  • MVC во главе с маршрутизатором (правда, Phoenix в своей архитектуре плавно сворачивает в сторону функциональной парадигмы).

  • Стандартный стек технологий с поддержкой реляционных баз данных (sqlite3 в Rails, PostgreSQL в Phoenix).

  • Наилучшие подходы к безопасности прямо из коробки.

  • Встроенные механизмы для тестирования приложений.

Различия

Несмотря на некоторые сходства, фреймворки Phoenix и Rails существенно различаются. Далеко не каждая среда выполнения готова предложить подход, который реализует Phoenix. Он проявляется в особенностях структуры приложения, способности к восстановлению после отказа, отладке системы или установлении связи с клиентом. Принципы OTP и характерные черты Elixir объединяются в Phoenix таким образом, что Phoenix-приложение является всего лишь частью более мощной инфраструктуры. Такое расхождение с Rails накладывает свой отпечаток на стек.

Приложения

Начнём с того, что понятия «Phoenix-приложение» не существует. Проекты Phoenix прежде всего Elixir-приложения, которые строятся на Phoenix для обеспечения дополнительной функциональности. Это означает, что существует только один путь для создания, запуска и развёртывания приложения. И это путь Elixir. Итак, почему же Phoenix выигрывает?

Факт № 1. Никаких синглтонов

В Rails всё реализуется в виде единственного приложения, получить доступ к которому можно с помощью Rails.application. Rails командует парадом: запускает приложение, создаёт конфигурации и даже выполняет задачи из коммандной строки. Данному подходу свойственно такое ограничение, как невозможность одновременного запуска двух Rails-приложений. Если же в этом существует необходимость, придётся каждое из них превратить в отдельный Engine и научиться с ними работать.

Всё глобальное чуждо для Phoenix. Никаких монолитов. Созданное Phoenix-приложение включает в себя одну точку входа (Endpoint), один маршрутизатор (Router) и один сервер PubSub. Но при желании этот список можно расширить. Так как глобального состояния или глобального сервера не существует, приложение можно разбивать на части по мере роста его инфраструктуры.

Факт № 2. Запуск и остановка

С точки зрения Elixir, проект представляет собой совокупность небольших компонуемых между собой приложений, которые запускаются и останавливаются как единое целое. Процесс запуска обычно протекает следующим образом (в качестве примера приведён Phoenix):

  1. В каждом приложении имеется спецификация, в которой могут быть указаны модули, вызываемые после инициализации приложения:

    def application do
    [mod: {Phoenix, []},
     applications: [:plug, :poison, :logger, :eex],
    ...]
    end
  2. В указанных модулях вызывается функция start/2:

    defmodule Phoenix do
      def start(_type, _args) do
        ...
        Phoenix.Supervisor.start_link
      end
    end
  3. Функция start/2 возвращает идентификатор контролируемого супервизором процесса (Phoenix.Supervisor.start_link в примере выше).

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

Сложный и требующий использования расширений процесс инициализации Rails-приложения послужит ярким контрастом единому последовательному набору действий в Phoenix. Для версии Rails 4.2.2:

$ rails c
Loading development environment (Rails 4.2.2)
irb(main):001:0> Rails.application.initializers.length
=> 74

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

Факт № 3. Мониторинг и интроспекция

Разделяя приложение на несколько более мелких, вы обеспечиваете управление, отказоустойчивость и интроспекцию запущенной системы. При помощи инструмента Observer можно без лишних усилий представить работу приложений по отдельности или в качестве единого целого:

Erlang Observer

Главное преимущество в том, что проект запускается как единое приложение, при этом его можно разбить на несколько более мелких приложений, что актуально как для запуска на одной ноде, так и для сервис-ориентируемой архитектуры. Среда выполнения не требует от вас никаких вложений. Она построена на проверенных и надёжных подходах. Собственно, наглядный пример этого можно увидеть в одной из глав книги Programming Phoenix.

Жизненный цикл запроса

Phoenix показывает отличную производительность «из коробки», что можно легко доказать, проведя бенчмарки. Жизненный цикл запроса в корне отличается от цикла, реализованного в Rails при помощи Rack.

Факт № 4. Читабельный код

Явное лучше неявного. Исключений из этого правила практически нет. Phoenix в большинстве своём поддерживает концепцию «явного». К примеру, в процессе написания приложения на Phoenix, можно просмотреть все плаги, через которые проходит запрос, в файле lib/my_app/endpoint.ex. Phoenix представляет все плаги в явном виде, в то время как Rais выделяет Rack middlewares в отдельную часть приложения. Таким образом, просмотрев плаги, можно очень быстро составить себе представление о жизненном цикле запроса.

defmodule MyApp.Endpoint do
  use Phoenix.Endpoint, otp_app: :my_app

  socket "/socket", MyApp.UserSocket
  plug Plug.Static, at: "/", from: :my_app, gzip: false, only: ~w(css images js)
  plug Plug.RequestId
  plug Plug.Logger
  plug Plug.Parsers, parsers: [:urlencoded, :multipart, :json], pass: ["*/*"]
  plug Plug.MethodOverride
  plug Plug.Head
  plug Plug.Session, store: :cookie
  plug MyApp.Router
end

Запрос берёт своё начало в точке входа, пробегает через «основной middleware», представленный в явном виде в форме плага, а затем передаётся в маршрутизатор, который сам по себе также является плагом. В маршрутизаторе запрос проходит ещё одну пачку плагов, после чего направляется в контроллер, который (бинго!) тоже представляет собой плаг. Единственная абстракция, обхватывающая все уровни стека, позволяет представить жизненный цикл запроса настолько явно, насколько это вообще возможно. Это также облегчает интеграцию с пакетами от сторонних разработчиков.

Проведём сравнение двух схожих контроллеров, чтобы увидеть, каким образом функциональный подход с плагами делает код Phoenix более читабельным:

# controller.rb

before_action :find_user

def show do
  @post = @user.posts.find(params[:id])
end

def find_user
  @user = User.find(params[:user_id])
end
#controller.ex

plug :find_user

def show(conn, %{"id" => id}) do
  post = conn.assigns.user |> assoc(:posts) |> Repo.get(id)
  render conn, "show.html", post: post
end

defp find_user(conn, _) do
  assign(conn, :user, Repo.get(User, conn.params["user_id"]))
end

Опытным Rails-разработчикам известно, что show неявно вызывает render "show.html". Даже если бы это делалось явно, немногие Rails-разработчики обращают внимание на то, что все объявленные в экземпляре контроллера переменные копируются в экземпляр представления, что только усложняет процесс программирования. Соглашения важнее конфигураций — это, конечно, хороший принцип, но есть некая грань, где неявная реализация сводит на нет ясность кода. Phoenix обеспечивает идеальное соотношение ясности кода и удобства использования API. Кроме того, программистам с объектно-ориентированным складом ума нельзя забывать о хэше параметров params, объектах request и других переменных, заданных неявно в фильтрах before_action. В Phoenix всё это задаётся явным образом. Структура conn выступает в качестве структуры данных и средства связи с сервером. Данные передаются через плаги — объединённые в цепочку функции, которые проводят некие трансформации над запросом и, если нужно, возвращают ответ.

Факт № 5. Лёгкое тестирование

Функциональное программирование и плаги позволяют проводить изолированное или интеграционное тестирование контроллеров простой передачей структуры conn в цепочку плагов и анализом полученных результатов. Кроме того, действия контроллера в Phoenix — всего лишь функции, не имеющие неявно заданных переменных. Необходимо изолированно протестировать контроллер? Просто вызываем функцию:

test "sends 404 when user is not found" do
  conn = MyController.show(conn(), %{"id" => "not-found"})
  assert conn.status == 404
end

Благодаря функциональному программированию, построение экземпляров контроллеров проходит без каких-либо трудностей. А если нужно провести интеграционное тестирование через точку входа, то фреймворку Phoenix достаточно лишь вызвать цепочку функций:

test "shows users" do
  conn = get conn(), "/users/123"
  assert %{id: "123"} = json_response(conn, :ok)
end

Представления в Phoenix устроены аналогичным образом: они состоят из функций, среди которых нет каких-либо скрытых данных.

Факт № 6. Удобно делиться кодом

Метод, написанный для Rails-контроллера не получится так просто перенести в Rack, потому как он зависит от множества внутренних переменных контроллера.

Так как плаги — это всего лишь функции, то нам известны их входные и выходные параметры. Для всего стека технологий HTTP, будь то точка входа, маршрутизатор или контроллер, существует единое абстрактное представление. Покажем на примере, как использовать плаг AdminAuthentication для всех запросов "/admin", а также для отдельного контроллера DashboardController. Плаг на уровнях абстракции маршрутизатора и контроллера будет одним и тем же:

defmodule MyApp.Router do
  pipeline :browser do
    plug :fetch_session
    ...
    plug :protect_from_forgery
  end

  pipeline :admin do
    plug AdminAuthentication
  end

  scope "/" do
    get "/dashboard", DashboardController
  end

  scope "/admin" do
    pipe_through [:browser, :admin] # plugged for all routes in this scope

    resources "/orders", OrderController
  end
end

defmodule MyApp.DashboardController do
  plug AdminAuthentication # plugged only on this controller

  def show(conn, _params) do
    render conn, "show.html"
  end
end

Так как плаги используются на всех уровнях стека, то можно подключить плаг AdminAuthentication в маршрутизатор и контроллер для осуществления детального контроля доступа. В Rails для этого можно наследоваться от AdminController, но тогда теряется прозрачность применяемых преобразований объекта запроса. Придётся прошерстить дерево наследования для того, чтобы выяснить нужные связи и отношения. В Phoenix цепочки функций маршрутизатора позаботятся о чётких и лаконичных запросах.

Каналы

Phoenix изначально создавался для укрощения современных, высоконагруженных, взаимодействующих в реальном времени веб-систем. Каналы предоставляют приложению независимые от протокола передачи данных соединения, работающие в режиме реального времени, которые могут обеспечить обслуживание нескольких миллионов клиентов на одном сервере. В Rails же функционал реального времени всегда отходил на второй план.

Phoenix vs Rails channels

Факт № 7. Веб не стоит на месте

Каналы Phoenix направлены на веб за пределами браузера. Уже сейчас веб включает в себя соединённые друг с другом устройства (телефоны, часы, умная электроника), а не ограничивается лишь браузером. Для этого нужен фреймворк, который смог бы развиваться, подстраиваясь под новые изменения и протоколы. Именно поэтому каналы являются независимыми от протокола передачи данных и имеют родные клиенты под платформы iOS, Android и Windows. Всё это можно увидеть в действии, запустив чат-приложение Phoenix в браузере, на iPhone и Apple Watch.

Факт № 8. Меньше зависимостей — больше производительности

Относительно недавняя разработка для внедрения функционала реального времени в Rails — Action Cable — привносит в приложение целый список зависимостей: Faye, Celluloid, EventMachine, Redis и другие. Так как Phoenix запускается на виртуальной машине Erlang, он обладает встроенным функционалом реального времени среды выполнения. Поддержка средой выполнения распределенных вычислений позволяет фреймворку Phoenix для использования PubSub обойтись без Redis или подобных зависимостей.

Именование

Phoenix не требует строгого соблюдения соглашения об именовании, как это делает Rails.

Факт № 9. Лёгкость в изучении

В Phoenix имена модулей не привязываются к имени файла. В Rails необходимо поместить контроллер UsersController в файл под названием users_controller.rb. Да, в таких соглашениях нет ничего плохого, но с Phoenix можно просто о них забыть. Мы отдаём свой голос за адекватные настройки по умолчанию, достаточно гибкие по отношению к индивидуальным требованиям. Присвоение имён вызывает большие трудности у тех, кто сначала изучил Rails, а потом попытался написать приложение на Ruby. В Rails для получения из каталога приложения всех файлов с именем, соответствующим соглашению о присвоении имён классу, используется метод const_missing. В связи с этим для разработчиков, желающих выйти за пределы Rails, процесс осуществления запроса файлов в обычном Ruby-приложении окутан тайной.

В Phoenix включена директория «web», куда помещаются контроллеры, представления и т. п., но существует она только ради перезагрузки кодовой базы, что воплощает в жизнь концепцию разработки через непрерывное обновление страницы.

Phoenix не различает единственного и множественного числа имён. Правила формирования имён в Rails ставят как начинающих, так и продвинутых разработчиков в тупик: модели называют только в единственном числе, контроллеры — во множественном, URL-хелперы стерпят и то, и другое, и так далее. В Phoenix, как и в Elixir, для имён используется только единственное число. Можно называть таблицы и пути маршрутов во множественном числе, но они задаются явно в рамках системы.

Ассеты

Для работы со статическими ассетами в Phoenix по умолчанию используется инструмент brunch, но также существует возможность подключить свой собственный JavaScript-сборщик вместо его написания специально под фреймворк (как это происходит в Rails в случае с его Asset Pipeline). Phoenix также более эффективно использует канальный уровень для предоставления возможности перезагрзуки изменённого кода прямо из коробки.

Факт № 10. Будущее за ES6/ES2015

Phoenix содействует развитию ES6/ES2015 вместо CoffeeScript, по умолчанию поддерживая в новых проектах ES2015. CoffeeScript уже выполнил свою главную цель по продвижению индустрии вперёд. ES2015 и его первоклассные транспиляторы — это следующий шаг.

Факт № 11. Разработка через непрерывное тестирование

Phoenix способен обеспечивать возможность непрерывной перезагрузки кода при внесении изменений. Файлы с расширениями .js или .css автоматически перезагружаются в браузере после внесения в них изменений. Однажды попробовав эту опцию, вы вряд ли сможете без неё обойтись.

Заключение

Независимо от того, каким языком вы владеете, вы ещё увидите, как Phoenix, основанный на лучших принципах своих предшественников, вместе с Elixir пробьют себе путь к пьедесталу современной веб-разработки.