はじめに
京都オフィス在籍で、AI・機械学習チームの山本(@hiro_o918)です。
このブログはサテライトオフィスのメンバーで投稿されるブログリレー 3 日目の記事になります。
関西在住だとオフラインでのコミュニケーションに悩みがちなのですが、
サテライトオフィスがあることでリアルでチームメンバーと飲む会える機会が増えて、とても楽しいです。
2 日目は福岡オフィスの氏家さんによる「エムスリー福岡 Quine を作りました!」でした!
Quine という概念をこの会社に入社してから知ったのですが、なかなかギークな遊びだなと感じています。 ぜひ読んで見てください!
さて、実は中途入社して 4 ヶ月、先日初めての評価フィードバックがありました。 内心ドキドキしていましたが、自分でも前々から得意だと思っていた「初見のコードベースでも素早く目的の変更を行える」という点を実感してもらえている旨のフィードバックを頂くことが出来ました。 新しい環境に入った直後でもこのように言ってもらえたので、この点には少し自信を持っています。 定量的にも、次の記事にあるような社内 OSS の変更を入れたり、他チームのサーバーに対して新規に機能を追加したりと、 目的に対してそこまで迷わず変更を加えることができていたなと感じています。
なかなか大きな声で何かを得意と言う機会は少ないですが、フィードバックを受けて自分の中でもチョットデキルかもと思えているタイミングなのもあり、 今回は「初見のコードベースでも、素早く目的の変更を行える」と思えるように至るまでの経験や考え、Tips を共有したいと思います。
前提となるマインドセット
まずは抽象的な観点ですが、私はマインドセットとして「どうせ全部コードなのだから読めば大体わかるし、変更も可能」という考えを持っています。 サードパーティのライブラリも、他チームの API も、結局は何かしらのコードが動いています。 言い換えるとプログラマである我々は、業務で利用するほとんどの処理部分でその一次情報である実装までたどり着くことができます。 もちろん社外の API は直接実装を見ることはできませんが、入出力のお約束については OpenAPI やドキュメント、クライアントコードを見ることで必要な内容は把握できます。
よって、プログラマにとってブラックボックスである範囲は少なく、追いかけようと思えばどこまでも追いかけることができると思っています。 この前提で開発に取り組んでいると、自分が普段扱っているコードベースでは解決が難しかったことが、他のコードベースの少しの変更で解決できることもしばしばあることが見えてきます。
具体的な Tips
マインドセットで述べた内容は、「それはそう、でもどうやる?」という疑問が残るかもしれません。 この手の内容は最初のハードルが高いですが、一方で一度成功体験を積むことができれば、自然と繰り返しの改善がされていくものだと思っています*1 。 そこで、自分の経験をもとに具体的な Tips をいくつか紹介したいと思います。 もし気になったものがあれば、日々の開発で取り込んでいただけると嬉しく思います。
コードを読まずに理解する技術
まず初めにコードを読まずに理解する技術について述べます。 よく言われることではありますが、ロジックを理解するためにコードベースすべてを理解する必要はありません。 読まない技術を身につけることで、大きなコードベースでも臆せず情報を探すことができます。
とりあえず Clone する
お気に入りのディスプレイやキーボードと同じく、コードを速く読むためには環境を整えることが重要です。 自分のコードベースであるかどうかに関わらず、内部実装を確認したいものや境界面のコードベースについては、とりあえず Clone しておくことをオススメします。
また、そこまで参照頻度が高くなさそうというものについては、GitHub では .
を押すとブラウザ上の VSCode が開くので、そちらを利用してみるといいかもしれません。VSCode のキーボードショートカットや全体検索が手軽に利用できて便利です。
なお GitHub のブラウザの方も現在は発展しており、画面内で定義ジャンプも可能です。
インタフェースで理解する
最近チーム内で「脳に収まるコードの書き方」という本を読んでおり、この本でも関連した内容が出てきますが、 コードを読むうえで重要なのは、エントリーポイントから潜っていき自分の関心のある粒度のレイヤーにたどり着くことです*2。 このためには、自分が課題に感じている部分について、自分のコードと外部のコードとの境界面において関心のある変数を起点とし、実装ではなくインタフェースの型を追っていくことが重要です。 IDE の定義ジャンプ機能や変数ジャンプを利用することで、目的のコードを発見しやすいので、これをどんどん活用していきましょう。
from thirdparty import read_csv read_csv(file_path: str, sep: SepEnum) -> list[dict[str, str]]: ...
例えば上記のようなサードパーティの関数があったとき、実装は読まなくても CSV ファイルを読み込むことができることがわかります。
ここで、セパレーターとして |
を使いたいという要件があったとき、SepEnum
の enum 定義を見ることで、どのような値を受け付けるかがわかります。
|
の受け付けができない場合は、read_csv
内のデータのパース部分のコードを探索することで、目的の変更するための情報を得ることができます*3。
パース部分のコードには sep
の変数が渡されているであろうということも予測でき、SepEnum
の型情報も合わせて探索時のヒントになります。
ここまで、実際のコードは何も示していませんが、関数のインタフェース、変数の型、命名だけを記述した 1 行のコードから実装や探索のヒントを得ることができます。
テストコードで理解する
テストコードは、コードの挙動を理解するための最も効果的な手段の 1 つです。 特に業務でよく利用するような OSS の場合、テストコードが書かれていることが多いです。
先ほどの、read_csv
はシンプルな内容なので CSV ファイルから
[ {"name": "Alice", "age": "20"}, {"name": "Bob", "age": "30"} ]
といったデータを読み込む関数だと想像はできますが、一方で複雑なロジックのものだと、関数名からだけでは挙動を推測するのが難しくなります。
そこで、テストコード側を見ることで、どのようなデータが想定されているのか、どのようなエラーが想定されているのか、どのような挙動が期待されているのかを知ることができます。 IDE の機能を使うことで、実装箇所を利用している場所を探すことができるため、テストコードを見つけるのも容易にできます。
慣習名で理解する
これは主にプロダクションコードを読む際に有効な手法です。 DDD やクリーンアーキテクチャー、デザインパターンに関する慣習名を知っておくことで、コードの意図を推測できます。 完全に理解してこれらの概念を実装に反映というのは中々難しいと感じていますが、キーワードや各コンポーネントの役割、他のコンポーネントとの関わり方を知っておくことで、 コードリーディングの際に内容理解のショートカットを行うことができます。
コードの詳細を理解する技術
ここからは、関心のあるコードにたどり着いた後、そのコードの詳細を理解するための技術について述べます。
デバッガを使う
デバッガは、コードの挙動を理解するための最も効果的な手段の 1 つです。 ブレークポイントを設定し、どのような変数が存在し、どのような値を持っているのかを確認できます。 デバッガの強い点として、自分のコードベースだけでなくサードパーティコードにもブレークポイントをうち、変数を追いかけることができます。 これを通じて、外部のコードベースでも詳細な挙動を理解できます。
とりあえずサンプルコードを書いてみる
少し違う観点ですが、コードの挙動を理解するためには、とりあえずサンプルコードを書いてみるという手法も有効です。 特に、ライブラリの使い方を理解する際には、公式ドキュメントを読むだけでは理解が難しいことがあります。 そのような場合、自分でサンプルコードを書き、先程挙げたデバッガも使いながら挙動を理解することで、ライブラリの使い方を理解できます。 もしくは、テストコードを書くことでサンプルコードの役割を果たすこともできます。
また、普段の業務で使っている技術の周辺分野のサンプルプロジェクトを一度手を動かしてみて作って見ることは有効だと思います。 例えば、Web 系の会社であれば、フロントエンド、バックエンド、CI/CD、インフラ、データ基盤、分析など、 周辺チームの活動も含めた簡単なアプリケーションを作ることは有益です。 最もレバレッジが効くのは、理解度 0 の部分が埋まっていく段階だと思うので、最新のプラクティスに従わなくとも、一度 0 -> 1 で作ってみることは経験になり、自信にもなると思います。 Docker を使えばローカル環境でも様々な技術を使った開発を用意できるので、触ったことがない領域で気になるところがある方は試してみるといいのではないでしょうか。
分からないなら聞く
当然に思うかもしれませんが、ロジックやコードの詳細については分かる人に聞くというのも重要です。 人に聞くことで、何をやりたいかのメンタルモデルを作ることができ、コード理解のスピードも格段に上がります。 また、質問を通して、自分が見落としていた部分や、他の視点からのアプローチを知ることができるので、コードの理解を深めるためには有効な手段です。
チーム内外だろうと一度聞いてみると、快く教えてくれることが多いので、悩みすぎる前に聞いてみるといいと思います。 特にチーム外だと、他の開発メンバーとの関係値にも繋がるので、個人的にカジュアルなコミュニケーションは大事だと思っています。
追加で、OSS だとしても Issue や OSS コミュニティが公開している Slack で質問するというのも有効です。 答えがくれば儲けものぐらいな気持ちで、気軽に質問してみるといいと思います。 特に、比較的大きい OSS は Slack の Workspace を用意していることがしばしばあり、そこの質問チャンネルを活用すると想像以上に返事をしてくれるのでオススメです*4。 実際、ドキュメントや事例の少ない OSS を利用した場合には、Slack を通じて問題を解決した経験が何度もあり非常に助かりました。
初見コードに安全に変更を加える技術
最後に、初見コードに安全に変更を加える技術について述べます。 基本的には、「コードを読まずに理解する技術」で述べた内容が実践できるコードを書くことになります。
レビュアはたとえコードベースには詳しくても、あなたが追加したいロジックについては未知であることが多いです。 特に、チームの境界をまたぐような変更する場合はこのような状況が想定できます。 そのため、読み手が詳細なコードを読まずとも、まずはロジックを理解でき、メンタルモデルを作れるようなコードを書くことが重要です。
インタフェースを明らかにする
まず、変更のインタフェースを明らかにすることが重要です。 言い換えると、レビュアには、何を入力としてどのような結果を得たいかというのを明確に伝えることに重きを置くということです。
これを達成するための手段はいくつかあると思います。
Stub としての実装を用意する
詳細なロジックに入るまえに、まずはインタフェースを確認するためのスタブを用意しレビュアに投げます*5。 そこで最低限のやりたいこと、入力と出力を確認してもらい、問題がなければ実装に入るという手法です。 この際、先にスタブだけマージしておくことで、レビュアには詳細のロジックのレビューに集中してもらうことができます。
例えば先程のコードを実装するのであれば、一旦次のコードだけでレビュー依頼を出すようなイメージです。
def read_csv(file_path: str, sep: SepEnum) -> list[dict[str, str]]: """CSV ファイルを読み込み、dict のリストを返す Args: file_path (str): ファイルパス sep (SepEnum): セパレーター Returns: list[dict[str, str]]: CSV ファイルの内容。キーがカラム名を示す。 """ raise NotImplementedError()
このレビュー依頼を出す時点で、例えば list[dict[str, str]]
という戻り値はどうなの? というツッコミが入るかもしれません。
実装後であれば、大きな手戻りになりますが、この段階のレビューですり合わせるのであれば、後々の手戻りを防ぐことができます。
Design Doc を書く
比較的大きい変更の場合は Design Doc を事前に用意するのも有効です。 これは自分のコードベースの変更も含めて、便利な手法だと思います。
Design Doc にも様々なフォーマットがありますが、私は次の内容にフォーカスしたものを書くことが多いです。
- 何を実装したいのか
- 何を実装しないのか
- コンポーネントとして何が存在し、入出力としてどう関わり合うか
- 最終的に生まれるアウトプット・アウトカムは何か
こうすることで、各種ステークホルダーとの共通認識を取りつつ、自分の中の実装面の課題も整理できます。
テストを書く
タイトル通りですが、変更内容に応じたテストを用意しましょう。 レビュアもテストを通じて、コンセプトと実装の整合性を確認できます。 さらに、変更を加える側としてもテストがあることによって、自信をもってレビュー依頼を出すことができます。 テスト無しでレビューを通した後、共通の開発環境を用いて確認しようとなると、修正のたびにレビュー依頼をすることになるかもしれません。 最低限、テストを通じて動作保証をすることで、このような手間も省くことができ、スムーズにリリースへ進むことができます。
また、普段から利用するコードベースについては、テストが書きやすいように設計することも重要です。 テストが充実している、テストのための DI がしやすくなっているなどの設計を意識することで、新しいメンバーや他のチームからの開発者の参入障壁も下げることができます。
まとめ
改めて自分の普段の行動をまとめてみて、コードもまた1つのコミュニケーションの手段だと感じました。
プログラマとして達成したい価値を実装に落とし込むという過程において、
- 素早く書き手の意図を読み取ること
- レビュアにわかりやすい変更を提案すること
- ステークホルダーにコンセプトを伝えること
これらすべては関心のレイヤーの違いであって、目的を同じくするコミュニケーションの一環だと思いました。
Tips でも示したとおり、コミュニケーション手段としてコードにこだわる必要もありません。 Design Docs などの非同期コミュニケーションや MTG やモブプロなどの同期コミュニケーションを活用することもまた、コードを読み書きするのと同じような活動だと捉えられると思います。
自身の境界を広げつつ、これらの取り組みを繰り返すことで、思ってもいない近道やアイデアに繋がるかもしれません。 私自身、まだまだ改善の余地が多いですが、これからもトライアンドエラーを繰り返しつつ、このような取り組みを続けていきたいと思います。
We are hiring !!
なんか気づいたら文量が多くなってしまいましたが、サテライトオフィスでも同期・非同期コミュニケーションを活用して、日々楽しく開発をしています! 勉強会や雑談の場も豊富で、技術的な話から日常の話まで幅広く話せる環境です! エムスリーでは福岡、京都、大阪でもソフトウェアエンジニアを積極採用中です! 福岡、京都、大阪オフィスでの働き方などが気になる方はカジュアル面談も実施していますので是非ご連絡ください!
エンジニア採用ページはこちら
カジュアル面談もお気軽にどうぞ
インターンも常時募集しています
*1:実際、普段業務で利用している OSS を眺めているとチームメンバーのコメントや PR を見つけるので、慣れてみると自然な行動になってくるんだなと感じます。
*2:もちろん、該当レポジトリすらわからない、エントリーポイントそのもの見つけることが難しい場面もあります。このような場合は組織全体のコード検索やプロジェクト全体の検索を利用しましょう。API であればパス情報など、全体でユニークになりそうなワードで検索するのがエントリーポイントを見つけるコツです。
*3:ここでデータのパース部分があるだろうというのはこの関数名と引数の内容からの推測になります。
*4:運営・質問に答えてくれるコミュニティの方には感謝しかありません。
*5:型情報は確実に付与しましょう。型も意図を使える偉大な情報源です。