Open24

Phoenix プラクティスを探してコードリーディング

tatotato

フォルダ構造

印象に残ったファイルとやっていることの抜粋。

tatotato

livebook_web/helpers/ansi.ex

  def ansi_string_to_html(string) do
  def ansi_string_to_html_lines(string) do
tatotato

livebook_web/helpers/codec.ex

  def pcm_as_wav_size(pcm_size) do
  def encode_pcm_as_wav_stream!(path, file_size, num_channels, sampling_rate, offset, length) do
  def encode_annotated_binary!(meta, binary) do
tatotato

livebook_web/helpers.ex

ANSIの処理などを分けているのは明確なコンテキストがあるからで、helpers.exにもいくつか処理があった。

  defdelegate ansi_string_to_html(string), to: LivebookWeb.Helpers.ANSI
  def platform_from_user_agent(user_agent) when is_binary(user_agent) do
  def live_dashboard_process_path(pid) do
  def names_to_html_ids(names) do
  def pluralize(1, singular, _plural), do: "1 #{singular}"
  def format_datetime_relatively(date) do
  def upload_error_messages(upload) do
  def upload_error_to_string(:too_large), do: "Too large"

platform_from_user_agentはsession_liveでしか使っていないが、helpers.exにあるのは興味深い。privateな関数ではダメだったのだろうか。

Helpers 自体は、最初から共通のhtml_helpersに突っ込んでいた。

      # Custom helpers
      import LivebookWeb.Helpers

コンテキストに依存しない IN:一般的なデータ, OUT: 一般的なデータ、であれば、helpers.exだろうか。で、その中でも大きいものや、そのうち増えてコンテキストが固まってきたもの(例えばエラー生成とか)があれば、helpers/something.ex に落として、importなり、defdelegateなりを行う。

tatotato

livebook_web/hooks/

app_auth_hook.ex
auth_hook.ex
sidebar_hook.ex
...

いずれも router.ex での on_mount なモジュールの様子。beacon_live_adminにもあったし、hooksが一般的なのかもしれない。

フォルダ構造とは別に、

  • sidebar_hookでは、attach_hookでイベントハンドラ(handle_event)を差し込んでいた。ここではPubSubのHookをしているが、それ用のHTMLもないのでLiveComponentなしで、イベントハンドルしたかったのだろうか。ユーザー情報変更をbroad_castで受ける処理をまとめている、など。
    • 話がそれて、attach_hook使うとimportじゃなくてもaliasするようなモジュール内で共通イベントの処理ができそう。
  • 途中で、LivebookWeb.ConfirmというComponentを見つけたが、ここに on_mount が定義されていた。なんとなくわちゃわちゃ感を覚えたが、そのcomponentに付属するhookだからありなんだろうか。LiveComponentにせずに、importもせずに、独立して関心のイベントハンドラを扱う(定義する)ためのテクニックという感じに見えた。
tatotato

livebookは、ほかにも livebook_web/*_helpers.exがある。

なるべくLiveViewやLiveComponentをコンテナコンポーネントにしておいて、プレゼンテーショナルなコンテキストに依存するinを取るものを集めている感じには見える.... ? :thinking 違うかもしれない? 微妙に線引きが不明。

名前は *_components.ex じゃないんだ。 *_component.ex (LiveComponent用)とややこしいからだろうか。

tatotato

livebook_web/output/

output用途なLiveComponentが複数あるディレクトリ。output_liveじゃなくてoutputか。確かに、中にLiveViewなファイルがないから、この方がわかりやすい気がする。

tatotato

live/components/以下に置いているもの。

livebookはほぼない。既存のlayoutsと唯一Confirmというコンポーネントがある。
(livebook自体は昔からあるので設計思想的に反映されていない可能性はある)

todo_trekはTimelineというコンポーネントがある。
これはHomeLiveでしか使っていないので、そっちでも良さそうだが切り離してある。

Presentationalなコンポーネントを、live/components/に集めるべきなんだろうか。そうするとおそらくlive/hoge_live/index.exlive/components/hoge.exがセット的に発生しそうではある。いや、Contextsは文字通りコンテキストであり、Componentsは文字通りコンポーネントであるので(特にAtomic的な意味合いで)混ぜちゃいけないか。live/components内のコンポーネントなモジュールの命名や関数名にコンテキストが入り込むときは、たぶん迷子になっている。

(最初からある CoreComponentはサンプルとしてはいいけど、まとめすぎていてミスリードでは...)

tatotato

LiveComponentをどこにおく?

共通使用しないもの(画面構成要素で独自に状態を持つ系)

=> live/<foo>_live/*_component.ex でよさそう(rootなLiveViewと同じフォルダ)。

共通使用するもの

=> live/<foo>/*_component.ex でよさそう。このフォルダには、root LiveViewなファイルがない想定。

PresentationalなComponent に関わるものをどこにおくか?

難しい。Livebookプロジェクトも統一感はなかったかもしれない。session_live.exにあるもの(button_item)とsession_helpers.exにあるもの(cell_icon)の線引きが不明。

  • 共通利用になっていないものは、個人的には構造的に閉じていた方が良いと思う。それはデータ依存かどうかは関係なく、コンポーネントとして切り出せてもそこにあった方が嬉しい。そうしないと(なんでも移すと)components/以下の命名が破綻して「どこに何が?」の迷子になる(ワンクッション必要と考える)。
    • データに直接依存するものは、LiveView(LiveComponent)モジュールでそのまま扱う。
    • データに直接依存しない形に無理なくできた(る)ものは、コンポーネントとしては成立しているので、attrを必ず使う形で、LiveView(LiveComponent)モジュールでそのまま扱う。さすがに多すぎると判断するならば、ファイルを分ける(live/<foo>_live/<foo>_parts.ex)。
  • 共通利用になっている(なった)ものは、liveではなくcomponentsでフォルダ側に移すことになると思われる。
    • 特定のコンテキストがあるもの ~ components/organisms/<foo>.ex
      • (templates相当はLiveViewのrenderで扱われる)
    • あるいは、components/<foo>.ex + components/<foo>/<organisms_name>.html.heexにしてembed_templatesを使う。
      • こっちかな。明らかに巨大でイベントを扱うものも1ファイル(e.g. modal.ex, form.ex, conform.ex)にできる。

atomsとmoleculusなコンポーネント

  • components/atoms.excomponents/moleculus.exに入れて、デフォルトでimportしてしまう。
    • core_component.exは消す。coreというのは意味が通らない。modalならmodal.exを作ること。
tatotato

app/ にかくか、app_web/ に記述するか

tatotato

livebookをみるとPubSubの処理が lib/livebook/users.exとかにある。lib/livebook_web/以下っぽい感覚だけどそうでもないのだろうか。必ずしも...だからPubSubは別にwebじゃない、という感覚だろうか。

tatotato

iexからそれを行うケースを思いつくかどうか、とか。

tatotato

Livebookは、sessionというコンテキストまで lib/livebook/ 以下にある。

tatotato

例えば、ライブラリパッケージを提供するとき、各機能はwebとか関係なしにlib/package_name/に定義するのだから、lib/app_webの方が例外的とみるほうがいいのだろうか。webに関するものではなく、ただのwebのUI置き場くらいの感じだろうか。

別デザインのwebが生えた(lib/app_web_2)として仮定して、どちらでも使うものは置かない、くらいのイメージとか。いや、このイメージだと逆にwebに膨れ上がる気もする。

なので、*_webはクライアントサイド相当のコード、それ以外はコンテキストとかの心持ちだろうか。

tatotato

todo_trekは、current_userをもつScopeなるモジュールもコンテキストにあった。

defmodule TodoTrekWeb.Scope do
  def on_mount(:default, _params, _session, socket) do
    current_user = socket.assigns[:current_user]
    {:cont, Phoenix.Component.assign(socket, :scope, TodoTrek.Scope.for_user(current_user))}
  end
end

for_userという名前は関数っぽさはないな、と思ったらstructだった。トップレベルにstructを置いているのか。

tatotato

コンテキスト内の共通処理

tatotato

データベースモデル

本当は、Ectoのスキーマはコンテキストごとに定義があってもいい気もする。
コンテキストで検証対象が違うこともあるだろうし、createはなくupdateだけのコンテキストもあり得る。

ただ、それらの共通処理をどこに書くべきか?と考えると、わざわざ別に作ってimportなりuseなりするよりも1つにしたくなるし、やむを得ないのかなという気持ちになる。ということで、モデルs/モデル.exができることが多い。

いっそのこと、ほかのコンテキストからは明確に分ける観点で、基本的なCRUD(と基本的なQueryチェイン系)以外の記述のみをlib/app/models/モデルs/モデル.ex に詰め込んでもいいのでは、と思ったりもする。modelsはモジュールにはしない、ただの箱。専用で必要であればそれはそれで用意すればいいわけなので。

「ほかのコンテキスト」は複数形を使わない、くらいのルールがあるといいかもしれない。

=> ただ実はそんなに大変でもない気もしてきたので、一度はっきりと各スキーマを混戦しないでコンテキストの下に置く思考で作ってみるのがいいかもしれない。つまり冒頭の「本当は、Ectoのスキーマはコンテキストごとに定義があってもいい気もする」に戻る。

ただし、確実にコンテキストが依存している場合はスキーマ関連において参照するのはあり、くらいだろうか。マスタと紐づくデータみたいなもの。そうしないと各マスタのスキーマを、各コンテキストに置く必要がでてくるため。...スキーマはもう1コンテキストでいいのでは、、、(ループ)

tatotato

コンテキスト内部で生じる共通処理

  • 部品的なデータを扱うとき
  • 他サービスのAPIをたたくとき
  • ...

独立したコンテキストとして、トップレベルに作っておいても良いかもしれない。

tatotato

preloadやページング

画面都合で生じるロジックをどこで行うか
=> コンテキストを強く出すとおそらく_web/側ではなく、コンテキスト側になる。ただ、実際にはどちらもオプション的に扱いたい。