let’s just で始まるシステム設計の提案は、たいてい予想より複雑になり、よいアーキテクチャ判断は 経験則 と文脈理解に大きく依存する
- プラグイン構造、API 追加、抽象化レイヤーはもっともらしく見えても、実際には 動作互換性、保守性、セキュリティ、性能、エコシステム要件を同時に背負わなければならない
- 非同期処理、アクセス制御、データ同期は馴染みのある話題に見えるが、製品環境では再現しにくいバグ、セキュリティモデルの再設計、同期の難題 につながりやすい
- クロスプラットフォームとネイティブへの脱出口(native escape)は、単純な初期製品では有効なことがあるが、プラットフォーム機能と内部状態が乖離すると 品質と一貫性 を保ちにくい
- こうしたパターンが常に間違いというわけではないが、多くは不要だったり代替手段があったりし、失敗しやすいアプローチより 第一原理(first principles) から問題を解き直すべきである
let’s just が危険な理由
let’s just の後につく提案は、10回中9回、会議室で想定したより はるかに複雑な作業 になる
- エンジニアリングには社会科学的な性格もあり、何がうまく機能するかは 文脈依存 である
- あるアプローチがうまくいかないと言うと、エンジニアにはすぐ反例を証明したくなる挑戦として受け取られがちである
- エンジニアリング管理とソフトウェアアーキテクチャのかなりの部分は、経験から得た 経験則(rule of thumb) と苦労して学んだ教訓の組み合わせでできている
「とりあえずプラグイン可能にしよう」
- 1つの実装だけでは不十分に見えるとき、同じアーキテクチャに新しい実装を差し込めば、API 呼び出し側を変えなくても改善や新機能を得られるように思える
- しかし「API はヘッダーファイルや文書ではなく 動作そのもの」なので、ただ動く程度のプラグインはほとんど存在しない
- 現代のソフトウェアで最もプラグインに近い構成要素は デバイスドライバ である
- 過去のドライバは動作品質が低く、もはや許容されないか、現代の OS が自前のドライバを構築する方向へ変わっている
- 本当にプラグイン可能な構造を作るには、基本実装と同時に2つ目の実装まで設計する必要があり、それによって少なくとも一度は動くという証拠が得られる
「とりあえず API を追加しよう」
- 製品や会社がある程度成功したあと、「プラットフォームになって開発者を呼び込むべきだ」として API を追加することが多い
- API 提供者は機能追加と 互換性・相互運用性 のあいだで継続的に折り合いをつける必要があり、既存の動作や性能特性のせいで変更の自由度は大きく下がる
- API があるからといって、誰かが必ず使いたがるとは限らない
- 新しい API は、製品がある機能を望んでいるのに、社内の優先順位を十分高く置けないときに生まれることが多い
- 対象市場が小さい、縦割り特化である、あるいは特定ドメインに限定されていて、外部パートナーが API でその空白を埋めてくれることを期待する形である
- そうしたパートナーにも自分たちの事業と顧客がいるため、別の製品を追加導入して問題を解きたいとは限らない
- プラットフォームになることは実際には大きな需要を要する事業であり、API をいくつか提供するだけで第三者の経済的基盤が生まれることはまれである
「抽象化をもう1段入れよう」
- Butler Lampson の「コンピュータサイエンスのあらゆる問題は、もう1つの間接層で解決できる」という言葉には実際の真実がある
- 失敗は主に2つの形で現れる
- 早すぎる段階で入れた抽象化は、実際の利用計画がないまま 過剰抽象化 としてアーキテクチャに残る
- 後から追加した抽象化は、保守、セキュリティ、性能最適化を非常に複雑にすることがある
- Windows NT には、最初から入っていたものの実際には使われなかった過剰抽象化が多くあった
- Mac OS の進化では、最初は奇妙に見えた抽象化が2リリース後に有用になった例があり、その違いは計画の有無だった
- 事後的な抽象化が一部のコードでしか使われないと、新しい抽象化を使わないコードが多く残り、保守負担 が大きくなる
「非同期にしよう」
- コンピュータサイエンス最初の25年のかなりの部分は、非同期動作を理解し実装する問題に費やされた
- 1980年代の大学院課程では、食事する哲学者、プロデューサー・コンシューマー、眠る理髪師のような話題が長く扱われていた
- 今日では多くのエンジニアが、データ層のルールや Web フレームワークのおかげで、非同期問題をかなり抽象化された形で扱っている
- フレームワークやデータ層の外で直接非同期を管理すると、最初はうまくいっているように見えても、1年後に 再現しにくいバグ が現れることがある
- そのバグがデータ破損問題でないことを祈るしかない
「アクセス制御は後で追加しよう」
- アクセス制御の配置は理論的な論争の対象だったが、現在のシステムは継続的な攻撃にさらされているため、環境ははるかに複雑である
- 誰もが最初からセキュリティが必要だと分かっているが、市場投入の速さのためにアクセス制御とセキュリティモデルを最初から完全に設計するシステムはほとんどない
- 顧客と攻撃者の視点から出発しなければ、製品に合った アクセス制御設計 を作るのは難しい
- アクセス制御を後付けする方式は失敗するか、将来製品を書き直す状況につながることがある
- その書き直しは、顧客を含む誰にとってもよい体験にはならない
「データを同期しよう」
- 複数のデバイス、SaaS アプリ、データストアがある環境では、「とりあえずデータを同期しよう」という提案がよく出てくる
- client/server とデータ同期の先駆者である Ray Ozzie が強調したように、同期は難しい問題 である
- コンピュータサイエンスで難しい問題とは、経験でしか学べない挑戦が多く、非常に厄介な問題を意味する
- 完全な意味体系とトランザクションを備えたデータストアでも同期は難しく、blob、非構造化データ、データ変換が入ると難度は急激に増す
- 解決策の土台をデータ同期に置くのはほとんど望ましくない選択であり、同期だけで数十億ドル規模の会社が存在する理由もそこにある
「クロスプラットフォームにしよう」
- クロスプラットフォームは長年繰り返されてきた議論で、誰かが自分の作ったコードはうまく動くと言ったり、Unity とゲームを例に出したりする
- 何かをクロスプラットフォームで作ると言うのは、実質的には OS、クラウドプロバイダ、ブラウザのいずれかを作ると約束するのに近い
- クロスプラットフォームがうまく機能するのは2つの場合である
- プラットフォームが新しく単純なとき。たとえばクラウドがコンピュートと単純なストレージ程度だったとき
- アプリケーションや製品が新しく単純なとき
- 基盤プラットフォームと乖離し始めたり、各対象プラットフォームでまったく異なる形で表現される機能を作り始めたりすると、この条件は崩れる
- Microsoft は Mac 向け Office と Windows 向け Office を同じコードで作るのが難しくなり、1998年に Office のコードをフォークし、その後は戻らなかった
- Microsoft はもともとクロスプラットフォームアプリを作る事業として存在していたが、それは OS API 文書が100ページ程度で、各 OS が CP/M から派生していた時代にうまく機能したやり方だった
- 関連記事: Divergent Thoughts on Cross Platform
「必要ならネイティブに逃がそう」
- クロスプラットフォームは短期間しかうまく機能しないことが多いため、フレームワークや API 抽象化はしばしば ネイティブへの脱出口(native escape) を提供する
- このアイデアは、プラットフォームが進化してフレームワークがまだ公開していない機能を、ネイティブプラットフォーム上で直接呼び出せるようにするものである
- しかし抽象化を提供するフレームワークや API は、内部状態やキャッシュを保持している
- ネイティブプラットフォームを直接呼ぶと、フレームワークが認識していないデータ構造や状態が変更される
- 一部のフレームワークは、脱出口コードとフレームワークのあいだでデータや状態をやり取りする仕組みを提供するが、自動メモリ管理時代に malloc/free のようなアーキテクチャを再導入するのに近い解法である
選べるがデフォルトではない
- こうしたアプローチに常に「ノー」と答えるべきだという意味ではない
- 特定の文脈ではこうしたやり方が機能することもある
- しかし大半の場合、こうしたパターンは不要であったり、もっとよい方法があったりする
- 失敗可能性の高いソフトウェアパターンを先に持ち出すのではなく、第一原理 から問題を解くべきである
2件のコメント
プラグのような場合、必須の動作だけをできるだけ絞り込んでインターフェースを設計するのが最も重要です。
インターフェースをただ現在のコードから大まかに構造を拾って作ってしまうと、当然その実装に縛られる不要なインターフェースになってしまいますが、そういうケースは本当に多いです...
Hacker Newsのコメント
こうしたアイデアの問題は、アイデアそのものよりも、その前に付く**「とりあえずやってみよう」式のアプローチ**や期待値にある
たとえば「とりあえずAPIを追加しよう」というように、APIを製品の「単なる機能の一つ」と見なすなら、「とりあえずUIを追加しよう」と同じくらいの成功率になりそう
良いUIを作るには慎重かつ徹底的である必要があり、その分野の専門家も必要になる
製品の他のインターフェースだからといって違う理由はなく、悪いアイデアか良いアイデアかよりも、雑にやってはいけないことだというのが要点
「とりあえず」と言えるのは、実際にそれを動くようにする責任を持つ開発者だけで、別の開発者がその言葉を言ったら、自分でその仕事を引き受けると申し出たことになっていた
私たちにはうまく合っているルールだった
APIをきちんと作るには、設計と複雑さがかなり伴う
うまく設計されていないと、1回で済む呼び出しをクライアントが何度も行う必要が出たり、APIが紛らわしくて誤って呼び出したり、そもそも使えなかったりする
認証と認可も必要なので、OAuth2を設定するか、少なくとも安全なAPIトークンの生成・保存・検証が必要になる
データも安全に扱う必要があり、性能が悪いとデータベースが負荷に押しつぶされることがある
キャッシュを入れると、キャッシュの無効化と、キャッシュ対応のためのサーバーやプロセスの複雑さが加わる
レート制限がなければ、雑なクライアントがAPIを叩きまくる可能性があり、ドキュメントが貧弱なら実質的に役に立たない
場合によっては複数言語向けのSDKまで提供する必要があり、良いエラーメッセージがなければ、初めて使うユーザーは呼び出しがなぜ失敗したのか分からない
自分にとってうまくいかなかったことを一般化しているが、非常によく似た状況にいる同じような人でなければ、ほとんどそのまま適用できない
一貫して正しい助言は「慎重に考え、正しいことをしようとし、結果を振り返れ」のようにあまりに一般的で、ほとんど役に立たなくなる
そういう話ではブログ記事も本もあまり売れない
氷山の残りの部分を水面上に持ち上げ始めるとき、顧客が怖がらないよう確認しなければならないからだ
そうすれば、可能か不可能かの問題ではなく、成功と失敗のあり方についての記事になる
必要な細部を知るようになると、何を知らないのかも分からないまま「とりあえず」と言うのも難しくなる
ただしDSLについては、ほぼ100%同意する
不要で、かわいく見せようとしているような複雑化に近く、作る資格があるのは、成功したプログラミング言語を作り、失敗を踏まえて第2版や2つ目の言語まで更新してみたが、それでも多くを間違えた人くらいであるべきだ
(1) DSLは時に非常にうまく機能する。https://www.jooq.org/ を参照
(2) Elastic Load Balancerは作業負荷に反応する制御ループであり、この種のものはすでに汎用化された技術である
(3) ほとんどの産業では過少プロビジョニングが蔓延している。https://erikbern.com/2018/03/27/waiting-time-load-factor-and... と https://www.amazon.com/Goal-Process-Ongoing-Improvement/dp/0... を参照
(4) 異常検知は他の項目のように本質的に分散システムの問題ではないが、一度ひどい目に遭った人なら必要だと感じることはある
知的にも難しい分野である
初めてある程度賢いと感じたアルゴリズムは https://scikit-learn.org/1.5/modules/outlier_detection.html#... で、時には奇跡のように機能する
2018年に使っていたCNNベースの埋め込みでテキストに適用したときはうまく合ったが、SBERTではまったく運がなかった
問題を解決し、誰からも文句を言われなかった
最も重要な要因は、どちらも小さかったことだと思う
2つはとても似ていて、コードも再利用した
1つは巨大なフォームを検証するルールを書くためのもので、もう1つはフォームの回答に応じた意思決定ルールを書くためのものだった
良いDSLは、それまでできなかった人ができるようにしてくれる
時間を節約するために作ったDSLは、有用である可能性がずっと低い。実際には時間を節約できない可能性が高いからだ
どちらの場合も、複雑なドメインの振る舞いをプログラムの中に取り込む必要があった
そのため、プログラマーにドメインを教えるか、プログラマーとドメイン専門家を組ませるか、ドメイン専門家にプログラミングを教える必要がある
作業量が多いなら、ドメイン専門家の手に力を持たせるのは魅力的だ
プログラマーは別の仕事ができ、フィードバックループも短くなる
深いドメインなら、プログラマーを学校に行かせて学ばせたいとは思わないだろうし、浅いドメインならもっと安価な人が処理できるかもしれない
DSLには大きな認知的負荷が伴う
ただし代替手段が完全なプログラミング言語を学ぶことなら、より合理的になる
時間節約用DSLは、すでにコードを書ける人がコードをあまり書きたくない場合のものだが、削減幅が小さいので概していまいちだ
そのうちプログラマーが何かを変えようとすると、直感的なコードの代わりにこのDSL全体を学ぶか思い出さなければならなくなる
もっと単純な経験則としては、プログラマー向けDSLは非プログラマー向けDSLより良いアイデアである可能性が低い
SQLクエリを書いてDataGripのようなツールでテストしたあと、それをDSLに変換する方法を突き止めるのに何時間も費やす
JSON式のような「エキゾチックな」SQL機能を使うと、問題はさらにひどくなる
デバッグは「生成されたSQLを出力し、DataGripのような場所にコピーして貼り付け、クエリをチューニングしてから、それを再びDSLにはめ込む方法を探す」という流れになる
とんでもない時間の無駄だ
jOOQの中心的な売り文句は型安全なクエリだが、IntelliJがコード内の文字列SQLを実データに照らして検証し始めたことで重要性は消えた
SQLを直接編集し、データベースでそのままテストする流れのほうが単純に優れている
jOOQはDSLに関する元記事の論旨を補強している
悪い、あるいは失敗に近いと言える人気DSLの例としては、HCL、E4X、XUL、Common Lispの文字列フォーマット言語などがある
HCLはTerraformの設定言語だが、変数の数だけ似た機器をプロビジョニングするというごく一般的な問題が、最初から扱われていなかったのは明らかだった
後から機能を追加しようとする試みもぎこちなく、問題を完全には解決できなかった
E4XはXML操作のためのJavaScript DSLで、単純な場合にはXML操作をより簡潔に表現できたが、すぐに句読点の壁のように読みにくくなり得た
MicrosoftのLINQとも似ていて、内部コードの計算量がどの程度なのかを作者にまったく知らせなかった
結局、このDSLを使うコードは、簡潔さでは劣るが分析しやすい方法で書き直されがちだった
XULはFirefoxブラウザーのchrome拡張のためのUI言語で、Firefox拡張を作る目的には悪くなかった
しかしFirefoxは企業内アプリケーションの基盤技術としても売り込みたがっており、その領域ではかなり力不足だった
単純なことをするにも、多くの小細工や回避策が必要だった
Common Lispの文字列フォーマット言語も同様に、小さな問題には悪くないが拡張性がない
フォーマットの問題によっては非常に奇妙な解決策が必要だったり、そもそも答えがなかったりし、
formatを再帰呼び出しするコードを見ると本当に嫌になる全体として、この種のアプローチで最もよくある問題は、場当たり的でうまく拡張できないことだ
すぐにきちんと解決できない問題にぶつかり、DSLで書かれた大きなプログラムは扱うのが悪夢であることが多い
Lispの上にDSLを載せれば、基盤言語ではなくドメインロジックだけを書けばよい
作業の大半はすでに終わっており、その言語は初日から有用だ
Lisp上のホスト型DSLとして作れば実際に使われ得るのに、なぜわざわざ新しい言語をゼロから作って枯れて死ぬのを見守るのか理解できない
これらの項目にはどれも成功事例が多い
「ほとんど」という言葉を逃げ道にするのは難しい
これは単なる悲観主義と疲れたシニシズムに見える
そういう気分は理解できるし、自分も感じたことがある。情熱的なエンジニアに悪いアイデアを諦めさせるのが難しいときもある
しかし、こうした空気は私には有害に感じられる
無限大や最大/最小境界へ暴走する制御ループ、分散障害から復旧できないキャッシュ、ライブマイグレーション中に壊れた状態、都合の悪い瞬間に過負荷を起こすバースト、世界中の祝日を知らせる偽の異常検知アラート、といったものだ
そのすべてのアイデアの下には、ほとんどの人が過小評価している複雑さの絡まりがある
これは最初に思うより難しいシステムアイデアのリストであり、真剣に取り組むべきで、軽く扱うべきではない
「良さそうに見えるがほぼ絶対に動かない」とぼんやり似て聞こえるが、細部ではまったく違う
難しい問題として扱い、それに見合う投資をすれば、きちんとうまく動くことは普通に達成可能な結果だ
後付けの機能だったり、素朴に簡単だと思ったりすると、よく失敗する
ビジネス上の利点がないのに、エンジニアが事前最適化に没頭する話として読める
業界には本当によくあることで、数時間以内にデプロイできるバックアップを用意し、サーバーを200%過剰プロビジョニングすればコストが10分の1以下で済むかもしれないことに対して、冗長な自動スケーリング宇宙船を設計して作るのが楽しいからだ
こうしたアイデアが妥当なときもあるが、それは必要になった後のことだ
初期プロダクトの段階であらかじめ設計に組み込むものではない
こうした実装が解こうとするスケール、可用性、複雑さを実際に必要とするプロダクトはごくまれだ
ここでは多くの人が例外を見分ける繊細な判定関数を探そうとしているようだが、実は簡単だ
これらのアイデアは自分がやれば素晴らしく、自分より前の愚か者がやれば意図どおりには絶対に動かない
そして時々、その前の愚か者は数か月前の自分自身だ
ここにドメイン駆動設計も追加したい
アプリケーションを事業構造に合わせようとしてビジネス設計を固定してしまうのは、災厄への定石だ
小さな事業や停滞した事業なら問題を感じないかもしれない
しかし事業が成功したり成長したりすると、すでに古くなった業務慣行に縛られた、ひどく説明的な名前のドメインを作ろうとしたことをすぐに後悔することになる
代わりに、何十年も検証されてきたやり方どおり機能レイヤー中心に設計し、可能な限りビジネスロジックは設定、データベース行、ユーザーワークフローに置くほうがはるかに柔軟だ
ドメイン駆動設計の落とし穴として、古い言語と新しい試みに対するコード・システムの再利用性の低さを挙げているが、反対に、ビジネスロジックを設定やワークフローなどにすべて入れた高度に抽象的な設計も、組織全体がその抽象化、設定、無数の組み合わせをかなりよく理解している場合にだけ柔軟だ
その組み合わせはすぐに迷路のように爆発し、人々が依存するようになる、未知で予想外の挙動を生み出す
新しい開発者のオンボーディングや開発チーム交代のコストも耐えがたいものになる
組織は互いに異なる二つの言語を話すようになる
一見単純な機能要求の大半は、抽象化を壊せば巨大なシステム再設計になるか、「今はより安全で小さな変更に見えるように、この抽象化をそのままハックしよう」になる
前者は、システム全体の挙動とコードベースを完全に理解している優れたエンジニアと、優れたエンジニアリング慣行・プロセスがあっても常に非常に難しく、数か月や数年かかることがある
後者のほうがより頻繁に起こる。だから「高度に抽象化され、機能レイヤー化され、設定ベースで、ビジネスロジックが創発する」プロジェクトは、最初は完璧で柔軟に見えるが、最後には「これはいったい何なんだ」になる
システムが実装された後は、その創発したビジネスロジックが全員の話す言語になる
組織が互いにまったく折り合えない二つか三つの言語を話すと非常に苦痛であり、上下左右にその間を流暢に翻訳できる人が複数いないなら、むしろドメインをもっと近く表現すべきだったと感じることになる
ある状態を型で表現不可能に設計するなら、その設計の寿命のあいだ、その状態が本当に不可能な状態だと確信できなければならない
負荷に反応する制御ループの項目はよく理解できない
多くのシステムの基本的かつ根本的な構成要素だ
1800年代の蒸気機関や1900年代のVictrolaレコードプレーヤーの遠心調速機も、負荷反応制御ループだ
電子工学全体が負荷反応制御ループの網であり、自動車のオートマチックトランスミッションも同じだ
CPU使用率は興味深い例だ
たとえば、クロスリージョンのロードバランサーがプロセス内の負荷遮断と衝突する状況が見られる
ロードバランサーの信号が負荷遮断を反映していないか、反映の仕方が不正確だからだ
もう一つの問題は、サービスローカルな結果を最適化しようとする制御ループが、全体の結果を損なう場合だ
全体としては、少数の制御ループを影響力の大きい位置に置くほうがよいと思う
CPU負荷には、https://arxiv.org/abs/2312.10172で説明されているように、いくつかの問題がある
これらの問題には共通するパターンがある
どれも、プログラマーに馴染みのある逐次的なデータ加工プログラミングモデルに制約を追加する横断的関心事である
制約を追加するたびに、その後すべての開発者がそのシステムで今後開発する間、考え続けなければならないことが増える
システムが過度に制約され、一部の制約を緩めなければ前に進めない状況に陥りやすい
不可能ではないとしても遅くなる
新機能が、すでにサポートすると約束した API、セキュリティ、同期、レイテンシ、他のプラットフォーム、ネイティブコードとどう相互作用するのかを、開発者が毎回考慮しなければならないからだ
だから、これらの特性をすべてサポートすることも可能ではある
たとえば透過的なデータ同期をプラットフォームの中核的な価値提案に据えれば、それ以降のすべての開発はそれを優先的にサポートし、その制約の中で可能な機能セットを発展させていくことになる
その機能セットがユーザーの望むものと正確に一致するとは限らないが、それがサポートできる範囲である
製品は、その特性を購入判断の最優先に置く顧客にとって魅力的に見える
複数の DSL、P2P キャッシュ、混合並列性を使うプロジェクトをやったことがあり、どれも動作した
作るのも本当に楽しかった
1つの例外を除けば、良い投資だった
P2P キャッシュは必要ではなかったため、結局あまり回収できなかった
したがって、こうしたものがほとんど絶対に動かないと言うのは明らかに間違っている
複雑ではあるが、その複雑さは他の方法では得にくい機能をもたらす
P2P キャッシュの例から得た教訓は、その機能が本当に必要かをまず確実にすべきだということだ
これらのアイデアのかなり多くをうまく実行したことがあるので、少し奇妙に読める
「とにかくデータを同期しよう」は、私にとってつらい日が存在する理由だ
「インターネット規模」のようなものを念頭に置いてキューやイベント処理などを追加したシステムをあまりにも多く見てきたが、実際の自然な範囲はその閾値よりはるかに低い
こうしたチームはナイーブなのか、最悪の場合、エンジニアリングを知らない経営陣を利用して、面白半分でこういう問題をいじるための資金を引き出しているのだ
ちゃんとやるなら、キューとイベント処理は必須だ