My Approach to Building Large Technical Projects

Mitchell Hashimoto

大規模な技術プロジェクトへの私なりのアプローチ

原文は Mitchell Hashimoto により に公開されました。 このブログを購読する

ゼロから新しいプロジェクトを立ち上げるにしても、大きな機能を実装するにしても、大規模なリファクタリングを始めるにしても、モチベーションを保ちながら大規模な技術プロジェクトをやり遂げるのは簡単ではありません。私にとって特にうまくいく方法は、常に実際の成果を目に見える形で確認しながら、それを基準に作業の順番を決めていくことです。

誰もが、新しいプロジェクトを始めるときのあの高揚感を経験したことがあるはずです。最初の数週間は、パソコンに向かって作業するのが待ちきれません。しかし時間が経つにつれて、だんだん気が散ったり、言い訳を作って作業から遠ざかったりします。これが仕事であれば、無理やり自分を奮い立たせてなんとかゴールまでたどり着くものの、毎日が苦痛です。趣味のプロジェクトなら、数年後に振り返って「あのとき続けていれば」と思うことになります。

大きなタスクを、目に見える前進が実感できる単位に細かく分割すれば、最後までやり遂げやすく、プロジェクトを通してワクワクした気持ちを保てることに気づきました。人によってモチベーションの源泉はさまざまなので、この方法が誰にでも合うとは限りません。ただ、あえて大ざっぱに言えば、良いデモを見て興奮しないエンジニアに私は出会ったことがありません。そして目標は、常に自分自身に良いデモを見せることなのです。

この記事で書くことに目新しいものがあるとは思っていません。よく知られたソフトウェアエンジニアリングやマネジメントのプラクティスのさまざまな側面と共通するところも多いはずです。ここでは、単に私が大きな技術的作業にどう取り組んでいるのか、そしてなぜそうしているのかを共有したいと思います。

この記事全体を通して、具体例として私のターミナルエミュレータープロジェクトを取り上げます。実際に経験した具体的な話ができるからです。他にも例にできるプロジェクトはたくさんありますが、仕事とは直接関係がなく、記憶にも新しいという理由でこれを選びました。

はっきりさせておきたいのですが、プロジェクトを完遂できない人を非難するつもりはまったくありません。楽しめていて達成感があるなら(あるいはそもそも気にしていないなら)、それは素晴らしいことですし、ぜひそのまま続けてください。この記事は、もっとプロジェクトを完遂したいと思っている人や、単純に私がどうやって完遂を目指しているのか知りたい人に向けて書いています。


スタートライン

最初に大きなプロジェクトがあるとき、まずどこから始めるかを決めなければなりません。私にとってこれが一番難しい部分で、正しい出発点を決められずに何時間、ときには何日も悩むことがあります。

私のターミナルエミュレーターの場合、完成させるために必要になるであろう大きなコンポーネントがいくつもありました。ターミナルのパース、シェルプロセスの起動と管理、フォントのレンダリング、グリッドのレンダリング、入力(キーボード/マウス)の処理などです。「完成」までの道のりには、比較的規模の大きなサブプロジェクトが何百と存在します。

もし最初の目標を「Neovimが動く、起動可能なターミナルを作る」にしていたら、大変なことになっていたでしょう。未知の未知を差し引いても、この目標は大きすぎると感じます。GUIのレンダリング、プロセスの起動、ターミナルのパースや状態管理など、その道のりに多くのコンポーネントがあることは直感的にわかります。これは悪い目標です。大きすぎて、1〜2ヶ月もすれば興味を失っていたはずです。

そうではなく、できるだけ早く結果を目にできるような、現実的なプロジェクトは何かを考えるようにしています。このフィルターをかけると、候補となるサブプロジェクトの数は劇的に絞られます。例えば次のようなものです。

  • VTパース - ターミナルのエスケープシーケンスを解析する
  • 空ウィンドウのレンダリング - ウィンドウを開いて空のキャンバスを描画する
  • 子プロセスの起動 - bashやzsh、fishといった子シェルを起動し、TTYをセットアップして、その出力を読み取れるようにする(つまり最初のシェルプロンプト)

この段階では、大きなサブプロジェクトをすべて洗い出そうとはしません。プロジェクトがどんな大まかな形になるかをなんとなく把握し、その中で独立して構築でき、かつ何らかの実際の成果を目に見える形で確認できるものを見つけます。

このフェーズが、経験が最も役立つところです。経験豊富なエンジニアほど、プロジェクトがどんな大まかな形になるかをより的確に描けます。さまざまなサブコンポーネントをより正確に特定し、それらがどう組み合わさるかを見通せます。経験が浅い場合や、馴染みのない領域では、私はとりあえず最善の推測で進め、いずれ作業を捨てることになる可能性が高いと覚悟しておきます。


初期の成果

初期の作業は目に見えにくいことが多く、そのせいで具体的な成果を実感するのが難しく感じられます。例えば、ターミナルでVTパースに取り組む場合、何らかのUIとつなぎ込まなければ、その動きを目で見ることはできません。別のプロジェクトでデータベーススキーマや最小限のAPIに取り組む場合も同様で、CLIやGUIを備えたクライアントを書かなければ成果を確認できません。

最初に選んだサブプロジェクトがUIであれば、もちろんすぐに成果を目にできます!ただ、私はいろいろな理由からフロントエンドから始めることはめったになく、たいていバックエンドから始めます。いずれにせよ、どのプロジェクトでもいずれはバックエンドに行き着き、同じような課題に直面します。

このフェーズを乗り越えるための最良のツールは、自動テスト(この段階では通常ユニットテスト)です。自動テストがあれば、実際にコードを実行して動いていることを確認できますし、良い習慣にもなります。

これは最初のいくつかのタスクを選ぶ際のもう一つの指針になります。グラフィカルなものでないなら、あまり手間をかけずにテストでき、成果を目にできるものを選ぶとよいのです。

私のターミナルでは、まずVTパースから始めることにしました。当時あまり詳しくなかったターミナルの領域であり、しかも非常にテストしやすそうだったからです。文字列としてサンプル入力を与え、出力としてパースされたアクションやイベントを期待すればよいのです。

「1件パス」「4件パス」「13件パス」とテストが通っていく過程を見るのは、私にとってすごくワクワクすることです。自分が書いたコードを実行して、それがちゃんと動いているのです。そして、より大きなプロジェクトの重要なサブコンポーネントが着実に前進していることもわかります。


デモへ向けてスプリントする

初期のサブプロジェクトにおける私の目標は、完成されたサブコンポーネントを作ることではなく、デモへとつながる道のりで次に進めるだけの十分なサブコンポーネントを作ることです。✨

このトレードオフは機能面だけに現れるわけではありません。アルゴリズムや設計上の判断にも現れます。例えば、将来的には本物のデータベースや凝ったデータ構造、ストリーミングデータへの対応が必要になるとわかっていても、最初の作業ではインメモリのデータや辞書のような組み込みのデータ構造を使い、入出力をすべて事前に用意する形で済ませてしまえばよいのです。

これは重要なトレードオフだと思うので繰り返します。完璧を進歩の敵にしてはいけません。さらに言えば、将来必ずやらなければならないとわかっている改善があったとしても、それを理由に次へ進むのを止めてはいけません。目標はデモにたどり着くことです。

何に取り組んでいるときでも、前章で説明した自動テストによるフィードバックと並行して、週に1〜2回はデモを作るようにしています。

デモを作ることは、プロダクトに関する貴重なフィードバックももたらしてくれます。完全に機能していなくても、何かが手触りとして良いかどうかを素早く直感できます。これは「minimum viable products」ではありません。実際にはまだ実行可能とは言えないからです。ただ、エンジニアが価値を感じられる自己省察をするには十分なものなのです。

これは経験がむしろ足かせになると思っている領域です。完璧なものを作ろうとして足を取られ、デモができた頃にはそれがイマイチだと気づくベテランエンジニアを何人も見てきました。実装がダメなのではなく、プロダクトや機能そのものがダメなのです。

ターミナルの例で言えば、最初に選んだタスクはVTパースでした。初期段階では、自動テストが通るのを確認するだけでした。最初のデモにたどり着くために、コマンドを実行して出力をキャプチャし、それをVTパーサーに食わせて、パースできたもの(あるいはできなかったもの)をすべて出力するシェルスクリプトを作りました。やがて、このCLIを最初の「UI」として反復的に改善し、ターミナルのグリッドをASCIIでレンダリングするようにしました。

これで、manlsのような単純なプログラムから、vimのようなより複雑なプログラムまで実行して、自分のパーサーが動くのを見る(あるいは壊れるのを見る、それはそれでまた違った興奮があります)ことができ、大きな満足感が得られました。

このケースで私が書いていたCLIは、長期的にはほとんど役に立ちませんでした(結局すぐに捨ててしまいました)。しかし、デモとしてそれを作るのに費やした1〜2日が、重要な前進の感覚をもたらし、何かが動くのを目で見ることがモチベーションの維持に役立ったのです。


自分のために作る

この章は、仕事で割り当てられたプロジェクトよりも、個人プロジェクトにより当てはまります。他の人に向けてソフトウェアをリリースしたい場合でも、必要なものを、必要になったときにだけ作りできるだけ早く自分のソフトウェアを使い始めるようにしてください。

私は、自分自身が直面している問題に取り組むときの方が、常にモチベーションが高まります1。そして、自分用に設計したプロダクトが自分にとって使い物にならなければ、他の人にとってもうまく機能しない可能性が高いのです。だからこそ、デモから実際に使えるプロダクトへ進む道筋として、私は自分が必要だと思う機能だけを作る最短ルートを探します。

私のターミナルの場合、それはまず自分のシェル設定(fish)を読み込めるようにし、そこからNeovimを起動して使えるようにすることを意味しました。そこで私は、そのために必要な機能だけにまっしぐらに絞って作業しました。つまり、それらのプログラムが使うエスケープシーケンスだけ、普段使っているフォントのレンダリングだけ、といった具合です。初期段階で省いた機能の例としては、スクロール、マウス選択、検索、タブ/スプリットなどがあります。

そして、自分のターミナルを日常使いし始めました。このステップではたいてい何度か躓きます。省いたり忘れたりしていた機能が実は必要だと気づくからです。私の場合、最初の試用で矢印キーが何も反応しないことに気づいたり、微妙な(しかしワークフローを破綻させる)レンダリングのバグがあったりしました。そこで一旦使うのをやめることになるのですが、次に取り組むべき具体的なタスクが明確になります。

さらに、自分が書いたコードで動くソフトウェアを使うことにはいつも大きな誇りを感じますし、それが作業を続けるモチベーションの維持にもつながります。


まとめ

  1. 大きな問題を小さな問題に分解します。重要なのは、小さな問題それぞれに、作業の成果を明確に確認できる方法があることです。

  2. 小さな問題は、大きな問題のデモに関わる部分を前進させられる程度にだけ解き、すぐに次の小さな問題へ移ります。

  3. ソフトウェアの動くデモを作り始められるだけの数の小さな問題を解いたら、そこからさらに機能を追加しながら反復していきます。できるだけ頻繁にデモを作ります。

  4. 可能であれば(個人プロジェクトや、自分自身が抱える問題を解決する仕事のプロジェクトなど)、自分のソフトウェアを自分で使い始められるようにする機能を優先します。そしてまずは自分の問題を解決し続けます。

  5. 必要に応じて各コンポーネントに戻り、将来の改善のために反復します。このプロセスを必要に応じて繰り返します。


おわりに

これでほぼすべてです。私は個人プロジェクトでも、グループプロジェクトでも、仕事のプロジェクトでも、学校のプロジェクトでも、この大まかなパターンに従ってきました。そしてこれが、私がモチベーションを保つ方法です2

触れなかったこともたくさんあることにお気づきでしょう!例えば出荷(shipping)については語っていません。出荷することにモチベーションを見出す人も多いのは知っています。でも、プロジェクトが成功するために必ずしも出荷する必要はないと私は考えています。私にとって、出荷は長期的なモチベーションにつながるには大きすぎるイベントなのです。ツール(GitワークフローやCIなど)についても触れていません。私は複数の職場でこのプロセスを使ってきて、どんな既存のプロセスにも適応させてきました。その他もろもろです。

これは、このプロセスがいかに個人的なものであるかを示していると思います。誰もが、健全な形でモチベーションを支える何らかのプロセスを見つける必要があると考えています。私は、成果を目にすることが自分を強く動機づけるのだと気づき、それを軸に仕事のスタイルを築いてきました。そして今のところ、それはうまく機能しています。

脚注

  1. だからこそ、私は自分が使いたいと思うプロダクトを作ったり売ったりしている会社でしか働かないようにしてきました。個人的な選択です。

  2. 皮肉なことに、私の好む学習方法はリファレンス資料を最初から最後まで通読することであり、これは何かを作るときのアプローチとはほぼ正反対です。

この記事は「muse-spark-1.2-contributor」を使用して翻訳されました。

コメント