Architectural Decision Records を知った

※ 正確には「知った」のは恐らく半年以上前なんだけど、ちょっと触ったりドキュメントを読んだりした。

"life is a succession of choices, what is yours?" by https://unsplash.com/@ivalex

設計の基準を明確にして残しておきたい

自分の問題意識としては「設計の決定のための検討、比較などの記録が issue や pull-req に埋れがち」というものがある。またこれらは実はそれほど検索しやすくないし、記録を遡ることもやりやすくない。

この問題は実はかなり長いこと意識としては持っていて、何かみんな困っているはずなんだけどなーと思いつつ、全然情報が見つからずに思い出しては放置、思い出しては放置していたのだが、今回の直接のきっかけは以下のツイート。

このもとは以下の記事で、

時間を区切って設計を打ち切るのはおすすめできない - hitode909の日記

この記事を読んでまさにちょっと前に自分たちのやったことを思い出していて1 、何回めかのトライでようやく ADRs という言葉にはたどり着いていたんだけど、時間がなくてちゃんと掘り下げられずにまた放置していた。

今回やっと時間が確保できたので、ちょっと調べてみてメモを起こしておく。

Technology Radar ではすでに 2017年末から Adopt

Lightweight という言い方がなされており、いわゆるライトウェイトプロセスの中で XP に寄せすぎると欠落しやすくなる「なぜその設計を採用したのか」を、ライトに残すためのドキュメンテーション テクニック のようだ。

Technology Radar では Wiki や Web サイトではなくコードそのものと同期させることができるので Source Control に store することを recommend してる。

※ example を漁ってみると分かるが、比較的仔細なレベルの設計の意思決定を記録しようとしているので、同じリポジトリに入っているのが扱いやすく、また技術的には「収集して再利用」しようと思うと Wiki ではなくファイルとして存在している方がよいので、Technology Radar の言う通りにするのがよさそう。手っ取り早く始めるには Wiki の方がよさそうに見えるけど。

基本のツールは MARD っぽい

おそらくドキュメントの読み書きが低コストで行えればなんでもよいが、昨今の流れだと大半のドキュメントは Markdown を基本に作成されているはずなので、Markdown で書く ARD ( Architectural Decision Records ) ということで MARD という呼び方があるようだ。

おそらく reST でも Asciidoc でも同じように考えられるはずだし、Word や Google Docs でも構わないように感じる。ただ、コードからの距離は遠くない方がよさそう。少なくとも詳細な実装に紐づく部分は。

Templates

ここまでの情報だと、意図していることはなんとなく分かるが、実際に何を書けばよいかがよく分からない。そこでまずは本家で紹介されている Template を漁ってみる。

RFC っぽい重厚さを持つ流派と、よりライトな流派があり、ライトな流派は以下のように似たような内容を扱っているようだ。

Nygard (2011) Y-Statements MARD Alexandrian Pattern
      prologue
context context context discussion ( context )
status      
  facing decision drivers  
    consider options  
decision we decided decision outcome ( consequences posi / nega ) solution ( decision )
concequenses alternatives not chosen   results ( consequences )
  to achieve    
  accepting pros and cons of options  

だいたいどれも

  • context
  • options
  • decision
  • pros / cons of options

みたいな感じになっている。

adr.github.io がおよそ 3, 4 年前にリリースされており、この頃からこうしたドキュメンテーションの話は一部で盛り上がっているっぽい。しかし Nygard の post は 2011 年であり、もう10年前の話になっている。

XP やライトウェイトプロセス、アジャイルなどの盛り上がりが一段落し、そうは言っても実践的に役に立つドキュメントの手法についても意見が出始めたのがこの頃なんだろうか。

examples

こうして見るとけっこう細かいレベルの記述になっていることが分かる。

処理系

書くには

とりあえず sh script の以下のものが手頃で扱いやすい。これは Markdown を基本にしている。

いろいろな言語に移植されているが、けっきょくのところ決まったテンプレートに従って Markdown を書けばよいだけなので、あまり凝った道具は要らないと思う。

読むには

Markdown で書くと決めたら Markdown なのでそのまま読めなくもないが、

なんかでもよさそう。また多くの人が手軽に読めるようにするという意味では

などを使って Web サイトを起こすのもよいのではないかという気がしている。Wiki と違ってファイルなので、どこかでまとめたり、統一的な UI をもたせたりということもそれほど難しくないはず。

template の種類と組織の回し方と記録の残し方

ということで Lightweight Architecture Decision Records | Technology Radar | ThoughtworksArchitectural Decision Records | adr.github.io をベースにあれこれ触ってみたけど、恐らく template の決め方はいくつかパターンがあって、その判断基準には

  • ドキュメントベースでの意思決定に慣れているかどうか
  • 記録の運用が長期間に渡るかどうか

があると思う。

上に挙げた template はあまり status を項目に持っていないんだけど、これがドキュメントベースでレビューして合議で決定するようなスタイルだと status を持っているのは非常に重要になるだろう。また長期間運用されて設計が更新されていくような場合にも status : obsolete などが欲しくなるように思う。

また、

Getting Started with Architecture Decision Records | Blog

では

  • 社内の既存のルールに従っているだけの場合には ADR は書かなくてよい
  • メール、電話、ミーティングなどを挟んで合意にいたった場合、それを書き起こすことには価値がある
  • ほとんどの場合、ADR は他のドキュメントと同じように Wiki やドキュメントシステムに書く

としている。

恐らくドキュメントベースの議論はそれほど重視しておらず、あくまで結果を写し止めておくような印象を受ける。こっちの方がふつうの組織にはよりマッチしそうに見える。少なくとも「始めやすく、残しやすい」のは間違いなさそう。

ただこういうドキュメントは基本的にエンジニア以外は興味を持たないので、共有のドキュメントシステムにあると、メンテナンスや検索、オンボーディングの視点ではイマイチかもしれない。(プロジェクトをまたいで ADR だけを収集するような方法にも向いていない。)

参考

Architecture Decision Record Template: Y-Statements | ZIO’s Blog: Architectural Decisions, (Micro-)Services and More

から辿っていける話がとても面白い。この Y-Statements も original は medium の post である 2020 年ではなく、SATURN という Software Architecture Community の 2012 年の際に発表されたものらしい。

こうして見てみると ADRs だけではなく AD ( Architectual Decisions ) や AKM ( Architectural Knowledge Management ) という言葉も多く登場してくるのが分かる。ADR という言葉だけでなく、その辺も掘っていけるといいのかな。

(1) SATURN: A Software Architecture Community - YouTube

もう少し全体感がまとまったものは

Documenting Software Architecture – @hgraca

からたどっていける話も面白い。

あと日本語の事例は非常に少ないけど

Architecture Decision Records導入事例 | Fintan

こんなものも。これは運用の事例(一部内容もアリ)。特に初期はコストとメリットが合わないように見えるというのはとてもリアルで参考になる。


  1. いくつかの仕組みが連携する際に、1) 連携元のアウトプット、2) 連携先のインプット、3) 連携先の振る舞いが関係するんだけど、完全な動作を前提にすると 1, 2, 3 の順に実装するしかない。しかし、実際には 3 が決まっていないと 1 が決まらない、なんてことはよくある。そこで 1 の設計を仮置きしておいて、2 はインプットできたものとして適当な値を fixture よろしく置いておいて 3 の実装に取り掛かって、そこから 1 を決定して 1, 2 を実装する、なんてことはよくある。この際、1 の仮置きをちゃんと参照しやすい形で残しておきたい、みたいな話。 

vite_ruby全体的にはとてもよい。けどちょっと注意も必要かも。

を見かけたので Vite 1 beta の頃に独自に Helper 書いて導入したものをリプレイスして v2 にしちゃおう作戦。

Home | Vite Ruby

前提

  • Vite を独自に導入済み。Webpack → Vite の migration で感動した!みたいな話はなし
  • Vite 1 beta を vite_ruby x Vite 2 に migrate.
  • Vite 1 beta を Ruby から利用してた際は Helper を独自実装していた

導入したのは vite_ruby 1.2.11

Vite 1 beta 利用時の課題

  • Vite の dev は Ruby 側の development で使う分にはいいが Ruby 側の test で使うと初回のいろいろな解決が走って重い、build を使ったら使ったで実際に build が走ってやはり重い
  • entry point が複数になる場合に vite.config.js を複数用意する必要があり、非常にダルい、というか現実のアプリだと entry point は複数になるでしょ

導入して良い点

  • vite_ruby の導入は Vite の抱えていた entry point 周りの課題をきれいに解決してくれて助かる
    • vite.config.js からの rollup の設定へどんどん深みにハマっていく必要がない
  • autoBuild を利用すると上記の重さはかなり改善される

導入するなら考えた方が良い点

  • Webpacker 同様 Helper を通して Ruby 側で asset 周りの reverse proxy を実現しているが、このおかげでもう WEBrick での開発は現実的でなくなる
    • ES Modules は Webpack と違ってブラウザ上で直接 import が動いて HTTP connection が爆発する
  • 同じく asset へのアクセスが大量に発生するので logger に工夫が必要かも
  • Webpacker 同様 Helper に新しい名前空間を用意し、そっちの利用を強制してくるのでちゃんと従おう

特に HTTP connection が増えて WEBrick では耐えられなくなる点は大きくて、恐らく Puma を使うのが素直な解決策1だが、Rails のようにライブラリが充実している環境でない、例えば Sinatra や Hanami を利用する場合、 Puma を development 環境で使うのはちょっと扱いにくいので注意が必要。

余談:Ruby外のasset bundlerとRubyを組み合わせる方法と注意点と自分の考えていること

ここからは完全に脱線なので、一つの考え方として参考になるかも。ならないかも。

個人的には Ruby バックエンドや静的サイトジェネレータと Webpack などの asset bundler の組み合わせはそこそこ試してて、

  1. Ruby 側の Asset Helper の実装
  2. Ruby で reverse proxy の実装
  3. Node.js で reverse proxy の実装

を何通りかやっている。2

そのうえで上で書いた Vite 1 beta 利用時の独自 Helper ではあえて reverse proxy を使わない Helper を書いていた3んだけど、vite_ruby は Webpacker を模倣してしまったので Ruby 側の負荷が爆発する形になってしまっている。

Webpack は bundle 済みの asset を serve するので reverse proxy が Ruby 側にあってもよいかもしれないけど、ES Modules は bundle せずに HTTP に丸投げになるので、かなり動作が違う。ここは真似する必要なかったのではないか。

まぁ reverse proxy を Ruby 側に持たせることが一概に悪いこととは言えないんだけど、シンプルな、「Rails を使うほどではないよなぁという開発」の場合に Rails のエコシステムを前提にした「なんでも揃ってる感覚」でやるといろいろハマりが多いので、できればそれほど Ruby の得意なわけではない大量の HTTP の処理が Ruby に回ってくる設計にはしない方がいいんじゃないかと個人的には考えている。

もちろん「デキアイのツールをただ組み合わせて実現できる」ものでやる方がいろいろ端折れて早いのも事実なので、鉄板構成を見つけて鉄板構成だけで押すという姿勢も大事なんだけど、それぞれの得意なこと不得意なことは知っておくといいよ、とも思った。

  1. thread ベースとか event ベースであればなんでもよい 

  2. 結局やるのは Node.js のサーバの URI の解決と manifest.json の解決だけなので、一度やってしまえば別に難しい課題じゃない。面倒なだけ。 

  3. HTML は Ruby へ、Asset は Vite Server へそれぞれ request する形 

Viteは静的サイトにも使えそう

Home | Vite

Vite の説明は割愛。

まとめ

2021-03 時点で、

伝統的な HTML を生成する使い方は Vite 単体では無理なので他のものと組み合わせる必要がある

具体的には

  • HTMLを個別に生成する静的サイトジェネレータ(例えば Jekyll)と組み合わせる
  • Vite に plugin を追加することでも案外手軽に Server-Side Generation が可能
    • この場合は HTML 全体を VDOM フレームワークで実現する必要がある

個人的にはそのうち各種ジェネレータ側の方で asset の処理を Vite に移譲する plugin も出てくるんじゃないかなぁという気がしている。

Viteにできること

以下は Vite 2.1.0 時点の話。

  • Vite はあくまで超高速アセットバンドラ
  • Vite 2 では build-in で複数ページ ( entry point ) を実現できている
  • HTML 側の共通化を諦めれば現状でもシンプルな静的サイト(ただし JAMStack ) の生成には使える

Vite の主眼はあくまで HTML ではなく bundle する asset の方なので、HTML を便利に作る方法は提供されていない。

HTMLを生成するには

SSG ( Server Side Generate ) を利用する。以下は Vue を使って SSG を行うもの。

antfu/vite-ssg: Server-side generation for Vite

セットアップにややクセがあるので注意が必要。Vue の createApp() を置き換えて自動的にセットアップする方式。

その他に

を利用して routing 情報をファイルシステムから自動的に構築してないとたぶんうまく動かない。

その他Viteに追加すると便利そうなもの

VDOM component を利用しないなら

Vueを利用するなら

Markdown を使うことで VitePress のようなものを作ることも可能。1

参考

つくってみた

wtnabe/example-vite-vue-ssg

全部入りスターターキット

antfu/vitesse: 🏕 Opinionated Vite Starter Template

これをマネするのが基本になると思う。

  1. 自分が試したところ VitePress は何が足りないのか分からないがうまく動かせなかった。vite-ssg + vite-plugin-pages は動作した。 

turbolinksで実現したかったことって、こういうことだったのか

Turbo: The speed of a single-page web application without having to write any JavaScript.

Stimulus ほどあれこれ考察はしていないけど、Turbo を軽く触ってみて結構よさげという印象を受けた。Google Analytics などの 3rd party のツールとの組み合わせの部分には注意が必要になるが、特に初期実装ではかなりの威力になりそうだ。

※ 今回は Turbo Stream は試していない。あくまで Turbo Drive と Turbo Frame の話。

Stimulusはturbolinksに欠けていたもの

まず あーありがち - Stimulus、悪くない の方を読んでほしいんだけど、Turbo は turbolinks が洗練されて戻ってきたものであることから始めると導入しやすいと思う。

細かく引用はしないが、turbolinks 登場時、最も困ったのは当時全盛と言ってよい jQuery を利用したコードと極めて相性が悪かった点であった。なんなら Rails 標準の JavaScript のライブラリが jQuery なのにどうなってんだこれは、という話である。

どういうことかというと、turbolinks を有効にしてしまうと jQuery を利用した以下のような典型的なコード

jQuery(function() {
  ..
})

が、初回のページロード時には動くが、ページ遷移やブラウザの「戻る」ボタンを押した際には動作しない、という問題があったのだ。というか今もこれはある。jQuery をやめて

document.AddEventListener('DOMContentLoaded', () => {
  ..
})

にしても問題は解決しない。なにせ turblinks も Turbo の Turbo Drive も

ページ遷移自体はせず、DOMの一部とHistoryへの変更であたかも遷移したフリをする

ものだからだ。

turbolinks や Turbo のために監視するイベントを増やしたりする方法もあるかもしれないが、それなら今なら潔く VDOM 系のツールで SPA を組む方が賛同を得られやすいだろう。ただし開発コストは大きく膨らむが。

しかし、Stimulus を使うことでこれは解消される。Stimulus ではこの部分は Application.start() の中に隠蔽されていて、DOM がゼロベースで構築完了したかバックグラウンドで必要な HTML を取得して一部の書き換えを行ったかに関わらず Controller の connect() メソッドは呼ばれる。

つまり、Stimulus を使うことでページの読み込みを監視する処理をそもそも自分で書く必要がなくなり、それによって Turbo を利用しているかいないかに関わらず安定して同じように動作するのだ。

これは非常に助かる。これまで turbolinks を off にするのは割とよくある話だったと思うが、フロントエンドエンジニアを多く確保しなくても Turbo と Stimulus を組み合わせることで、よくある多くのパターンの動的な DOM 構築に対応でき、明らかに体感速度は向上する。

実際にブラウザの Developer Tools で見てみると HTML を取得するたった一つの connection で、JavaScript も Stylesheet も読み込み直すことなくシュッと遷移するのは実に快感だ。

方法は簡単。例えば Webpack や Vite などの bundle ツールを利用していたら import するだけである。それだけで有効になり、ページ遷移は爆速になる。具体的には

import * as Turbo from '@hotwired/turbo'

かな。これだけでよい。使っていなくても大丈夫。CDN から script タグで読み込むだけで動く。

Turbo Frameはiframeを使わないiframeのような何か

これも非常によくあると思うのだが、HTMLの中の一部のコンテンツを遅延ロードしたいという場合、以下のように書くと実現できる。

読み込む側 /importer

<turbo-frame id="export" src="/exporter">
</turbo-frame>

読み込まれる側 /exporter

<turbo-frame id="export">
  ..
</turbo-frame>

これで importer 側の <turbo-frame> の中身が exporter 側の <turbo-frame> の中身に挿し変わる。

id でコンテンツを紐付けて対応する DOM を入れ替えてしまうという方法である。

同じことを JavaScript で実現する側として正解を挙げると、従来なら

  • サーバサイドで JSON を出力
  • クライアントサイドで JSON を取得
  • クライアントサイドで(サーバサイドとは異なる記法で)DOM を構築

という3ステップを要する。

しかし Turbo Frame を利用すれば全部端折ってサーバサイドで普通に HTML を書いて別な entry point を与えればよい。bot 避けに meta でも書いておけば変なお漏らしもないだろう。これだけで済む。

もちろん、そもそもコンテンツをどのように見せるかは import する側の責務なのでは? という指摘はあると思う。自分もそう思う。でもじゃあそのためにフロントエンドのツールでゼロから書きますか、というと「なんだかなぁ」という気がしないだろうか。DOM の書き換えだってせいぜい最初の一回しか起きないのに VirtualDOM とか必要ですか?という気持ちになるし、今書いているバックエンドのコードから完全に頭を切り変えてフロントエンドの海に潜らないといけない。

Turbo を使うとそこはずっとサーバというか HTML のままで実現できるので、スイッチングコストが発生しない。しかも Turbo Drive でのページ遷移と組み合わせると、遅延読み込みしたかどうかも人間にはよく分からない。

まとめ - Stimulus + Turboはよいぞ

以上のような形で

  • サーバ側の routing (あるいは単なる HTML のパス)で
  • サーバ側の HTML の生成で
  • JavaScript をいちいち書かずに

ユーザーの体験を大きく向上させるのが Turbo の狙いだ。

そしてより細かい部分や 3rd party のツールと組み合わせる際には Turbo 固有の event を利用すればよい。

Hotwire という名前や DHH という名前で大袈裟に構えることはない。なんてことのない、だが確実によく利く勘所を押さえたツールと言えそうだ。

フロントエンドのツールの理解、VirtualDOM や reactive という考え方はそれはそれで必要になるし、例えばタイマーのようなより細粒度、高頻度の更新が必要なものは JavaScript で閉じた方がよいと思うが、ほとんどサーバ側が正解の情報(HTMLなど)を持っているのに JavaScript から遠回りして DOM に反映しているケースも実は多い。

そういう場合に .js.erb のように無理やり JavaScript に情報を渡すのではなく、もうそのまま HTML を渡せばいいじゃんという考え方は一見大胆だし、細かくベンチマークを測るとそこまで速くもないかもしれないけど、そこそこ十分に速くて開発コストが大きく抑えられるのもまた事実。混ぜて使う際にはやや注意が必要かもしれないが、少なくとも初手でコストを掛けずにユーザーの体験をよくするためには大きく貢献してくれそうだ。

※ テストが難しそうというかエラーハンドリング周りの情報が全然ない1状態なのは確かに気になるので、そこは追試が必要。あと HTML を「直接」「普通に」取得するのでサーバサイドや昨今のフロントエンドを書く際に忘れていたキャッシュ周りに気をつけないとハマる可能性もある。あと地味にレビューが難しい。Stimulus も Turbo も生々しく自分で実装する量がグッと減るので、テストコードも恐らくかなり少なくなる代わりに、HTML 側の記述ミス防止は恐らく目視チェックになりやすいので、何か工夫があった方がよさそう。

  1. 普通に console にエラーが出るんだけど、その情報が十分に出揃ってない。 

Stimulus、悪くない

Stimulus: A modest JavaScript framework for the HTML you already have.

2020年末にリリースされた Stimulus 2.0 を試して気づいたことをメモしておく。Stimulus は Rails で有名な Basecamp のメンバーによって開発された生 DOM をベースに、できるだけ template に JS を書かずに動的な DOM の生成、変更を実現するためのライブラリである。

昨今流行りの React, Vue, Angular などの VirtualDOM 系のツールとは根本的に考え方が異なり、今からこれを採用するのはやや申し訳ないというか、正直に言うとよいイメージはなかったし、例えばフロントエンドエンジニアとしてスキルアップしたいぜーという人には合わないと思うが、ただ、どうせサーバサイドの開発が必要で、分業できるほどパワーに余裕のない現場においては VirtualDOM 系よりもかなり高い生産性を維持できると言えそうだ。

少なくとも多能工と多能工が組んでアジリティを確保していこうぜ、という2000年代の空気、Rails誕生前後の空気を再び感じることができる。実際に少ないコードでそこそこのものを高速に開発してオレツエー感を味わえるという意味ではかなりよい選択肢の一つと言えると思う。

StimulusはDOM操作の世界でのよいレール

Stimulusのイメージコード

<div data-controller="counter">
  <span data-counter-target="count"></span>
  <button data-action="click->counter#up">up !</button>
</div>

class CounterController extends Controller {
  static targets = ['count']
  static values = ['count']

  up () {
    this.countValue++
  }

  countValueChagend () {
    this.countTarget.innerText = countValue
  }
}

const app = Application.start()
app.register('counter', CounterController)

簡単に説明すると

  • data-controller で指定した DOM 要素と Controller インスタンスが紐づく1
  • Controller の中で変化を検知できる値として values があり、DOM 側からは data-*-value=<identifier> でアクセスできる
  • Controller 側から操作可能な DOM 要素として targets がある2
  • Controller のメソッドを呼ぶには data-action を利用する
    • event は default のものは省略することができ、この書き方を利用すると event を意識することなくメソッドを呼び出しているように見える

伝統的なDOM操作の世界でのStimulusのメリット

  • DOM の影響範囲を data-* で限定し、そこに効力を発揮する JavaScript を Controller と呼び、関係する処理をこの中に閉じ込めることができる
    • 要素が複数あった場合は自動的に別々の Controller インスタンスと bind されるのでお互いに干渉しない
  • event driven と reactive への習熟という、伝統的なプログラミングスタイルからの飛躍を必要としない
  • DOMContentLoaded 周りの繊細さがない
  • CDN から読み込んだだけで動作する(ES2015 と Proxy に対応しているブラウザが必要)
  • template 側に JS を一切書かずに済むので template 側は副作用込みの行儀悪いものにならない

伝統的なDOM操作コードの問題点とStimulusのアプローチ

残念ながらこの日記にはまとめていないんだけど、上のメリットについては自分も以前からとても頭を悩ませていた。

初歩的な jQuery や VanillaJS で書いたちょっとした装飾や効果、遅延読み込みを実現するコードはほぼ必ずと言っていいほど以下のような問題を抱えている。

  • (event が起きてから DOM の走査を行うので)どこまで影響が広がっているのか想像がつかない
  • やりたいことに名前がついていないので意図が分からない

これは本当に致命的な問題だと思っていた。3

この問題を解消するには 対象と操作を明示する名前を付けたコードの中で、明示した範囲内の DOM だけを操作対象にする しかないと考えていて、自分でそういうツールを書いたりもしていたんだけど4、Stimulus はほぼそのままの回答になっている。

VirtualDOM系ツールを使わないメリット

VirtualDOM系ツールを利用した開発時の課題

VirtualDOM と reactive という考え方は

  • もろもろの「操作」系のコードを省くことでおかしな副作用が途中で混ざり込んでしまう可能性を排除できる
  • event を stream として functional に処理できる

など、より複雑な処理により安全に対応するよい作法である。

しかし逆に、VirtualDOM 系のツールには以下のような問題がある。

  • サーバサイドで組み立てていた HTML の世界に一切歩み寄りがなく、サーバサイドを書いていた手を止めてクライアンドサイドの考え方とツール群に完全に持ち替えないとコードが書けない。完全に分業できる組織ならそれでもよいかもしれないが、特にプロダクトの初期実装時にとても扱いにくく感じる。
  • うまく作用する component を作ろうと思うととても小さいものになり、結果として DOM ツリー全体を構築するためには当初の想像の何倍もの数の component を作らなきゃいけなくなりがち
  • 数多くの component を書いているうちにすべて component だけで解決しなきゃいけないような気になる → 誤った設計を誘発しがち

簡単に言うと VDOM 系は基本的に高くつく。設計ミスも誘発する。そこまでしても速くなるのは DOM の書き換えだけであり、最初の表示は逆に遅くなったりするので、意外と最適化しないと体感速度は改善しない。

VirtualDOM系ツールに対するStimulusのメリット

Stimulus はこれらの問題のすべてを回避できる。改めてまとめ直すと、以下のようにかなり低コストに動的な DOM を利用した体験を実現できる。

  • VDOM 系のツールを動作させるセットアップが一切不要
  • サーバサイドとクライアントサイドの両方をまたぐ処理を書く際に起きる分断がほとんどなく、概ねサーバサイドのコードのような書き味で作れる
  • Stimulus の Controller は VDOM 系の component より少し大きな粒度になるので数をかなり減らせる

もっと細かく調整したり様々なイベントを扱いたいといったニーズはあると思うが、

ツールの大きな持ち替えで手が止まることなくだいたいのケースに対応できる

という実に Rails っぽい考え方でうまくまとまっていると思う。

もちろん、より複雑なロジックやグローバルなステートなどは Controller の守備範囲外だ。その辺りは当然の前提としたうえで考えると、実にニーズナブルでよい解決方法に見える。

そして、実はここが大きいんだけど

  • template 側でのデザインと Controller 側での処理の分業も行いやすい

これ。

VDOM 系のツールを使うと必要以上にデザイナが VDOM 系のツールへの習熟を必要としてしまい、例えばデザイナがツールの習熟度で詰まった際にバックエンドの処理を書いているエンジニアにヘルプがくるとそこでエンジニアの手が止まってしまう。

つまり、

VDOM 系のツールは実はフロントエンドにある程度余裕のあるエンジニアを確保できていないと、デザインの実装に対してもバックエンドの実装に対してもブレーキになり得る

という問題を抱えている。これを解消できる Stimulus というアプローチがとても助かる現場は多いと思う。

Stimulusのデメリット

もちろんデメリットもある。

  • テストしたければ結局フロントエンドのツールチェインの整備は必要
    • そりゃそうだ。もしかしたら頑張って Selenium 経由で console のエラーを拾う、みたいなこともできなくもないかもしれないけど、それはたぶんかなりつらいと思う。
  • Controller のユニットテストは難しい
    • 生 DOM と紐づいてこそ意味があるので、独立したテストは意味を成しにくい。純粋な関数部分のテストはできなくもないが、そもそも Controller に複雑な処理を入れるな、でよいと思う5
    • テストしたければ小さな e2e のテストに

総じて テストコードを書くモチベーションが下がる と思う。これは凝ったことをし始める時、量が多くなった時に突然牙を剥く可能性がある。

※ もっとも、小さな e2e のテストというものはセットアップさえ分かってしまえば書くこと自体はそんなに難しくはない。少なくとも Turbo のように DOM 外の resource へのアクセスが絡まないうちは。

あとこれはデメリットじゃないけど、

VDOM + State Management の開発と異なり、生DOM相手なのでデバッグもブラウザの素の Developer Tools を利用する

ことになる。

参考

  1. DOM 側からは register した名前で紐づける 

  2. どのように操作するかは決まっていない。上の例のように直接書き換えてもいいし、何らかの JavaScript の template を挟んでそこでコンテンツを生成してもよい 

  3. 少なくとも jQuery をやめれば解決するという話ではない。jQuery 的な神オブジェクトを採用するすべての SDK がこの問題を抱えていると言ってよい。 

  4. しかも偶然にも class ベースで書かれている 

  5. 恐らくだが Controller へのアクセス方法を DOM の data-* だけに限定できていて「意図しない呼び出しが行われていない」ことを前提にした設計になっているので、そもそもカジュアルにインスタンスを生成して動かせないようになっていると予想される。 

ついにCloud FunctionsにRuby Runtime登場!

今さら感がないと言えば嘘になるけど、

Ruby comes to Cloud Functions | Google Cloud Blog

ついに、Cloud Functions で Ruby が使えるようになりました! いやー長かったな。

個人的には closed beta の段階で申し込みはしてて動かすことだけはできていたのに結局試す時間がないまま public beta になってしまって申し訳ないという気持ち半分、いやでも単純に嬉しいという気持ち半分です。

何はともあれ、これで Ruby の実行環境にカジュアルな FaaS の二つ目1が加わったわけです! いやめでたい。

というわけでこれまでの他の言語での経験と Ruby / Rack / Ruby版 Functions Framework の特徴をざっと眺めて、実際のプロダクトコードを書く際に気を付けることなどに触れて紹介に代えたいと、思います!

Functions Frameworkというものがあるよ

Ruby で Function を書く際には Functions Framework というものを利用する。

GoogleCloudPlatform/functions-framework-ruby: FaaS (Function as a service) framework for writing portable Ruby functions

closed beta の頃にはすでに存在してて、こいつが何をするものかというと、

  • Cloud Functions, Cloud Run または Knative ベースの仕組みの上で動かせる
    • Cloud Run は Knative ベース
  • local での開発にも利用できる

という代物で、先行している Node.js や Python 版と同様の役割を果たす。すでにこれらの言語で経験のある人には説明の必要はないんだけど、

特徴的なのは GCP 上のイベント(代表的なものは PubSub)に対応する関数も HTTP 関数も同等に扱える

点。

どういうことかと言うと、Functions Framework の中身を見ると分かるのだがイベントも頑張って Rack::Request に変換してくれているおかげで、対応するすべてのものを Rack アプリで処理できるようになっている。

つまり Ruby で HTTP のサーバサイドを扱う人には最も馴染みの方法で HTTP 関数だけでなく様々なアプリを書けるようになっているということです。素晴らしい。

Functions FrameworkやFunctionsの不便な点

ただ、Functions Framework は Node.js なんかもそうなんだけど、注力しているのはこの request の source の抽象化であって、実際のアプリ内の話については重視していない。そこで以下にいくつか不便な点を挙げていこうと思う。

reloaderの問題

Ruby では多くの場合でフレームワーク側で reloader を用意してくれていて、Sinatra も Rails も開発環境ではいい具合にソースコードの変更に応じて reload してくれるのだが、Functions Framework はそういう部分をケアしてくれるものではない。

ではどうするかと言うと、方法は大きく二つあって、

  1. Functions Framework を Sinatra など reloader を持つ framework と組み合わせる
  2. nodemon のような汎用の reloader ( restarter ) を利用する

1 の方がたぶん慣れている人には分かりやすい。具体的にどのように Function を書くかについては公式に記述があって、

File: Writing Functions — Functions

にあるように、Sinatra などと組み合わせることができる。使い方としては要は Rack アプリに対して Rack アプリが期待する env を渡してやるだけである。公式の Sinatra サンプルは class ベースの modular style アプリだが、class を定義しない classic style でも同様に利用できる。

そのうえで、Sinatra のアプリケーションコード内で sinatra/reloader を require してやれば普通に development 環境では reload が有効になる。

ただ、後述するが実際には典型的な人間向けの Web アプリの実装に向いているこれらのフレームワークを利用する機会はそんなに多くないように思う。その際はフレームワークではなくこのあとに述べる reloader と組み合わせるとよい。

nodemon

nodemon はもとは node コマンドを置き換えて

$ node app.js

の代わりに

$ nodemon app.js

のように使うとファイルの変更を検知して自動的にサーバを restart してくれるというものだったが、watch する拡張子と実行するコマンドを指定すると Ruby アプリでも問題なく利用できる。具体的には

$ nodemon -e rb --exec "functions-framework-ruby -t <func>"

のようにしてあげるとよい。ここに書いたものは npm run や bundle exec は省略してあるので必要に応じて適宜追加してほしい。

functions-framework-ruby 0.7時点でのNode.js版1.6との違い

functions-framework-nodejs は body-parser を含んでいたりするので、素の express の request, response を扱うこととは異なり、もう少し便利な機能が備わっている。

対して functions-framework-ruby 0.7 時点では今のところ追加の支援はなさそうなので、かなり生々しい Rack オブジェクトを扱うことになる。現実的なアプリを書くには、例えば比較的全部入りのアプリケーションフレームワークと組み合わせる2か、あるいは

を使いつつ

のような機能を追加して対応していく感じになるのかな。こっちのアプローチの方が express っぽいけど、Rails も Sinatra もあまり軽くはないので、特にリソースの制約の厳しい Functions 環境ではこれくらいの生々しさや軽量さへのこだわりはあってもいいかも。

cf.

Functions Framework独自の情報をどう引き回すか問題

これは Functions Framework が担う部分と従来の Web アプリケーション Framework の担う部分の違いの話。

Rails などの通常の Web アプリケーションフレームワークは request, response の入出力の方法からアプリケーションロジックに当たる部分、DBMS の読み書きの部分までトータルにサポートするのが一般的3と言える。

対して Functions Framework の責任の範囲はあくまで HTTP と CloudEvents を抽象化し、Rack 互換のアプリケーションで処理できるようにするための繋ぎの部分であって、それ以外は対象外となっている。

そのうえで、Functions Framework 固有の情報もある。例えば global, set_global というメソッドは Functions Framework 内だけで利用できる global 変数のようなもので、これを利用すると以下のように request を受けるたびに実行するには重たい初期化処理を行ったり、その結果の情報を保持しておくことができる。

FunctionsFramework.on_startup do |function|
  set_global :config, heavy_initializing_process()
end

FunctionsFramework.http <name> do |request|
  config = global :config
  ...
end

ただこれが通用するのは Functions Framework 内の話であって、例えば先ほど挙げたように Sinatra と組み合わせるような場合には Sinatra の中ではこの global にアクセスすることはできない。

そこでどうするかというと、Sinatra アプリに唯一渡せるのは rack env オブジェクトなので、この env の中に global をぶら下げる形になる。先ほどの例で言うと、

FunctionsFramework.http <name> do |request|
  config = global :config
  env = request.env
  env.config = config

  Sinatra::Application.call env
end

のようになる。

Functionを書く際に考えなければいけないことは意外に多くなる

これも別に言語は関係なくて、今まで FaaS を使っていた人にとっては割と当たり前の話。

実際に Cloud Functions を書く際には恐らく従来の人間向けの Web アプリよりも小規模で、かつ逆にインフラにより近いものを直接扱うようになると思う。インフラというのは例えば Cloud Storage, Firestore などのストレージだったり、PubSub や他の Function など、Google Cloud 上のフルマネージドインフラ。Functions を選ぶということは GCE ではないということで、それはつまりアプリケーションの実行環境から直接同じ OS 上で管理できるような伝統的なインフラが存在していない、という意味になる。だからフルマネージドインフラのお世話になるだろうという推測である。

で、そうなると Sinatra や Rails などのアプリケーションフレームワークの機能や構造の分け方などはあまり役に立たない。これらの便利機能は主に

  • Cookie や Session
  • HTML や response の文字列(JSONなど)の生成
  • (DBMSアクセスの抽象化)

は担ってくれるが、それ以外のインフラを扱う部分に対しては基本的には管轄外となってしまう。

例えば Memcached や Redis を利用する際にそのコードをどのように配置すべきかについては Rails には答えはなくて、利用用途がほぼ cache だろうから Rails.cache のバックエンドに置くことを支援してくれるまでに留まっている。実際には cache を扱うコードをどこに置くべきかについてはガイドがないので、作成および更新処理と読み込む処理が分散してコントロールが難しくなってしまうという問題を抱えていたりする。

こうした課題への対策には恐らくいわゆるアプケーションアーキテクチャを考えることになるだろう。いわゆるオニオンアーキテクチャやクリーンアーキテクチャと呼ばれる類のアレだ。

Functions は周知の通り実行時間や利用できるメモリに制限があり、あまり複雑な機能を実装することはできないが、代わりにいわゆる Web MVC のようなシンプルな構成とは異なり、アプリケーションの扱わなければいけないインフラが複雑になりやすい傾向があるので、その分で考えることは増える。

特に異なるインフラを扱うコードが密に結合してしまうと「本番でしかテストできません」みたいなことが容易に起き得るので注意が必要である。

注意深くインフラとロジックを分離し、依存の方向に気をつけて DI で組み立てていくようにする、そういうコードの量が増えるはず。

参考

  1. 一つ目はもちろん AWS Lambda 

  2. 当然、spin upが重すぎるのでFunctionsには向かない 

  3. Ruby を日常的に利用している人には当たり前の話になるが、基本的に request, response は Rack の作法に従うことでその部分の独自実装をあまり行わないようにするのが一般的であり、フレームワーク独自の部分はあまりない。 

自分が思うRubyの「動的さ」が世間とずれているっぽいので書きとめておくメモ

何かに強く反論したいとかじゃなくて、自分で引用しやすいようにpermalinkを作っておく、くらいの意味。

Rubyは動的型であるという言い方

動的型というのは実行時にならないと型が決まらないという意味なので、それはそう。例えば Ruby には

int a;

のようなコードは存在しないので、

a = 1
a = '1'

も正しく動く。この時 a の型は実行時にしか決まらず、動的である。間違いない。

※ 1 や '1' は実行前に決まっているけど、こっちの話題は今回は取り上げない。

動的型言語に動的型変換の機能があるけど、Rubyは意外と厳格

1 + "2"を計算する

awk

$ awk 'BEGIN { print 1 + "2" }'
# -> 3

3 が返ってくる。これは数値の 1 に対して文字列の 2 が数値の 2 に動的に変換されて計算された結果である。

PHP では

$ php -B 'print 1 + "2";'
# -> 3

結果は同じ数値としての 3 になる。

JavaScript は

$ node -p '1 + "2"'
// -> 12

これは文字列として連結して 12 になっている。

Ruby

$ ruby -e 'p 1 + "2"'
Traceback (most recent call last):
        1: from -e:1:in `<main>'
-e:1:in `+': String can't be coerced into Integer (TypeError)

で実行できない。

これは Integer である 1 の + というメソッド(演算子のように見えるけど)が String である "2" を強制的に Integer にすることができず、Type が合わないという例外で死んでしまっている。

"1" + 2を計算する

awk では

$ awk 'BEGIN { print "1" + 2 }'
# -> 3

先ほどと同じ 3 になる。awk では + に文字列の連結としての機能は存在せず、常に数値で解釈される。

PHP では

$ php -B 'print "1" + 2;'
# -> 3

先ほどと同じ 3 になる。PHP も + は必ず数値の演算として解釈される。文字列の連結には . という専用の演算子がある。

JavaScript では

$ node -p '"1" + 2'
// -> 12

JavaScript では + は数値の演算にも文字列の演算にも利用できるが、混ざると文字列としての演算が優先される設計になっている。

Ruby では

$ ruby -e 'p "1" + 2'
Traceback (most recent call last):
        1: from -e:1:in `<main>'
-e:1:in `+': no implicit conversion of Integer into String (TypeError)

先ほどと同じように思いっきり TypeError となる。若干エラーメッセージは異なるが、String である "1" の + メソッドが 2 という Integer を強制的に String にできず Type が合わないという例外で死ぬ。

もちろん揃っていれば普通に動く。

$ ruby -e 'p "1" + "2"'
# -> "12"

実はRubyは型について厳しいのだが、静的に強制できないことが問題視されるようになった

Rubyの挙動を整理し直すと、

  • Ruby は文字列の連結も数値の加算も + を利用する
  • ただし + は演算子ではなくメソッドであり、メソッドは左辺に当たる値の class に定義されている
  • String#+ は Integer を受け取っても文字列の連結を行うことはできず、Integer#+ は String を受け取っても数値として加算することはできない

オブジェクト、メソッド、class で考えると至極自然な動作をしている。

このように Ruby は型については実は意外と厳しい。よく分からない挙動で悩まされることは少ない。少なくとも動的変換で変な踏み抜き方はしないので、そういう意味では「型に関する挙動ではだいぶ安全」である。もちろん明示的に変換を指示する際にヘマをすることはできる。そこはプログラマの自由だ。

ただし、標準の機能では変数への代入、関数の引数への割り当て、戻り値について型を強制することができないという問題はある。(動的に死ぬようなコードを作ることはできる。)

逆に、PHP や JavaScript はエラーが起きずに意図しない動作をすることが問題となり、開発規模の拡大とともに型を指定したいという要求が高まり、TypeScript や PHP 7 以降の型の扱いに結実した。結果、Ruby より静的に解釈しやすくなり「より安全に『開発できる』」と言われるようになった。

「動作としての安全性よりも人間の頑張りに期待する安全性の方がよりよいものとして評価されている」ような状況なので個人的にはあまり納得はいっていないのだけど、周辺のツールを含めて「結果としてどうなのか」だけに注目すると「実際に動作させる前に検知できる」のはバグの発見の早期化という意味でよいことだと思う。

これについての Ruby の 2020 年時点での回答は Ruby 3 なので、Ruby 3.0.0 Released など、関連情報を漁ってもらうとよいと思う。少なくとも TypeScript 的なアプローチで静的に問題を検知することはできるようになっている。

エコシステムとしては TypeScript ほどの充実はまだ実現できていないけれど、システム自体は Ruby 2.6 以降でも利用できるので、今すぐ実践投入することもできる。

Rubyの本当の動的さはそっちじゃない

上の例に挙げた String や Integer では不可能だが、PHP や JavaScript のようなふわふわした挙動の演算子のようなものを持った値も以下のように作ることができる。

class AmbigiousValue
  def initialize(val)
    ..
  end

  def +(other)
    ..
  end
end

さらに、上のように普通に class で定義したものについてはインスタンスの状態でメソッドを上書きできるので、あるオブジェクトだけ + の意味が違う、みたいなこともできる。

※ 残念ながら(?) Integer ( Numeric ) や String は継承ツリーに Class を持っておらず、Module に定義されている動的なメソッド定義を実現するメソッド群を持っていないので、Ruby の構文解析を維持したままいきなり 'a' + 1 が 'a1' になるようなコードを作ることはたぶんできない。

もちろんオブジェクト単位で挙動が異なるようなことは「できる」というだけで推奨してる人はたぶんいないけど、この点については自分は以下のようなことだと理解している。

午前4時。あと数時間でユーザが出勤してくる。それまでにシステムをまがりなりにも 動く状態にしておかねば… どうもサードパーティ製のあるライブラリ(ソース無し)の 挙動が怪しいのが問題の原因のようだが、APIにどういうパラメータを与えた時に バグが再現できるか絞りこめていない。もちろんサポートが開くのは明朝、それでは 間に合わないッ。だがッ! ライブラリの内部のみで使われる ある関数に、まれに異常な引数が渡っていることが分かったッ! この内部関数の呼び出しをフックして引数を修正すればとりあえず動かせるッ!—とか、

そういう状況において、「どんなに汚くても、打てる手段がある」というのは 何物にも替え難い救いなのです。というより、そういう予想外の事態に対して エスケープポッドが備えられていない処理系を使うなんて恐くて出来ません。 Lisperは臆病なんです。

Lisp:よくある正解

Ruby が安全なだけの言語でないのは間違いないと思う。

Hanami::Controllerで共通の処理を差し込む

Meetup #100 - Kanazawarb に参加したらちょっと高まったので!

Hanamiは比較的継承が少ない

Hanami のコードの一つの特徴として、「継承を避けて mixin を使いがち」というものがある。もう少し具体的には

パス 実現方法
apps/ Action も View も moulde の mixin
lib/ Hanami::Entity も Hanami::Repository も継承
lib/*/mailers/ mixin
db/migrations/ 単なる DSL

のようになっていて、簡単に言うと Rails のようには

ApplicationController にメソッドを追加したらすべての controller で使える、みたいな素朴な OOP が通用しない

ようになっている。

これはこれで巨大な superclass を作らないようにさせる矯正ギプスとして面白いなと思う反面、mixin が多くなるのは Ruby 力を要求しやすいのでは? という気もしないでもない。

ApplicationControllerの代わりになるもの

application.rb の中の

module Web
  class Application < Hanami::Application
    configure do
      ..
      # Handle exceptions with HTTP statuses (true) or don't catch them (false).
      # Defaults to true.
      # See: http://www.rubydoc.info/gems/hanami-controller/#Exceptions_management
      #
      # handle_exceptions true

      controller.prepare do
        # include MyAuthentication # included in all the actions
        # before :authenticate!    # run an authentication before callback
      end
    end
  end
end

ここ。

Controllerのbefore, after callback

Actions: Control Flow | Hanami Guides

Controller には before / after の callback があって、Symbol を使ってメソッドを呼んだり、直接 block を与えたりすることができる。これは Rails の Controller によく似ていて、この before / after メソッドを 上の controller.prepare block にも与えることができる。

もし ApplicationController の before / after で処理を加えていたのなら、ここに同じように書くことができる。

configure do
  controller.prepare do
    before do
      ..
    end
  end
end

また、この中で self を参照すると実際に request を受けた Controller を取得できる。

例外を拾う

例外を拾う方法は以下のようになっている。

フレームワーク 例外を処理するメソッド
Rails rescue_from
Hanami handle_exception

Rails の ApplicationController で言う rescue_from は Hanami では handle_exception になるんだけど、これも上の before / after と同じように Action に該当する class 内に書いても controller.prepare に与える block 内に書いても同じように動作する。

ただし、development では handle_excepions が false になっているので、

  configure :development do
-   handle_exceptions false
+   handle_exceptions true
  end

に変更しておかないと実際のエラーを目視しながら開発することはできない。これも Rails と同じだと思う。

About

例によって個人のなんちゃらです

Recent Posts

Categories

Tool 日々 Web Biz Net Apple MS ことば News Unix howto Food PHP Movie Edu Community Book Security Text TV Perl Ruby Music Pdoc 生き方 RDoc ViewCVS CVS Rsync Disk Mail FreeBSD Cygwin PDF Photo Zebedee Debian OSX Comic Cron Sysadmin Font Analog iCal Sunbird DNS Linux Wiki Emacs Thunderbird Sitecopy Terminal Drawing tDiary AppleScript Life Money Omni PukiWiki Xen XREA Zsh Screen CASL Firefox Fink zsh haXe Ecmascript PATH_INFO SQLite PEAR Lighttpd FastCGI Subversion au prototype.js jsUnit Apache Trac Template Java Rhino Mochikit Feed Bloglines CSS del.icio.us SBS qwikWeb gettext Ajax JSDoc Rails HTML CHM EPWING NDTP EB IE CLI ck ThinkPad Toy WSH RFC readline rlwrap ImageMagick epeg Frenzy sysprep Ubuntu MeCab DTP ERD DBMS eclipse Eclipse Awk RD Diigo XAMPP RubyGems PHPDoc iCab DOM YAML Camino Geekmonkey w3m Scheme Gauche Lisp JSAN Google VMware DSL SLAX Safari Markdown Textile IRC Jabber Fastladder MacPorts LLSpirit CPAN Mozilla Twitter OpenFL Rswatch ITS NTP GUI Pragger Yapra XML Mobile Git Study JSON VirtualBox Samba Pear Growl Mercurial Rack Capistrano Rake Win RSS Mechanize Sitemaps Android JavaScript Python RTM OOo iPod Yahoo Unicode Github iTunes God SBM friendfeed Friendfeed HokuUn Sinatra TDD Test Project Evernote iPad Geohash Location Map Search Simplenote Image WebKit RSpec Phone CSV WiMAX USB Chrome RubyKaigi RubyKaigi2011 Space CoffeeScript Nokogiri Hpricot Rubygems jQuery Node GTD CI UX Design VCS Kanazawa.rb Kindle Amazon Agile Vagrant Chef Windows Composer Dotenv PaaS Itamae SaaS Docker Swagger Grape WebAPI Microservices OmniAuth HTTP 分析基盤 CDN Terraform IaaS HCL Webpack Vue.js BigQuery Middleman CMS AWS PNG Laravel Selenium OAuth OpenAPI GitHub UML GCP TypeScript SQL Hanami Develop Document Jekyll