ブラウザ上で完結するGit組み込みエディタ作っている

PWA-Editor(仮)

https://i.gyazo.com/e95abffcddcb0f21f4b004ae076e34ef.gif

デザインとかは適当なんだけど、コンセプト的にどこまで実装可能かの検証を一通り終えた。頑張れば本格的なものが作れそう、という手応えがある。 IndexedDB バックエンドに fs 動かして ismorphic-git を動かしている。 UIは全然足りないが、 ポテンシャル的には GitHub に push できることも検証済み。ServiceWorker でオフラインで動くようになっている。

デプロイ先は https://nervous-kilby-73c9b0.netlify.com/

開発中のものなので、予告なく互換が壊れることがある。

動機

Chromebook 買ったんだけど、やはり開発機として使うには厳しい気持ちがあった。主にまっとうなエディタがないのが辛い。cloud9 とか試したけど、辛かった。

フロントエンドのツール周りはJSで完結して PWA でオフライン化できるのは検証済みで、 isomorphic-git が使い物になる品質だったので、一旦は GitHub を実質的なバックエンドとして編集したファイルの push/pull/clone できるもの、というゴールを設定した。

うまいことやれば金になりそうな気がするので、まだソースの公開はしない。これを利用したビジネスプランがあって金を出せる!みたいな人がいたら twitter @mizchi まで教えてほしい。

前作ってたエディタは、コンセプト実装終わった段階のセルフレビューで、使い心地が微妙、という感じで、お蔵入りしてしまった。前回の反省を生かしていい感じに進めたい。

ゴール

非エンジニアを開発ワークフローに巻き込むのに足りないのは Git 周りのツールだと思う。Git がブラウザで動いて push できれば、いい感じにブラウザ上のUIから特定ドメインの作業を手軽に使えるようにして、markdown みたいなドキュメントとか、フロントエンド系のプレビューを組み込めばいい感じにできそうな気がしている。

React Component 視点でのアトミックデザインの解釈といくつかの疑問

フロントエンドの中でも、JS書くプログラマと、CSSを書くマークアップと、デザインカンプを作るデザイナで、コンポーネントという概念がズレる。だいたいこれらが一人だったり兼任だったりで1~2レイヤーの開発ステップになるが、完全分業だったり人が多くなると混乱の元になる。

誰かが決定的に間違ってるというつもりはない。正直、どっちかというと本来のデザイナ側の用語定義に倒した方がいい気がしているが、プログラム上の都合もいろいろ混ざってきて、話が簡単ではない。

自分の理解が間違ってる可能性もある。この記事はレビューをもらうために書いている側面もあり、指摘されたら追記していく。

読んだもの。

Atomic Design の大雑把な理解

基本的にはあるコンポーネント所属するドメインではなく、独立した稼働単位(Atoms)でコンポーネントを分割して、それを積んで(Molecules)、それらを稼働単位(Organisims)でまとめ、ページ(Page)は複数の稼働単位を持つ、という考え方、と理解している。

自分は Reactの src/components はこういう構成を取っていることが多い。

atoms/
  Text.js
  Button.js
  Comment.js
molecules/
  CommentList.js
organisms/
  Header.js
  UserProfile.js
pages/
  User.js

コンポーネント名は仮のもの。

ゴールの一致

プログラマ視点だと、コンテキストを作るのが難しいコンポーネントを storybook で作れたり、 storyshots でスナップショットテストできたり、 puppeteer でスクショとったりできて嬉しい。あと地味に嬉しい点で、粒度別にアルファベット順になってエディタで読みやすい。

コンポーネント境界と分割指針がズレる

ここから問題。

デザインとプログラムで、「コンポーネントが独立稼働できる」の単位が違う。

プログラマ的にはコンポーネント定義の再利用性で決めることができる。例えば React のプログラム的に独立した atom を発見するのは簡単で、他の Component を import していない、ということになる。

しかし、デザイン上の出現単位とってプログラム的な依存ステップは必ずしも一致しない。そういうときは、自分はさらにディレクトリを掘って分解している。

atoms/
  Button/
    ButtonIcon.js
    ButtonLabel.js
    index.js

1ファイルに押し込めないのは、コードが長くなって可読性が悪くなったりテストしづらい、というプログラム上の問題で、後は import path を深くしたくないので、 ./atoms/Button で import できるというのは変えない。index.js から import するだけ。index.js 以外は、「外から見てプライベートなモジュール」としている。

ただ正直 Atoms と Molecules はプログラム上の境界が曖昧でいつも困っている。上の例だと atoms/Icon.js になるのかもしれないが、そう出来ないときもある。

こうして、プログラム上では Atoms が Atoms に依存したり、Molecules が Molucules に依存しそうになって、(さすがに Atom が Molecules に依存することはないが)都度解釈をこねくり回している。

プログラム上にしか存在しないデータパス(props)

プログラマ的には、React Component は基本的に props のデータパスを設計し、末端でデザインが表出する、というマインドセットになる。

逆に、デザイナ的にはデザインモックから起こしていくので、その過程にデータパスは存在しない。プログラマはそのデザインモックを見て、props 渡す経路を考えたり、 connect する単位を考えることになる。

コンポーネント A - B はデザイン上独立しているように見えているのに、A - B のデータパス上の関係においては依存があって、実質的に紐付いている場合、これは Atom か Molecule かの解釈が立場によってズレる。

自分は、基本的には Organisms から redux へ connect にするように心がけるが(これは React のパフォーマンス的な理由もある)、デザイン的な稼働単位に忠実に分割すると、 Atoms に直接 connect せざるを得ないケースも出現する。

Pages はデザインモックなのか

Pages は 「ページ単位のデザインモック」単位と定義されていることが多いが、 実データを入力に持てる JS ではページ単位の入力という単位に pages を当ててることのほうが多いような気がする。自分はとりあえず Router でマウントする単位としている。

この辺や Templates、いろんな実装を見たが人によって解釈が違う。 templetes/DefaultLayout.js みたいな 「ヘッダーとその中身」みたいなコンポーネントがあったのもみた。(流石にこれは語感から拾っただけだと思うが)

React や storybook を使うとき、storybook が実質的にデザインモックの実装になったりするので、 Pages の実装では、モックであるというというのは忘れたほうが良いのではないか。

また、例示した DefaultLayout のような、Layout 情報だけを持っているユーティリティ的なコンポーネントがどこに所属するか難しい。Row や Column などは Atom でいいのか? それに相当する言葉を自分が知らないだけかも。

おわり

とりあえず自分が決めているのは、デザインガイドから作るのではなく、大きな Organisms を作りながら、ユースケース単位で小さなコンポーネントに分割していく、というフローを取っている。最初にデザインガイドをトップダウンでみっちり決めてもあんまり使われない。

正直一人で作るときは完全にプログラマ都合で作るのだが、大規模だとそうもいってはいられなくなる。

間違っていたり、こうしたらいいい、というのを指摘いただけると嬉しいです。

最近のフロントエンドのエディタ事情

これは、個人でどんなエディタを使うべきか、ではなく、「チームとして」新しいものを採用するとき、あるツールがエディタ横断で便利かどうかを考える必要がある。

自分個人としては、基本はAtomを使って、TypeScriptを書くときだけVS Code を使っている。ターミナルでは Vim

環境でエディタを選ぶ

最近の新規プロジェクトでは、とくにブロッカーがなければ TypeScript を使っていいと思う。TypeScript を使うなら当然 VS Code を使うことになる。AtomVim でもいいが、TypeScriptのエディタとしては、流石に完成度が頭一つ抜けてる。JavaならJetBrains 的なノリで、TSならVSCode、そういうものと思ったほうが楽。

TS以外なら、エディタはなんでもいいが、ある程度流行ってるものでないとエコシステムに追いついてくれない。

prettier の勝利

フォーマッタは当然のように prettier が入る。もう選択肢はなくて、猫も杓子もprettier。すでに勝負は決している。

保存の度に自動整形を掛ける。人によってはエディタに書き換えられるのが嫌かもしれないが、最終的にCIで通ってればなんでもいい。.prettierrc あればよし。

話がややこしくなるのは、styled-components のような CSS in JS 系が入ってきたときで、 JS(TS)内のインラインでCSS書くときにCSSとしてハイライトされてほしい。それができる環境を作れる人は CSS in JS を便利だと思えるが、そもそもそれができるという発想がないとハイライトがない環境でCSSを書くことになる。このとき CSS in JS へのヘイトが当然のように高まる。これは、CSS だけでなく、GraphQL などにも当てはまって、GraphQLのクエリも prettier で整形されるし、インラインでハイライトされる。

なので、最近はこういうJSも、環境を整えればちゃんとハイライトされる。

// GraphQL
const query = gql`
  query {
    users {
      name
    }
  }
`

// StyledComponents
const RedText = styled.span`
  color: red;
`

// JSX
export default () => <RedText>red</RedText>

Linter

エディタ環境、今ではいろんな意味で prettier ファーストで作ると圧倒的に楽で、prettier 自体はエディタとは独立しているので、単にそういうプラグインとして実装すればいい。流行ってればだいたいある。なければ作るか諦める。

あとは linter。エディタで動的にプレビューされてほしい。とくに大事なのは unused-vars で、未使用変数が常に通知されるのはコードの書く順序に少なからぬ影響がある。これから使う変数なのか、消えるべき参照なのか。

最近、空白系はeslint/tslint に書かずに prettier に丸投げするのが流行っている。むしろ干渉するから書くべきではないという意見もある。

最近試した中では https://www.npmjs.com/package/lynt という lint ツールが便利だったが、これはエディタ組み込みがなくて、小さい npm パッケージ作るときぐらいしか採用できなかった。

追記: Languagre Server Protocol

最近のエディタはTSのとき以外なんでもいい、といった理由だけど、最近は Language Server Protocol という補完エンジンの規約があって、言語側で実装されてればエディタがこの仕様で実装されていれば補完できる。自分がしる限り、 TypeScript/Flow/C#/Rust とかで実装されてる。たしかMSとGitHubあたりが規約作ってたはず

https://langserver.org/

自分が主にAtomを使ってる理由は、エディタのコードをだいたい全部読んでるので、いざとなったら自分で色々やって解決できるという安心感があるから。

大事なこと

自分一人で開発する際に便利だと思って環境を作っても、他人が不便な環境でやってることに気づけないことがあって、開発環境作る人は誰かの不便を可能な限りキャッチアップしてカバーするのが大事だと思う。

ちょっと前に、JSXハイライトがない状態で、JSX出現すると以降のハイライトが全部壊れる環境でReactを書いてる人を見たことあるが、「そういうものだと思っていた」みたいなことを言っていた。こういう状況を放置すると無意味なヘイトを生んでしまう。

Flutterのコミットログを読んで、これは一体何由来なのか調べた

本当に凄い雑にコミットメッセージと気になったdiff だけ読んだ感じなので正確性は保証しません。

結果としてこれは間違ってました

s/demenic/domenic 今思うとここは単にmerge コミットのような気がする

docker + k8s やってる

この記事は別にチュートリアルとかではなくて所感を書いてるだけ。

だいぶ長くフロント沼やってきて、主観ではあるが、だいぶ落ち着いてきた。落ち着いてきたというか、自分は一通りキャッチアップ済みなので後は差分だけ抑えていけばいいという状態。PWA周りは色々あるが、各種標準化を寝て待って方がはやい。どうせ各自IE11が死ぬ2020まで特に大きな動きは取れまい。外から今フロント何やればいいのって聞かれたら、 とりあえず typescript + react + redux + styled-components + prettier 余裕あったら react-native(expo) やっとけって言う…。

というわけで、停滞してる今こそ他のジャンルに手を出すチャンスだ、と思って、横から動向を眺めつつそろそろかなーと思っていたのが docker + k8s と keras + tensorflow のどっちかで、docker はちょうど仕事で dockerfile 書く必要あったので、やることにした。よく考えたら、今まで他人が書いた docker-compose.yml を使うだけだった。

同時に複数の沼に浸かるのは不可能ではないが、ダルい。効率良くやりたい。docker + k8s は docker に k8s 同梱されて、EKS控えてる今だろ、ぐらいの感じで触り始めた。

で、とりあえず Linux わからんわけじゃないし箱に入ってるだけのそれに port の穴あけてガチャガチャするだけじゃろ、みたいな雑な世界観で手を付け始めたが、そのとおりだった部分もありつつ、Dockerfile の世界観がだいぶわからなくてキレながら書いてた。ただ、大昔(何年前か忘れた)に触ったときと比べて、ハマりどころは皆ハマったというのが検索して出てくるようになっているので、まだマシな世界になったと思う。

multi-stage build はなんだこの地獄はって感じだった。named volumes がどこにマウントされてるか不明だった。どこから仮想環境でどこからホストのパスなのかわからなかった。 docker-compose up はなんか挙動不審なので、なんども stop してたら <none> なイメージが無限に増えていって、 node の PID 1 問題などを順当に踏み抜きつつ、alpine が busybox に毛が生えたものということを学び、とりあえずテスト環境で 8080にwebpack-serve 立てつつ本番だと生成物をビルドして nginx で配信するサーバーを書いた。 https://github.com/mizchi-sandbox/docker-webpack-server

Twitter でわからん!!!!っていいながらやってたら @vvakame @babie @orisano @wreulicke (敬称略) あたりに添削してもらえて助かった。

書いたが、たぶん自分の理解が追いついてないので、なんじゃこりゃ、となってる部分がだいぶあって、dockerやる前は誰かが書いた賢いコンテナをちょっとチューニングしてポンポン投げるだけと思っていたが(nginxやredisやmysql単体だけなら簡単だったので)、なんだかんだで細かいことをしようとすると無限にハマることになった。Dockerfile、docker-compose は、結局のところ次元が1つ増えたシェルスクリプトみたいな印象。ホストから仮想マシンにスイッチしたり共有しつつ任意のスクリプトを発行していく感じ。

Docker、ハマるといいつつ一度やればいい問題なので、地雷たくさん踏んで体力つけましょうという問題でしかないのだが、こういう作業してると細かいタイポとかでドンドン死ぬのだが、何もわかってないがゆえのハマりポイントの勘を洗い出せないがゆえに調査スコープを絞れなくて歯がゆかった。たとえば昨夜 server と service を間違えてるのに1時間気づけなかった。それで自信を喪失していくが、プライドなんて捨ててわかりません助けてください!!!って言ったほうが結果として早い。教えて君(死語っぽい)していた。

今 minikube とか kubectl あたりで試していて、むしろこの辺はまだ整理されてる感を感じるが、そもそも難しいことをしようとしていて、登場人物が多い。コンテナ監視する中央管理者がいて、コンテナをとっかえひっかえ入れ替えるマンがいるというのはわかった。

とはいえ今後 serverless などを主戦場にしたければ避けられない範囲に思えるので、今のうちに消耗しておきたい。フロントエンドやり続けるにしても、結局次の伸びしろはそこだと思っているので。

クライアントサイドのモデルとは何か 後編 ~ 単方向データフローと参照透過性

この記事は クライアントサイドのモデルとは何か 前編 ~ クライアントサイド MVC の死 - mizchi's blog の後編。

前提として、今回の出す例で、「Web フロントエンドで、そこまで複雑な状態を考慮するなんてそもそも間違ってる」という意見があると思う。これに関して、そもそも「SPA というものが、いかに実現可能になったか」という視点の話であり、また、自分の経験上「フロントエンドなんて雑でシンプルでいいでしょ」というものが、複雑な構成を取っていくのを、何度も目にしてきた、という2つの前提がある。

適切な粒度に応じた適切な構成をとるべし、というのは別の話で、今回、対象が複雑なアプリケーションなのは前提とする。

Flux 以前

先の記事で ActiveRecord を前提にしたサーバーサイド ORM をクライアントで輸入しようとすると、クライアントでは Storage 層が存在しないので概念的にインピーダンスミスマッチがあり破綻することを述べた。今回は View の構造から逆算して、あるべき「クライアントのモデル」の姿を振り返ることにする。

まず最初に、複雑な Backbone.View でどうなっていたかを説明する。

このブログのような画面を構造を想定する。(Backboneでこういう構造に見覚えがある人がいるのではないだろうか)

- Router
  - ItemsController(/items/:id)
    - HeaderView
    - ContentView
      - ItemView
      - CommentListView
        - CommentView
    - FooterView
  • Router が URL を受け、対応する Controller を発火
  • Controller が View を複数生成する
  • 各 View は親子構造を持ち、それぞれの View のイベントを受けて通信させる

例えば iOS だとこの 「View を束ねた実体を持たない View」 は ViewController と呼称されるかもしれない。

問題

ここで問題になるのが、ルーターから伝搬された初期値を元に、View の親子関係の間に「状態」が発生する。それらはイベントドリブンに個別に更新される。

ツリー構造の状態の様々な関係の「隙間」に「状態」が生まれていく。しかも、この状態の発生パターンは一意に決まらない。発生しがちでいて複雑なパターンの一つが、何かの配列を元にしたリストビューで、動的に伸びたり縮んだり入れ替える必要がある。

結果として、トップダウンに読み下した時に、どこに状態が埋もれているかが、どの状態とどの状態がイベントを通して通信しているかが、非常に不明瞭になる。また、ツリー構造に応じて、再帰的にインスタンスを捨てるコードを書く必要があったり、メモリ管理がシビアになる。(フレームワークによっては再帰的に dispose してくれるが…)

後期 Backbone (と勝手に自分が呼称している) Marionette.js や Chaplin.js は、これらの構造を比較的わかりやすく助けてくれるヘルパが用意されていて、自分はそれらを使っていたが、やはり大規模なパターンでは破綻しがちだった。

ここで何が問題だったか。思うに、サーバーサイド MVC は View についてのドメイン用語が貧弱なので、単に View とした際にそれが親子構造をとって、相互に通信する状態まで考慮していない。複雑なケースでそれが破綻を招いた。

これは Flux 以前の様々な GUI フレームワークで発生している問題で、ちょっと前だと Flash の Stage のツリーがそうだったし、Vue 1.x 系 がそうだったし、 AndroidiOS もそうだった。(今は多分 Clean Architecture などである程度解決されているだろう。詳しくないので識者に譲る)

Flux 以降

問題は、状態が散逸してしまって見通しが悪いことにある。そして通信経路は必ず親を経由する必要があり、複雑な Pub/Sub を構成する。それによってコード上の見通しの悪さや、メモリ管理の難しさが発生する。

じゃあ結局どうなってると良かったか。

よく考えると、いや考えなくてもわかることだが、クライアントのモデル、というのは本質的に今画面を見ている人間に対して必ずシングルトンである。マルチタッチを前提にユーザーが複数いることもあるが、そういうのは例外として考えなくともよい。

理想的にはこうなる、と Flux の発明者は考えたに違いない。

  • 状態はViewを抽象するシングルトンである
  • 状態を一箇所に集約する
  • 状態から常に一意な View を生成する

つまりはツリー構造中にどこで埋もれるかに関わらず、こういう JSON を集約して管理したい。

{
  "loginUser": {
    "id": 3,
    "name": "mizchi"
  },
  "item": {
    "title": "About Models",
    "body": "...",
    "comments": [{"name": "foo", "body": "..."}]
  }
}

このプレーンな JSON のような構造体が、本記事における「クライアントのモデル」とは何か、に対する答えになる。クライアントのユースケースを抽象する構造体。

で、これを入力値にすると状態を生成する update 関数があれば良い。

function update(state) {
  render(RootView, state);
}

View におけるイベントは、「その発生箇所に関わらず」この JSON を直接操作するアクションを発行すればいい。

擬似コードだとこう。

// アクション発行元
view.onClick(_ => dispatch({ type: "logout" }));

// 受け手
function logout(state) {
  delete state.loginUser;
  return state;
}

// 更新
root.on("logout", () => {
  update(logout(state));
});

これがいわゆる Flux の Store - View - Dispatcher 構成となる。

注意してほしいのは、 Dispatcher の責務がややこしく混乱を招きがちだが、実際は Store -> View -> (...) -> Store -> View -> (...) というループがあって、View <- Store に逆走できない点だ。これを単方向データフローと呼ぶ。

なぜ今になって Flux が出てきたのか

たぶん、素直な直感として最初に思い浮かぶのは、「常にルートから生成するとなると、常にすべてのインスタンスを生成するので実行効率が悪いのでは?」という懸念だろう。これはその通りで、これがまさに React 以前に Flux が存在しなかった理由であると思う。

React はルート要素からすべて生成しているように見えて、その実、状態の差分だけを更新するフレームワークで、仮想 DOM アルゴリズムによってこのフローを実現する下地を用意した。また、Angular は仮想 DOM ではないが、効率のいいデータバインディングとは、仮想 DOM と実態にそんなに差はないと思っていて、結果として差分適用の手法にすぎない。Vue や mithril, riot は React 以前からあるフレームワークだったが、基本コンセプトとして仮想 DOM を実装していった。フレームワークにとって差分適用の手法は本質ではない。(ライフサイクルの違いとして表出することはある)

実際、実行効率を無視すれば単方向データフローを実践するのは簡単で、更新があるたびに毎度 document.body.innerHTML を書き換えてイベントハンドラを更新し直せばよい。

Store != Model

自分は Store has a Model の関係だと思っている。Store は一方向に流れてくるアクションを捌きつつ、Model(=State) を更新する。

サーバサイドにおける Storage や他のドメインモデルは、Store がその更新時に参照しうるリソースの一つにすぎない、というのが自分の見解で、Store はその仲立ちをするだけである。

正確を期すと サーバーサイドへのアクセスは、 Store の中の処理ではない。Dispatcher が アクションを生成する処理(ActionCreator) のトランザクションの中で参照しうる IO が、いわゆるサーバーサイドのビジネスロジックの参照、という解釈になる。

関数型との奇妙な一致

正確には、親子間で埋もれる「状態」はある。ただし親が子の状態を参照する手段はない。これによって state の用途は著しく制限される。基本的に、Dispatcher を通じてアクションの副作用を一周させるしか親に変更を伝える手段がない。

// 閉じた状態の例
class Counter extends React.component {
  state = { counter: 0 };
  render() {
    return <button onClick={() => this.setState({ counter: this.state.counter + 1 })}>+1</button>;
  }
}

「状態は埋もれているが、ルートから参照できない」ことによって、そのスコープに閉じたマイクロマネジメントは個々の Component に任せつつ、シングルトンな本質的な Store にロジックが集中することになるのが、Flux の本質であると思う。

ここまで、あえて Redux の話をしなかったが、Redux は単なる「状態管理を関数で表現する」の方法論であって、Flux の本質ではない。とはいえ理解を薦める助けにはなる。

Redux の Reducer というのは、とても単純な思想で、 (State, Action) => State という型で表現される関数でこの状態遷移を表現しよう、というアプローチである。

const initialState = {
  counter: 0
};
const counter = (state = initialState, action) => {
  switch (action.type) {
    case "increment": {
      return { counter: state.counter + 1 };
    }
    default: {
      return state;
    }
  }
};

この返り値が、クライアントの現在の状態を示すモデルとなる。

正確に言うとこの関数合成のヘルパなどがあるが、State の型さえ守っていれば関数の実装はどうでもよい。型の扱いがセンシティブなので TypeScript や Flow の需要が増えた、という側面はある。ちなみに作者の Dan Abramov 曰く redux のインスパイア元は elm。

状態が関数を通して変化する、というのが、関数型的な参照透過性の考え方と相性が良く、関数プログラミング的アプローチが React/Redux で主流になった、と自分は理解をしている。(ここを掘り下げようとしたが力尽きた)

(自分が個人的に嫌いなのは Redux Middleware で、それはこの外の話で、今回はしない)

まとめ

状態が各 View の隙間に散逸するのが GUIアンチパターンの一つで、それを仮想 DOM の関数型的なアトミック性と、「メッセージの伝達が親方向へ逆走しない」という単方向データフローによって解決した。

クライアントのモデル、とは画面抽象であって、サーバーサイドのモデルを API 等の IO を通して参照するが、サーバーサイドの MVC におけるモデルと同一ではないし、同じに考えてはいけない。

React は Web の為に発明された手法だったが、Facebook はこれをあらゆるプラットフォームに適用する可能性を見出し、その結果 ReactNative や ReactVR などが生まれることになった。自前のブラウザエンジンを持たない Facebook らしいポジション取りだと思う。

クライアントサイドのモデルとは何か 前編 ~ クライアントサイド MVC の死

前置き

この記事、本来は Flux には Model がないのではないかと思った覚書 - ナカザンドットネットFlux の Store が ViewModel かって話からの MVW とかどうでもいいって話 - 猫型の蓄音機は 1 分間に 45 回にゃあと鳴く のアンサーとして書き始めた記事だが、前置きだけで別テーマとなったので、前後編に分割する。

僕は元々がゲームクライアント屋だったときの発想を引きずってるのと、既存の Web の開発の文脈に対して距離を置いていることを明言しておく。あとこういうテーマでとある原稿書いていたので、頭の整理も兼ねて。

ActiveRecord の功罪を振り返る

このテーマを語るにあたって、まず RailsMVC について述べなければならない。なぜなら、フロントエンドのアーキテクチャとは、サーバーサイドの MVC の模倣に始まり、破綻し、結果として iOS/Android/Desktop の GUI アプリ設計手法と合流したからだ。

Rails の ActvieRecord の失敗は、端的に指摘できて、それは Storage 層 と、ドメインを記述するための Entity を区別しないというところにある。本来の MVC を WAF に特化した省略形で、この設計は MVC2 と言われることもある。

MVC と MVC2 について改めて考えてみる - スタジオ・アルカナ技術ブログ

この MVC2 は Web 特有のリクエスト・レスポンスに起因していて、コントローラで受けてから値を返すまでのライフサイクルが以上に短い(50ms~1500ms)。また雛形のコードも短い。なので、Enitity と Storage を区別しているステップがもったいない。

あえて区別しない express の擬似コードを書くならこうだろうか。

server.post('/users/save', (req, res) => {
  const attrs = req.body
  // 無意味なコードなのであえて冗長に書いている
  const newUserEnitity = new UserEnitity(attrs)
  const userStorage = await UserStaroge.save({
    id: newUserEnitity.id,
    name: newUserEnitity.name,
    email: newUserEnitity.email
  })

  res.json(userStorage.attributes)
})

まず req.body が外部 IO から来る値なので静的検査しづらい。最終的に値を返す res.json(...) も同様。*1

そして外部 IO から来る値を元に Entity を組み上げても、短いコードではそのロジックを使う間もなく、Storage にセーブして終わり、となりがち。なので、この Entity と Storage は一体化した Model という名前のストレージ兼ロジック抽象の何かになった。そしてたぶん CakePHP で導入されたその思想は Rails 等の他の MVC に受け継がれていった、と理解している。

簡単なうちは簡単で済む。それはいいことだと思う。他を害さない限り。

追記: ActiveRecord パターン、 Cake => Rails という順番だと聞きかじっていたけど、Martin Fowler の PoEAA => Rails => Cake3 らしいです

良い解決策とは何か

セットアップを短くして、初心者や初学者にいい顔するのはいいことで、不必要なことを抽象化できてるということだし、興味のスコープを宣言できてることなので、悪いことではない。

最近でも next.js なんかはそれの権化で、pages/index.jsexport default () => <h1>Hello</h1> と書くだけで SSR する React アプリケーションの開発がはじめられる。この体験は鮮烈だった。確かに、本質的に削ぎ落とすと最初はこれだけでいいはずだ。

https://github.com/zeit/next.js/

で、問題は、これが複雑化した画面でどういう柔軟性があるか。

next.js の難点は、多様な要求に対してオプションを提供するのではなく、 作者の @rauchgミニマリズムな思想に従うことを強制してくるタイプのフレームワークで、その点使い勝手は後発の nuxt.js に劣る。

Rails に Service 層を生やすかどうかよく議論にあがるが、それは ActiveRecord が Entity としての振る舞いをどこに書くかの居場所が RailsMVC モデルだと用意されてないからだと思う。Controller が分厚くなったら、共通処理は Model に書きましょう、というのがこのアーキテクチャから自然と導かれてしまうアンチパターンで、これは悪い DRY の話にもつながる。

俺が悪かった。素直に間違いを認めるから、もうサービスクラスとか作るのは止めてくれ - Qiita

クライアントの「モデル」

前置きが長くなった。で、本題のクライアントサイドのモデルだが、複数の解釈が発生してしまった。

  1. サーバーサイドのモデルを抽象したプロキシ
  2. クライアント上のユースケースを表現した Entity
  3. クライアント側の永続層(IndexedDB/LocalStorage) のストレージ抽象

これに対して誰も一貫した答えを持っていなかった。というのが 2012 年ぐらいから段階的に明らかになったことで、DDD 的解釈や素朴な WAF の延長と捉えた人で解釈が違っていた。僕は Electron アプリの開発をしていたので 3 とも向き合うことになった。

1 が Backbone と Ember で、これは前提に REST がある。1 画面が 1 つの Model に紐付いていて、その Model のクライアントにおける写像を用意すれば、一貫した開発体験が得られる、というわけだ。

結果から言うとこれは破綻した。理由は 2 つある。

まずサーバーとクライアントのライフサイクルが違う。サーバーは先に述べたようにせいぜい 200ms~15 秒だが、クライアントサイドはタブが生成されてから破棄されるまで動き続ける。(ここは暗に SPA を意図している) なので、想定すべきは 3 分とか 15 分、しかも複数レスポンスに跨って状態を持つ、みたいな話になる。そのトリガーは何かしらのイベント駆動で、また抽象が違う。そもそもリソースに関与しない振る舞いすらある。

2 つ目は、クライアントサイドで発生するリレーションの問題で、クライアントの要求が増える度、必要なデータのクライアントサイドのジョイン が発生する。開発が長い環境ほどインターナルな REST 抽象は破壊され、専用 ViewAPI が増えるかカスタマイズされるかどっちかになる。ちなみに、これに対する解答の一つが GraphQL だったりする。 (ちなみに自分は REST 懐疑派で、もはや誰から見てもユニバーサルなリソースなど存在せず、クライアントからの要求は専用 API か RPC を作るのが良いと思っている)。

node だと isomorphic というテーマがあって、それを無理矢理に一致させようという研究は行われていたが、結果として上手くいったとは言えない。専用の PaaS が必要な Meteor は結局流行らなかったし、結果としてその差を強く意識するようになってしまった。

じゃあどうなったのか?それは ウェブの MVC という前提を捨てて、Flux という名前で MVC モデルを見つめ直したことで、結果として GUI プログラミングと潮流と合流しつつ、ストリームの監視と差分適用というものにフォーカスしたパラダイムに進化したのだ、と自分は思っている。

後編に続く。

mizchi.hatenablog.com


おまけ: 初心者にいい顔できるツールが流行る

やや愚痴っぽい話。

ちょっと本題からずれるが、プログラミング言語フレームワークの流行は、以下に初学者に対していい顔をするか、という点に尽きると思っている。かっこいいものを手数少なく書けるとカッコイイ。チュートリアルは短ければ短いほどいい。ここ近年の静的型付の復権は、単に型表現のパターンや推論機が発達して、チュートリアルのサンプルコードを短く書けるようになったかどうかに過ぎない点もあるのではないか。

これは悩ましい問題で、実際仕事でアプリケーションを書いていくにあたって、フレームワークの選定などを行うアーキテクトの立場では、コミュニティで人気があるものと、アーキテクチャ的な伸びしろがあるかどうかはまったく独立した要素だ。前述した next.js は、僕も便利だと思いつつ、仕事のような要件がコントロールできない場合に採用するのを薦めることができない。SEO 上の理由で SSR する必要があるなら苦労してでも redux SSRのボイラープレートを一つ採用するのを薦める。そもそも SSR 不要なら SSR 不要であると言うことのほうが多い。

極端なのは「プログラミング抜きで〜できる!」という煽りで、その場合、プログラミング言語に等しい一つの DSL やツールを覚えることになるのだが、それらのツールがその説明を果たしているとは言い難い。極端なのは RPG ツクールだと思っていて、例えば僕がプログラミングを最初にやったのは、WolfRPG Editor(ウディタ) の戦闘画面のコモンスクリプトを改造していて、こんなんプログラミングじゃん!って思ってはじめたのが最初だったような気がする…。

*1:個人的に、ここの静的検査の弱さが 10 年前の動的型付ブームの理由の一つだったのでは、とも思っている。自分は逆にアノテーションとしての型を書くべき派だが…