Хабрахабр

Различия Phoenix и Rails глазами новообращённого

Что больше всего бросилось в глаза заядлому рубисту, когда он только только начал изучать Elixir с Phoenix-ом.

Примечание

Посему, будут различия рабоче-крестьянского уровня, а про разницу на уровне запуска приложения, про принципы работы виртуальной машины Erlang'а и протокол OTP ничего сказано не будет. Я человек простой и глубоко лезть не буду.

Главное впечатление

Как некоторые английские фразы: по отдельности слова знакомые, а вместе — непонятно. Elixir/Phoenix очень похож на Rails и одновременно совсем не похож на него.

Erlang vs Ruby

Регулярно заходишь в тупики, ибо то что ты хочешь делается совсем не так, как привык делать… либо, на самом деле, ты этого вообще не этого хочешь. Думать на руби и пытаться писать на эликсире — это тяжело.

Для меня главные засады были с заменой рельсовых "паровозов" на пайпы, с переориентированием мышления на функциональщину (благо был старый опыт Haskell'я и общая любовь к inject/foldr) и с, субъективно, более строгими требованиями к типам данных (хотя, официально, оба языка со строгой динамической типизацией). А в остальном, про различия Erlang и Ruby люди книги пишут, поэтому буду краток.

Просто интересный инструмент. Паттерн-матчинг никакого удивления не вызвал и я так и не понял, почему про него столько разговоров.

Общий скоуп

Никакого глобального скоупа. В Эликсире всё лежит в модулях. Навевает C#.

Эликсир — наоборот, всё по модулям. Иными словами: рельса плоская донéльзя и местами мешает сделать иерархию (помню были когда-то баги с контроллерами, лежащими в модулях). В рельсе назначение объекта угадываешь по родительскому классу, а в эликсире — по полному названию класса/модуля.

Компилируемость

Так как можно найти добрую половину ошибок прямо при компиляции, а не в рантайме на продакшене. С одной стороны — это то, чего мне иногда не хватало в рельсе. Но с третей стороны, его нужно немного, а больших проектов на эликсире я пока не видел (да и не по заветам эрланга писать большие монолиты). С другой стороны, на компиляцию нужно время. И пока что, скорость работы вкупе с отсутствием богомерзких zeus/spring мне греет душу. В довершении, ребята из эликсира отлично поработали над динамической перезагрузкой кода и страницы.

Где-то в районе продакшн окружения и деплоя. Конечно же это и минусы порождает, но они вылезают сильно позже. Об этом будет ниже.

Поначалу — дико непривычно. Тут же интересный момент, который физически не может случиться в рельсе: миграции и прочие штуки, которые в rails через rake, в elixir требуют компиляции проекта и может случиться что-нибудь вроде: забыл написать роуты, на них ссылается path-хелпер во вьюхе, а отвалились миграции.

Документация

Но вот объём документации и примеры — это то, в чём ruby/rails далеко впереди. Сайт с документацией эликсира выглядит гораздо бодрее рубидока и апидока. Да и описание некоторых методов, по сути, не ушло дальше сигнатуры. В Elixir сильно не хватает примеров на всё, что чуть сложнее табуретки. Иной раз приходилось долго тыкаться и экспериментировать, чтобы понять как пользоваться тем или иным методом, ибо язык знаю не так хорошо, чтобы свободно читать исходники пакетов. Мне, как приученному рубями к обилию примеров и описаний, было сложно с некоторыми методами эликсира.

Независимость расположения файла от его содержимого

С одной стороны можно натворить вакханалию и разложить объекты так, что враг точно не пройдёт. Как говорится "with great power comes great responsibility". В частности, можно вспомнить trailblazer и ему подобных с идеей объединения всего, что связано с экшеном, в одном месте. А с другой стороны можно именовать пути более логично и наглядно, добавляя логические уровни директорий, которых нет в иерархии классов. В эликсире это можно сделать без сторонних библиотек и кучи классов просто правильно переложив существующие файлы.

Прозрачный путь запроса

То в эликсире такого желания не возникает совсем (хотя может я ещё молод и всё впереди). Если в Rails вопрос про rack — это непременный атрибут любого собеседования, ибо рельса — это верхушка айсберга и периодически хочется сделать свой middleware. И там явно видно где фетчится сессия, где обрабатывается flash-messge, где csrf валидируется и всем этим можно управлять как вздумается в одном месте. Там есть явный набор pipeline, через который проходит запрос. В рельсе всё это хозяйство частично прибито гвоздями, а частично разбросано по разным местам.

Роуты наизнанку

Там даже (.:format) заложен прямо в роуты. В Rails, ситуация когда один экшн может отвечать в нескольких форматах — это норма. Разные форматы идут по разным pipeline и имеют разные url'ы. В эликсире, из-за вышеозначенного свойства с pipeline, мысли об аналоге format вообще не появляется. По мне так это здо́рово.

Схема в модели

Как опишешь поля модели, так и будет. Это вообще сказка. Плюс нет костылей, чтобы запретить доступ к полю, которе есть в БД, но его по каким-либо причинам нельзя использовать в веб-приложении. Никакого неявного каста типов.

Валидации и колбэки

Там всё более прямолинейно. В эликсире нет колбэков. И, кажется, мне это нравится.

А остатки колбэков идут через Multi, который даёт возможность набрать кучу операций, транзакционно их выполнить и обработать результат. Вместо rails-way в эликсире changeset, который совмещает в себе strong_parameters, валидации и немного колбэков.

Сначала это непривычно. Короче, всё просто по-другому. А потом начинаешь замечать "необъянимую прелесть", ибо приходится делать правильно, а не как привык. Потом местами дико бесит, ибо нельзя просто ещё один колбэк для всего воткнуть и не думать о разных бизнес-кейсах.

Работа с БД

Repo, Ecto. Вместо ActiveRecord появился некие Ecto. Рассказывать все отличия — это отдельная статья получится. Query и ещё несколько их собратьев. Поэтому скажу основные субъективные ощущения.

Так как там общий скоуп, константы из load path подгружаются при обращении к ним и можно просто открыть rails c, написать User.where(email: 'Kane@nod.tb').order(:id).first и получить результат. В дебаге удобнее AR.

Нужно сделать ряд действий: В Elixir'е консоли недостаточно.

  • заимпортить метод для построения sql-запроса: import Ecto.Query, only: [from: 2];
  • заалиасить классы, чтобы не писать через точку их полные названия:
    • alias MyLongApplicationName.User — чтобы вместо MyLongApplicationName.User писать просто User;
    • alias MyLongApplicationName.Repo — аналогично для обращения к классу, умеющему выполнять sql и отдавать результаты;
  • и только теперь можно написать from(u in User, where: u.email == "Kane@nod.tb") |> Repo.one

То есть ты сам выбираешь какие методы, модели и другие объекты, нужны для работы, явно их подгружаешь и пользуешься. С другой стороны, в коде приложения эти "формальности" дают более читаемый код, плюс есть ощущение, что ты контролируешь происходящее, а не оно живёт своей жизнью.

Название приложения

Поэтому на длину названия внимания не обратил. По образу и подобию Rails я полагал, что имя приложения используется в паре конфигов и всё. В Elixir модуль с названием приложения — это верхний уровень в иерархии модулей веб-приложение и он будет фигурировать везде. А зря.

И теперь немного страдаю, так как это довольно длинное название и писать его нужно постоянно. Я вот назвал свою песочницу Comindivion. Кстати да, кому интересно, вот песочница на GitHub. Как в файлах классов, так и в консоли при вызове чего угодно.

N+1

Там на этапе сборки запроса можно указать какие реляции понадобятся и они будут загружены в ходе выполнения этого самого запроса. В Rails мы её имеем "из коробки", а в Elixir "из коробки" такой проблемы нет. Не будет у тебя доступа к этой реляции. Не загрузил? Всё просто и красиво.

Обработка запроса и ответ на него

Если коротко: в фениксе всё более явно, нежели в рельсе.

Везде conn

Напоминает request из ActionController, только более всеобъемлющий. Так как состояние не хранится в куче разных объектов, его приходится таскать за собой в одном объекте. Содержит вообще всё: и request, и flash, и session и всё всё всё. Зовётся он в Фениксе connection. Он же фигурирует в вызове всего, что связано с обработкой пришедшего запроса.

Рельса в этом плане развращает. Тут и минусы, так как попервой очень лениво лепить везде conn и не до конца понимать зачем. А в Phoenix conn постонно напоминает о работе с конкретным соединением или сокетом, а не просто методы вызываются и там внутри магия происходит. Ты пишешь render или flash и нет мыслей о том, что это действие с соединением.

Partial&template

В конечном итоге всё функция. В Фениксе нет разделения на partial и template. А в Elixir всё функция, и вьюшки в том числе. Тут же кроется ещё одна прелесть: рельса даже в прод окружении постоянно лезет за вьюшками на диск и порождает IO плюс оверхед на их преобразование из erb/haml/etc в html. Скомпилили вьюшку разок и всё: получает аргументы, выплёвывает html, на диск не ходит.

Views

В частности, там лежат "переопределения" render'а. В Rails под view понимают партиал и темплейты, а в Phoenix они лежат в templates, а во views, грубо говоря, обитают разные способы представления данных.

Всё вызывается явно. То есть, по умолчанию, контроллер ничего не рендерит. А если у вас нет партиала и он вам особо не нужен (например в случае с json, когда он легко билдится сервисным классом), вы переопределяете рендер как-нибудь так:

def render("show.json", %) do %{ groups: groups }
end

И партиал больше не нужен.

Heplers

И это круто! В Phoenix их нет. Ибо в рельсовых хелперах, обычно, собирается всякий хлам, который либо лениво было распихивать по углам, либо просто нужно было быстренько чего-нибудь накодить.

добавлять можно. Однако, методы в контроллер, представления и тд. Делается это в специальном месте web/web.ex и выглядит довольно прилично.

Статика

Это когда поменял css, вернулся в браузер, а там изменения уже сами подгрузились. В девелопменте всё как обычно, разве что в фениксе ещё прикрутили live reload, попервой вызывающий "Уау!" эффект.

По-умолчанию, явно прописаны места, откуда можно тащить статику и нельзя просто так добавить файликов в ассеты, чтобы раздавать их. В продакшене в Phoenix поведение статики немного иное, нежели у рельсы. Ещё есть маппинг дефолтных ассетов, чтобы лишний раз по ФС не блужлать, а сразу брать нужный файл и отдавать его.

Ассеты

Можно заменить на webpack. Из коробки в Фениксе — brunch. Но есть довольно правдивая шутка про то, что многие проекты загибаются на этапе настройки webpack'а.

Её либо копипастирь руками прямо в проект из node_modules (мне этот вариант совсем не нравится), либо писать хуки на баше. Короче, js и css более-менее собираются, а вот с остальной статикой в бранче не очень. Например, так.

Работа с SSL

С виду напоминает рубийную пуму. "Из коробки" в Фениксе идёт маленький http-сервер, называемый cowboy. Но как-то мне не зашла настройка SSL ни в одном из вышеозначенных. У них даже количество звёздочек на GitHub примерно одинаковое. Так что как http-сервер — ок, а для ssl беру прокси на localhost через apache/nginx. Особенно вместе с Let's Encrypt, доп.файлом конфига веб-сервера и регулярным обновлением сертификата.

Деплой

В Rails, в минимальном варианте, склонил репку на сервер, поплясал с бубном для бандла, конфигов, ассетов и запустил приложение. Он вообще другой, по сравнению с рельсой. Нужно собирать пакет. А эликсир же компилится и закопать трамвай склонить репку не прокатит. И тут начинается:

  • узнаёшь зачем нужен applications в mix.exs, ибо без правильно их указания в проде чудесные ошибки;
  • узнаёшь, что переменные окружения вкомпиливаются на моменте сборки пакета, а не на момент его запуска и это в первые разы вызывает дикое удивление; потом узнаёшь про relx вместе с RELX_REPLACE_OS_VARS=true и немного отпускает;
  • удивляешься, что в собранном пакете для продакшена нет ничего похожего на rake, в частности нет миграций и их нужно как-то отдельно запускать, например, из дев.окружения через проброс порта к БД (или через eDeliver, который сделает примерно то же самое).

А потом, как с вышеописанным разберёшься, начинаются плюсы:

  • можно сделать пакет самодостаточным и на боевой машине вообще ничего не ставить из зависимостей; просто tarball распаковать и запустить содержимое; разве что erlang раскатать может понадобиться, так как его cross compile вариант немного нетривиальнен в сборке;
  • можно делать upgrade release, чтобы деплоить без downtime.

Дебаг

Даже есть аналог rails c, выглядящий как iex -S mix. В Elixir есть Pry и работает аналогично рубям.

Приходится подключаться к работающему процессу. Но в продакшене консолью пользоваться приходится иначе, так как пакет собран и mix в нём нет. В итоге понимаешь что делать всё нужно иначе и вызываешь что-то вроде: iex --name trace@127. Это радикально отличается от рельсы и в начале тратишь много времени на гуглинг способа запуска эликсир-консоли в продакшене, ибо ищешь что-то аналогичное рельсе. 0. 0. 0. 1 --cookie 'from_env' --remsh 'my_app_name@127. 1'. 0.

Продолжение следует...

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть