Почему изменения в новом Phoenix 1.3 так важны

в 7:10, , рубрики: Elixir, Elixir/Phoenix, phoenix, ruby on rails, функциональное программирование

Почему изменения в новом Phoenix 1.3 так важны - 1

Phoenix Framework всегда был классным. Но он никогда не был таким классным, как с новым релизом 1.3 (который сейчас находится в стадии RC2).

Произошло много значительных изменений. Крис МакКорд написал полный путеводитель по изменениям. Так же доступна его речь с LonestarElixir, где он подробно рассказывает про ключевые моменты. Вдохновленный его трудами, в своей статье я постараюсь рассказать вам про самые важные изменения в проекте Phoenix.

Давайте начнем!

Перевод выполнен самим автором оригинальной статьи Никитой Соболевым.

Существующие проблемы

Phoenix – новый фреймворк. И, естественно, у него есть некоторые проблемы. Основная команда работала очень старательно, чтобы решить некоторые из самых важных. Итак, каковы эти проблемы?

Директория web — чистая магия

При работе над проектом с использованием Phoenix у вас есть два места для исходного кода: lib/ и web/. Концепция такова:

  • Поместите всю свою бизнес-логику и утилиты внутрь lib/.
  • Поместите всё, что связано с вашим веб-интерфейсом (контроллеры, представления, шаблоны) внутрь веб-каталога web/.

Но понятно ли это разработчикам? Я так не думаю.

Откуда появился этот веб-каталог? Это особенность Phoenix? Или другие фреймворки тоже используют его? Должен ли я использовать lib/ с Phoenix-проектами или он зарезервирован для некоторой глубинной магии? Все эти вопросы появились у меня после моей первой встречи с Phoenix.

До версии 1.2 только директория web/ автоматически перезагружалась. Итак, зачем мне создавать какие-либо файлы внутри lib/ и перезапускать сервер, когда я могу поместить их где-то внутри web/ для быстрой перезагрузки?

Это приводит нас к еще более важным вопросам: относятся ли мои файлы-модели (назовем их моделями в этом конкретном контексте) к web-части приложения или к основной логике? Можно ли разделить логику на разные домены или приложения (например, как в Django)?

Эти вопросы остаются без ответа.

Бизнес-логика в контроллерах

Более того, код шаблона, который идет в Phoenix, предполагает другой способ. Можно получить следующий код в новом проекте:

defmodule Example.UserController do
  use Example.Web, :controller

  # ...

  def update(conn, %{"id" => id, "user" => user_params}) do
    user = Repo.get!(User, id)
    changeset = User.changeset(user, user_params)

    case Repo.update(changeset) do
      {:ok, user} ->
        render(conn, Example.UserView, "show.json", user: user)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(Example.ChangesetView, "error.json", changeset: changeset)
    end
  end
end

Что должен делать разработчик, когда пользователю после успешного обновления должно быть отправлено электронное письмо? Контроллер так и просится, чтобы его расширили. Просто поставьте еще одну строку кода перед render/4, что может пойти не так? Но. Только что Phoenix сам подтолкнул нас к неправильному использованию своей кодовой базы: мы пишем бизнес логику в контроллере!

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

Схемы не являются моделями

В какой-то момент без особых причин схемы Ecto стали называться «моделями». В чем разница между «моделью» и «схемой»? Схема — это всего лишь способ определить структуру — структуру базы данных в данном конкретном случае. Модели как концепция намного сложнее схем. Модели должны обеспечивать способ управления данными и выполнять различные действия, как модели в Django или Rails. Elixir как функциональный язык не подходит для концепции «модели», поэтому они были упразднены в проекте Ecto.

Файлы внутри models/ не были организованы. По мере своего роста ваше приложение становится хаотичным. Как эти файлы связаны между собой? В каком контексте мы используем их? Это было трудно понять.

Кроме того, директория models/ рассматривалась как еще одно место для размещения вашей бизнес-логики, что нормально для других языков и фреймворков. Существует уже знакомая концепция «fat models». Но такая концепция, опять же, не подходит для Phoenix по уже названным причинам.

Решения

С момента последнего крупного релиза многое изменилось. Самый простой способ показать все изменения — на примере.

Требования

В этом руководстве предполагается, что у вас есть elixir-1.4, и он работает. Нет? Значит, установите его!

Установка

Для начала вам нужно будет установить новую версию Phoenix:

mix archive.install
https://github.com/phoenixframework/archives/raw/master/phx_new.ez

Создание нового проекта

По завершению установки надо проверить, всё ли на месте. mix help вернет вам что-то вроде этого:

mix phoenix.new       # Creates a new Phoenix v1.1.4 application
mix phx.new           # Creates a new Phoenix v1.3.0-rc.1 application using the experimental generators

Вот тут и проявляется первое изменение: новые генераторы. Старые генераторы назывались phoenix, а новые — просто phx. Теперь нужно меньше печатать. И, что более важно, новое сообщение разработчикам: эти генераторы новые, они будут делать что-то новое для вашего проекта.

Затем нужно создать структуру нового проекта, запустив:

mix phx.new medium_phx_example --no-html --no-brunch

Прежде чем мы увидим какие-либо результаты этой команды, давайте обсудим параметры. --no-html удаляет некоторые компоненты для работы с html, поэтому phx.gen.html больше не будет работать. Но мы строим json API, и нам не нужен html. Аналогично --no-brunch означает: не создавайте brunch-файл для работы со статикой.

Изменения

Веб-директория

Глядя на ваши новые файлы, вы можете задаться вопросом: где находится веб-директория? Ну, вот и второе изменение. И довольно большое. Теперь ваша веб-директория находится внутри lib/. Она была особенной, многие люди неправильно поняли его главную цель, которая состояла в содержании веб-интерфейса для вашего приложения. Это не место для вашей бизнес-логики. Теперь все ясно. Поместите всё внутрь lib/. И оставьте только свои контроллеры, шаблоны и представления внутри новой web-директории. Вот как это выглядит:

lib
└── medium_phx_example
    ├── application.ex
    ├── repo.ex
    └── web
        ├── channels
        │   └── user_socket.ex
        ├── controllers
        ├── endpoint.ex
        ├── gettext.ex
        ├── router.ex
        ├── views
        │   ├── error_helpers.ex
        │   └── error_view.ex
        └── web.ex

Где medium_phx_example — имя текущего приложения. Приложений может быть много. Итак, теперь весь код живет в одной и той же директории.

Третье изменение откроется вскоре после просмотра файла web.ex:

defmodule MediumPhxExample.Web do  
  def controller do
    quote do
      use Phoenix.Controller, namespace: MediumPhxExample.Web
      import Plug.Conn
      # Before 1.3 it was just:
      # import MediumPhxExample.Router.Helpers
      import MediumPhxExample.Web.Router.Helpers
      import MediumPhxExample.Web.Gettext
    end
  end

  # Some extra code:
  # ...

end

Phoenix теперь создает пространство имен .Web, которое очень хорошо сочетается с новой файловой структурой.

Создание схемы

Это четвертое и моё любимое изменение. Раньше у нас была директория web/models/, которая использовалась для хранения схем. Теперь концепция моделей полностью мертва. Внедрена новая философия:

  1. схема представляет структуру данных;
  2. контекст используется для хранения нескольких схем;
  3. контекст используется для предоставления публичного внешнего API. Другими словами, он определяет, что можно сделать с вашими данными.

Наше приложение будет содержать только один контекст: Audio. Начнем с создания Audio контекста с двумя схемами Album и Song:

mix phx.gen.json Audio Album albums name:string release:utc_datetime
mix phx.gen.json Audio Song songs album_id:references:audio_albums name:string duration:integer

Синтаксис этого генератора также изменился. Теперь требуется, чтобы имя контекста было первым аргументом. Также обратите внимание на audio_albums, схемы теперь содержат префикс с именем контекста. И вот что происходит со структурой проекта после запуска двух генераторов:

lib
└── medium_phx_example
    ├── application.ex
    ├── audio
    │   ├── album.ex
    │   ├── audio.ex
    │   └── song.ex
    ├── repo.ex
    └── web
        ├── channels
        │   └── user_socket.ex
        ├── controllers
        │   ├── album_controller.ex
        │   ├── fallback_controller.ex
        │   └── song_controller.ex
        ├── endpoint.ex
        ├── gettext.ex
        ├── router.ex
        ├── views
        │   ├── album_view.ex
        │   ├── changeset_view.ex
        │   ├── error_helpers.ex
        │   ├── error_view.ex
        │   └── song_view.ex
        └── web.ex

Каковы основные изменения в структурах по сравнению с предыдущей версией?

  1. Теперь схемы не принадлежат web/, а директория models/ вообще исчезла.
  2. Схемы теперь разделены контекстом, который определяет, как они связаны друг с другом.

И схемы прямо сейчас являются не более чем описанием таблицы. Чем и должна быть схема в первую очередь. Вот как выглядят наши схемы:

defmodule MediumPhxExample.Audio.Album do
  use Ecto.Schema

  schema "audio_albums" do
    field :name, :string
    field :release, :utc_datetime

    timestamps()
  end
end

defmodule MediumPhxExample.Audio.Song do
  use Ecto.Schema

  schema "audio_songs" do
    field :duration, :integer
    field :name, :string
    field :album_id, :id

    timestamps()
  end
end

Всё за исключением самой схемы исчезло. Нет обязательных полей, никаких функций changeset/2 или каких-либо других. Генератор теперь даже не создает belongs_to для вас. Вы сами управляете связями ваших схем.

Итак, теперь это довольно ясно: схема — не место для вашей бизнес-логики. Всё это обрабатывается контекстом, который выглядит следующим образом:

defmodule MediumPhxExample.Audio do
  @moduledoc """
  The boundary for the Audio system.
  """

  import Ecto.{Query, Changeset}, warn: false
  alias MediumPhxExample.Repo

  alias MediumPhxExample.Audio.Album

  def list_albums do
    Repo.all(Album)
  end

  def get_album!(id), do: Repo.get!(Album, id)

  def create_album(attrs \ %{}) do
    %Album{}
    |> album_changeset(attrs)
    |> Repo.insert()
  end

  # ...

  defp album_changeset(%Album{} = album, attrs) do
    album
    |> cast(attrs, [:name, :release])
    |> validate_required([:name, :release])
  end

  alias MediumPhxExample.Audio.Song

  def list_songs do
    Repo.all(Song)
  end

  def get_song!(id), do: Repo.get!(Song, id)

  def create_song(attrs \ %{}) do
    %Song{}
    |> song_changeset(attrs)
    |> Repo.insert()
  end

  # ...

  defp song_changeset(%Song{} = song, attrs) do
    song
    |> cast(attrs, [:name, :duration])
    |> validate_required([:name, :duration])
  end
end

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

Использование контроллера

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

defmodule MediumPhxExample.Web.AlbumController do
  use MediumPhxExample.Web, :controller

  alias MediumPhxExample.Audio
  alias MediumPhxExample.Audio.Album

  action_fallback MediumPhxExample.Web.FallbackController

  # ...

  def update(conn, %{"id" => id, "album" => album_params}) do
    album = Audio.get_album!(id)

    with {:ok, %Album{} = album} <- Audio.update_album(album, album_params) do
      render(conn, "show.json", album: album)
    end
  end

  # ...

end

В действии update/2 теперь есть только три осмысленные строчки кода.

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

Контроллеры даже не обрабатывают ошибки. Для работы с ошибками предназначен специальный новый fallback_controller. Эта новая концепция — шестое изменение. Оно позволяет иметь все обработчики ошибок и коды ошибок в одном месте:

defmodule MediumPhxExample.Web.FallbackController do
  @moduledoc """
  Translates controller action results into valid `Plug.Conn` responses.
  See `Phoenix.Controller.action_fallback/1` for more details.
  """
  use MediumPhxExample.Web, :controller

  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> render(MediumPhxExample.Web.ChangesetView, "error.json", changeset: changeset)
  end

  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> render(MediumPhxExample.Web.ErrorView, :"404")
  end
end

Что происходит, когда результат из Audio.update_album(album, album_params) не соответствует {:ok, %Album{} = album}? В этой ситуации вызывается контроллер, определенный в action_fallback. И будет выбран правильный call/2, что в свою очередь возвращает правильный ответ. Легко и приятно. Никаких обработок исключений в контроллере.

Заключение

Внесенные изменения весьма интересны. Их много, они все сфокусированы на том, чтобы загубить старые привычки программистов, которые пришли из других языков программирования. И новые изменения стараются пополнить философию Phoenix-Way новыми практиками. Надеюсь, эта статья была полезна и побудила вас использовать Phoenix Framework по максимуму. Заходите ко мне на GitHub.

Благодарим Никиту за подготовку перевода своей собственной оригинальной статьи и с радостью публикуем материал на Хабре. Никита представляет сообщество ElixirLangMoscow, которое организует митапы по Эликсиру в Москве, а также является активным контрибьютером в опенсорс и вносит значительный вклад в наше сообщество Вунш. На сайте вас ждут 3 десятка тематических статей, еженедельная рассылка и новости из мира Эликсира. А для вопросов у нас есть чат в Телеграме с отличными участниками.

Автор: jarosluv

Источник

Поделиться

* - обязательные к заполнению поля