エンジニアリングの心得
MeDiCU では医療者や医学部生などエンジニアリングを専門としていないメンバーがコードを実装することがよくあります。そういったメンバーに日々伝えている心得をまとめました。問題の解き方、バグへの対処、コミュニケーションの3つのカテゴリで合計9個のセクションにまとめました。
あえて細かなスキルよりも長期的に少しずつ伸ばしていくべきスキルをまとめています。各セクションとも深く理解するにはテキスト量が十分ではないので、仕事を進めながら繰り返し読み返したりメンターや同僚と議論したりしながら理解を深めていってください。この記事を定期的(数ヶ月~半年程度に)一回読み返すことで「前に読み取れていなかったことが読み取れた」と感じることができることを意図してまとめました。
1. 問題の解き方
1.a 選択肢を出して選ぶ
あるタスクをアサインされてそれを解くとき、必ず複数個の実装方法を出すようにしましょう。そしてその複数個の選択肢から様々な情報収集や思考を行うことで、少しずつ絞り込んで行くと良いです。「すぐに最適な実装方法がわかった」というときはむしろ全ての選択肢を正しく吟味できてないことを疑うべきです。これは実装方法以外でも調査/命名/解析/ライブラリ選定など色んなところで出てきます。
復習パターン
はじめからこれが十分にできるようになるのは難しいので、自分なりに考えてみるチャレンジをしたあとの復習が大事です。メンターや周囲の人と議論をして違う視点が出てきた場合はそれが何であるかを確認しましょう。
- そもそも選択肢として思いついていなかった場合
- なぜ思いつけなかったのかを考える
- 何を知らなかったからか?
- どこを深堀りしなかったからか?
- 思いつけていたとしたら選べていたか?
- なぜ思いつけなかったのかを考える
- 思いついていたが選択しなかった場合
- 判断基準のどこに差分があるのかを考える
- どんな前提が違ったか?
- 例: 今はコード品質より実装速度が大事だった
- 自分のロジックになにか誤りがあるか?
- 例: 単純にAかつBならCなのに B が偽だった
- 何らかの想定値が異なるか?
- 例: 素朴な書き方でも実行速度は誤差レベルだった
- どんな前提が違ったか?
- 判断基準のどこに差分があるのかを考える
知識だけのせいにしない
うまく解けなかったときに、「知識が足りないからだ」と感じることはよくあるでしょう。実際それが正しいことも多いという実感がありますが、それが 「出す選択肢の精度」 に起因することか、「選択肢の選び方」 に起因することかの区別をしてみましょう。この区別をしていくと、「解ける知識を持っていたが解けなかった」ということにも気付けるようになります。知識や経験だけに自分の改善ポイントを求めず、「解き方自体」のスキルにも目を向けていきましょう。
より詳しく考えたい人には『技術の創造と設計』の 3 創造学のすすめにヒントになることがたくさん書かれていておすすめです。ここではこのセクションの根幹になるメッセージと共通する1文を紹介します。
「選択・決定のない設計は設計ではない」と言っても過言ではないのである.
『技術の創造と設計』畑村洋太郎著、岩波書店、2006年、p. 143
1.b 一次情報を見る体力を得る
ここで言う一次情報とは公式ドキュメント、論文、RFCなどのことです。
Zenn や Qiita でも調べられる
プログラミングしたての場合調べることがたくさんあります。例えば下のような基本的な内容の場合を考えます。
- SQLを書いていて2つの日付の差分を取る方法が知りたい
- JSON とは何であるかわからない
ここで、Zenn, Qiita などのサイトにいくと今すぐコピペして使えるようなコード片がたくさん見つかるでしょう。
例えば下のような感じです。
おそらく「今すぐ知りたい情報を高速に得る」という目的においてはこれらのページが最適でしょう。しかし、プログラミングをしていると割とすぐにこういったわかりやすいページでは解決できない問題に出くわします。
そのような場合に備えて 「一次情報を使って問題解決をする体力」 を鍛えておく必要があります。
一次情報を見に行く
例えば日付の計算であれば BigQuery のドキュメント を見に行くと良いでしょう。JSON については json.org または rfc7159 を見に行くと良いでしょう。
実際のところ一次情報から知りたいことを理解していくのは簡単ではありません。言葉の意味が掴めず余計にわかりにくいと感じるでしょうし、抽象的すぎると感じることも多いでしょう。
一定経験のある筆者でも「概要がわかっている場合のみ一次情報を読み解ける」というのが実感です。
しかし自分が主戦場にする言語/フレームワーク/ライブラリなどに関しては遅かれ早かれ公式の文書を理解できる必要があるので、本当に必要になったときに読み解きに行けるように少しずつでも訓練をしていくべきです。そこで下のフローを勧めます。
おすすめフロー
- まずは一次情報を見に行く
- 30秒だけでもいいから読解を試みる
- わからなくなったら諦めてわかりやすい情報を探す
- 解説記事
- AIに質問する
- AIに読解をお願いする
- 理解ができたらもう一度一次情報を読みにいく
- 理解したことが公式ドキュメントではどのように表現されているのか観察する
こうしていくことで徐々にそのドキュメントが自分の庭になっていきます。ある程度の臨界点を超えると一気に「最も読みやすくわかりやすい文書」に変わっていきます。そのスキルを徐々にでいいので育てていくと良いでしょう。
1.c 実装の都合の利用者の都合の分離
自分が どんな処理を実装するか ではなく、利用者にどう見てもらうべきかを考えるべきだということです。
インターフェースと実装は違う
インターフェースの定義を正しくすると抽象的すぎるので例から考えます。インターフェースとは下のようなものです。
- 関数であれば名称/引数/戻り値の型
- テーブルであれば名称/カラムの型
より広義には「その関数が達成してくれる約束事」や「そのテーブルにレコードが存在する基準」などもう少し曖昧性のある情報も含みます。これらをまとめて定義すると少し抽象的ですが下の引用文のようになります。
The interface consists of everything that a developer working in a different module must know in order to use the given module. Typically, the interface describes what the module does but not how it does it.
John Ousterhout『A Philosophy of Software Design』Yaknyam Press, 2018年, v1.01, p. 31
インターフェースはあるものを利用するのに知る必要がある情報で、実装は実際のコードの中身や実装方法です。自分が書いたコードを利用する人が「書いたコードのすべて」を理解しないと使えない場合そのコードの価値は高くありません。インターフェース部分を小さくしつつ、できることが多いコードを書いていけるようにしましょう。
実装ベースでインターフェースを考えない
タスクにアサインされたときに「どんな処理をすればいいか(≒実装)」がわかってもそれをベースにインターフェースを決めてはいけません。
例として蛇口を考えましょう。温度調整ができる蛇口は下の2種類のインターフェースがあります。
- お湯と水の流量の2つをそれぞれ調整できる
- お湯の温度と全体の流量の2つをそれぞれ調整できる
どちらのインターフェースでも「お湯と水を混ぜる」ということをやっているので実装からインターフェースを考えると前者のインターフェースを選んでしまいます。ただ、利用者の立場になると「同じ温度で保ちながら流量を変えたい」というニーズを叶えられるので後者のほうが嬉しいでしょう。もちろん前者が嬉しい場合もあるかもしれませんが、「これしかない」と安易に考えてはいけません。
ただし選ぶのは難しい
蛇口の例においては、他にあり得るインターフェースを(明らかに使いにくいものも含めて)10個位出してみると「選択肢を出して選ぶ」の良い訓練になると思います[1]。
たくさん選択肢が出せたあとにそこからどう絞り込んで行くのかはこれもまた簡単な問題ではありません。先に引用した A Philosophy of Software Design の前半あたりたくさんのヒントがあるので参照してみてください。
2.バグへの対応
2.a エラーメッセージを読む体力
まずはエラーメッセージを自分で読んで理解を試みる
まず自分で読んでみると言うのは「一次情報を見る体力を得る」に通じるものがあります。エラーが起きたら「ただググってみる」「あり得る対応策を手当り次第試す」というアクションではなく「何が起きているのか」を理解するように努めましょう。
おすすめフロー
- まずはエラーメッセージを読む
- 30秒だけでもいいから読解を試みる
- 理想: どうなっているべきものが
- 現実: どうなっていたことが問題か
- わからなくなったら諦めてわかりやすい情報を探す
- 解説記事
- AIに質問する
- AIに読解をお願いする
- 理解ができたらもう一度エラーメッセージを読みに行く
- 理解したことがエラーメッセージではどのように表現されているのか観察する
2.b 原因を切り分ける
エラーメッセージの意味や、想定外に起こっている事象が理解できてきたら次に原因の切り分けに進みましょう。あり得る原因は最初は無限にあるわけですが、これをアキネーターのように あり得る原因全体の集合をどんどん小さくしていく ことで正しい原因に近づけます。例として身近な例で家にいてスマホが Google に繋がらなくなる場面を考えましょう。
- まずはエラーメッセージを理解する
- まず「ただ繋がらない」ではなく何が起こっているのか理解を試みる
- イメージ的には下のようなパターンがあるはずでどれなのかだけでも一定ヒントになる
- サーバーが応答しません
- インターネットにつながっていません
- 切断されました
- 選択肢を出す
- スマホがおかしい
- スマホとルーターの間がおかしい
- ルーターがおかしい
- ルーターからインターネットの間がおかしい
- インターネットから Google の間がおかしい
- 切り分ける
- Google 意外にはつながるか?
- スマホを再起動したら変わるか?
- スマホ意外のデバイスで変わるか?
- 携帯回線ならつながるか?
このようにしていくと自分が詳しくないことでも一定問題を狭めることができます。
切り分けTips
- 実験は「逆の結果」を出す方法を必ず持つ
- 「どこをいじっても壊れ続ける」というときは開発環境自体が壊れているかもしれない
- 必ず「うまくいく方法」と「うまくいかない方法」の両端から狭めていくイメージを持つと良い
- 参考: https://www.youtube.com/watch?v=vKA4w2O61Xo
- あたりまえのことをあえて実験する
- 「失敗する一番シンプルな例」が見つかると強い
- 次に「失敗と成功を分ける最も小さい変更」を見つけると更に強い
- 例: 電卓がバグったら 1 + 1 を計算してみる
2.c リファクタリングも視野にいれる
軽微な修正でも積み重なると読むのも編集するのも難しいスパゲッティなコードになっていきます。なにか修正を加えるときは「そもそも今の問題の解き方がよくないのではないか」という視点を常に持ちましょう。このときに行うことがリファクタリングです。
プログラムの外部から見た動作を変えずにソースコードの内部構造を整理することである。
リファクタリング (プログラミング) - Wikipedia より
挙動としては同じだが、より適切な実装に移していくことでより簡単にバグ修正ができるようになっていきます。一つのバグを治すために2箇所以上に修正を入れないといけない場合(さらにその場所が遠いとき)は現状の解き方がよくない可能性を疑うべきです。
リファクタリングとバグ修正は分離する
ここでリファクタリングとバグ修正を同時に行うと、「そのバグを治すために新たなバグを生んでいないか」の確認が大変になります。こういったことを避けるために下の順序を守ると良いでしょう。
- テストをしっかり書く
- テストに守ってもらいながらリファクタリングを実施
- バグを再現するテストを書く
- 簡単な修正をいれることでバグを解消
少し余談ですが、こういったアクションを将来的に妨げないためにもインターフェースと実装の分離は重要です。
3. コミュニケーション
3.a 結論ファースト
質問を受けたらまず結論を返しましょう。誰かに何かをお願いしたいときも結論から伝えましょう。Yes/No なのか 5W1Hなのか、相手の「相手の質問の型」に合わせて答えると答えやすいでしょう。「XXは完了しましたか?」と聞かれたら「Yes」か「No」、「いつになりそう?」と聞かれたら日時や時刻をまずすぐに答えるということです。「完了条件によって変わる」とか「どれくらい急いでほしいのかで変わる」みたいな場合はそのように「シンプルな結論ではない」という結論を最初に伝えればよいでしょう。
結論の量と抽象度にバリエーションが有ることを理解する
自分が結論として話すべき分量や抽象度はその時々で変わります。まずは抽象度を3パターンくらいで言えるように話すことを整理しておくと良いでしょう。例えば「MeDiCUって会社で何してるの?」と聞かれたら同じタスクをしていても下のようにいくつか答え方があります。
- 集中治療室で使える医療AIを作っている
- 医療AIを作るためのデータを前処理をしている
- 電子カルテのデータを自然言語処理で正規化している
親戚のおじさんに話すときと同じ業界の友人に答えるときで、適切な抽象度は変わるはずです。社内のコミュニケーションでも同様で、同じチームの人には「読み込みモジュールのボトルネックを潰している」と話すのが適切なタスクも全社MTGでは「データ変換の高速化を行っている」くらいの抽象度の方が良いことがあるでしょう。理想的には自分の説明を常に任意の長さに伸縮できるように理解を整理しておくと良いでしょう。「Aを達成するためのB」「Bを達成するためのC」「Cを達成するための...」というチェーンを理解しておき、状況に合わせてそのどの部分を伝えるべきかを変えるという考え方がおすすめです。
相手がそうしてくれるように努める
余談ですが、自分以外の人(特に自分より社歴が若い人)が「まず結論から話しても安心できる」という雰囲気をしっかり作っていきましょう。まずは質問の型がわかりやすいように明示しましょう。「XXはどうなりましたか?」ではなく「完了しましたか?」と聞くほうが答えやすいはずです。
「XXは完了しましたか?」と聞いたときに相手が忘れていたとして「YYだけ残っていますが、それ以外はすべて完了しました」とすぐに言ってくれるようにするにはどうしたらいいのかを考えると良いでしょう。責められているような気持ちになると「すべて完了するはずでしたが、急にZZということがわかって、それに対応しようとしましたがちょっと時間が足りなくて間に合わず…」とどうしても言いたくなってしまいます。なにか問題が起こったときに、システム、問題、仕組みなどに目を向けて人に原因を求めない意識を維持していくといいでしょう。
3.b コンテキスト情報をつける
結論や簡単な説明だけではわからないときのためになにか URL をつけるようにすると良いでしょう。URL があることで「よく整理してみると二人が違う前提で議論をしていた」みたいな空中戦が減っていきます。Pull Request の URL、fail したCIのリンク、Notion のリンク、GitHub 上のファイルへのリンク、Slack の message へのリンクなど、一つコンテキストになる情報を加えるだけでかなりミスコミュニケーションが減ります。
未来のためにも役に立つ
今この場で共有しているコンテキスト情報を数ヶ月~数年後に正しく思い出すことは非常に困難です。自明だと思うリンクでも一つつけておくことで将来調査をするときにかなり楽になります。コミュニケーションは今いるメンバーに対してだけではなく、未来の会社にいるメンバーに対しても行っているという意識を持ちましょう。
パーマリンクを意識する
なにかURLをつけるときは時と場合によって変わり得るものではなく、どれだけ時間が経っても半永久的に同じものを指し続けるURL を選びましょう。一番多い例は GitHub の特定のファイルを参照する URL ですが、ここでコミットを指定したパーマリンクを使うことであとからの再利用性に寄与します。
3.c 質問は期待と現実のギャップを伝える
質問をするときには何を聞いていいのかわからないことも多いでしょう。そこですべてを説明したくなりますが、基本的には期待と現実のギャップを伝えてください。
- 意図せずテストが落ちたとき
- NG: テストが fail したので対応を教えてほしい
- OK: 何もしていないのでテストが落ちないはず(期待)なのに落ちた(現実)この原因はなにか
- memo: NG例ではテストを直したいのか、開発環境を直したいのか、はたまた不要なテストを消したいのかわからない
- 思った通りの実装方法がわからないとき
- NG: 小数の誤差を治す方法が知りたい
- OK: 0.5 + 0.5 と 1.0 がイコールになってほしい(期待)のに0.000000… いくつ程度の小さな誤差が出る(現実)この原因はなにか
自分が問題だと思っていることが実際には問題ではないことがあります。そこで期待をまず明確に示すことで、「そもそも満たすべき期待なのか」から議論を進めることができます。
最後に
繰り返しになりますがこれらの項目はこの記事を一度読んでできるようになるものではありません。これらのスキルを付けるために必要な考え方やノウハウの表面を少しなぞっただけです。これらの項目のスキルを伸ばしていけるように実践しては振り返るということのループによって徐々に身についていきます。焦らずに少しずつ着実に身に着けていきましょう。
-
例えば、洗顔用、食器洗い用、手洗い用の3択を決めたらそれに合わせていい感じの温度がいい感じの量出てくるとか ↩︎
Discussion