良いコードを書くための原則
良いコードを書く理由と、PS/CPや実務で役立つ簡潔性・モジュール化・命名規約などの主要原則を解説する。
良いコードを書く必要性
目先の実装だけを急いでコードを書いていると、技術的負債が手に負えない水準まで膨れ上がり、後の保守に支障が出ることがある。したがって、開発プロジェクトを進める際は、最初から読みやすく保守しやすい良いコードを書くことが重要である。
アルゴリズム問題解決(PS, Problem Solving)やプログラミングコンテスト(CP, Competitive Programming)の場合、通常は問題や大会が終わればそのコードを再利用することはなく、特にCPでは時間制限があるため、良いコードを書くことよりも素早い実装の方が重要ではないかという意見もある。この問いに答えるには、自分が何のためにPS/CPを行い、どの方向性を目指しているのかを考える必要がある。
私見だが、汎用的な問題解決能力の涵養を除き、プログラミング面に限って言えば、PS/CPを通じて学べる点は次のとおりである。
- 与えられた実行時間制限やメモリ制限の中で問題を解く過程で、さまざまなアルゴリズムやデータ構造を使って身につけられ、実プロジェクトでも状況に応じてどのアルゴリズム・データ構造を使えばよいかの勘所が養われる
- コードを提出すると即座に正解/不正解や実行時間・メモリ使用量の客観的フィードバックが得られるため、抜け漏れなく正確なコードを素早く巧みに書く練習ができる
- 上級者のコードを見て自分のコードと比較し、改善点を見つけられる
- 実際の開発プロジェクトに比べると小規模で似た機能のコードを繰り返し書くため(特に独学でPSを練習する場合)、締切に縛られずディテールに気を配りつつ、簡潔で良いコードを書く練習ができる
PS/CPを純粋に趣味として楽しむ場合も当然ありうるが、プログラミング力を伸ばすためにPS/CPを行うのであれば、最後の「良いコードを書く練習」も前の3点に劣らず大きな利点である。良いコードを書くことは最初から自然にできるものではなく、反復練習を通じて継続的に熟達すべきものだからだ。また、複雑で読みにくいコードはデバッグが難しく、書いた本人でさえ一度で正確に書き上げるのは容易ではないため、非効率なデバッグに時間を取られて結局それほど素早く実装できないことも多い。PS/CPと現場には大きな違いがあるとはいえ、だからといって良いコードを書くことをまったく意識せず目先の実装にだけ躍起になるのは、以上の理由から本末転倒だと考える。個人的にはPS/CPにおいても、簡潔で効率的なコードを書くのが望ましいと思う。
12024.12 コメント追加:
現時点の流れを見るに、アルゴリズムやデータ構造など効率的なプログラムのための基礎知識を積み、問題解決力を鍛えること自体はこれからも意義がある。しかし、それを実際に動くコードに落とし込む段階では、すべてを自力で書くことに固執せず、GitHub Copilot(GitHub Copilot)やカーサー(Cursor)、ウィンドサーフ(Windsurf)といったAIを積極的に活用して時間を節約し、浮いた時間で他の作業や学習をする方がよいのではないかと思う。汎用的な問題解決力やアルゴリズム/データ構造の学習のため、あるいは純粋に趣味としてPS/CPをするなら誰も止めないが、コード執筆だけを練習する目的でPS/CPに時間と労力を投じるのは、費用対効果がかなり下がってきたと感じる。さらには開発職においても、少なくとも入社試験としてのコーディングテストは、従来より重要度がかなり下がると予想している。
良いコードを書くための原則
大会で書くコードであれ実務で書くコードであれ、良いコードと言える条件は大きくは変わらない。本稿では、一般に良いコードを書くための主要原則を扱う。ただしPS/CPでは素早い実装のため、実務に比べて相対的に妥協する部分もありうる。その場合は本文中で別途言及する。
簡潔なコードを書く
“KISS(Keep It Simple, Stupid)”
- コードは短く簡潔であるほど、当然タイポや単純なバグの懸念が減り、デバッグも容易
- 可能な限り、別途のコメントがなくても容易に解釈できるように書き、本当に必要な場合のみコメントで補足説明を付す。コメントに依存するより、コード構造自体を簡潔に保つのが望ましい
- コメントを書く場合は明確かつ簡潔に記述する
- 1つの関数に渡す引数は3個以下にし、それ以上の多数の引数を一緒に渡す必要があるなら1つのオブジェクトにまとめて渡す
条件分岐のネストの深さ(depth)が二重三重と深くなると可読性が下がるため、ネストは可能な限り避けるべき。
例)上のコードよりもガード節(Guard Clause)を用いた下のコードの方が可読性の面で有利1 2 3 4 5 6 7 8 9 10
async def verify_token(email: str, token: str, purpose: str): user = await user_service.get_user_by_email(email) if user: token = await user_service.get_token(user) if token : if token.purpose == 'reset': return True return False
1 2 3 4 5 6 7 8 9 10 11 12
async def verify_token(email: str, token: str, purpose: str): user = await user_service.get_user_by_email(email) if not user: return False token = await user_service.get_token(user) if not token or token.purpose != 'reset': return False return True
ただしPS/CPでは、さらにコード行数を減らして素早く書くため、C/C++のマクロを活用する便法が使われることがある。時間が逼迫した大会に限っては有用な場合もあるが、PS/CPに限って通用する方法であり、一般にC++でのマクロ使用は避けるべき。
例)1
#define FOR(i,n) for(int i=0; i<n; i++)
コードのモジュール化
“DRY(Don’t Repeat Yourself)”
- 同じコードを繰り返し使っている場合は、その部分を関数やクラスに切り出して再利用する
- モジュール化によって積極的にコードを再利用すれば可読性が向上し、後で修正が必要になった際もその関数やクラスだけを一度直せば済むため、保守が容易になる
- 原則として、1つの関数が二つ以上のことをせず、単一の機能だけを担うのが理想的。ただしPS/CPで書くコードは概して単純な機能の小規模プログラムで再利用にも限界があり、時間制約もあるため、実務ほど厳密に原則に従うのは難しい場合がある
標準ライブラリの活用
“車輪の再発明をするな(Don’t reinvent the wheel)”
- アルゴリズムやデータ構造を学ぶ段階では、キューやスタックといったデータ構造、ソートアルゴリズムなどを自分で実装して原理を理解するのは有益だが、そうでなければ標準ライブラリを積極的に活用するのがよい
- 標準ライブラリはすでに数え切れないほど使われて検証され、最適化もされているため、自前で再実装するより効率的
- 既存ライブラリを使えば、同じ機能をするコードをわざわざ実装して時間を浪費する必要がなく、共同開発時にも他のメンバーがあなたのコードを理解しやすい
一貫性があり明確な命名を用いる
“標準規約に従え(Follow standard conventions)”
- 曖昧でない変数名・関数名を使う
- 使用するプログラミング言語ごとに適した命名規約(naming convention)がある。使っている言語の標準ライブラリが採用する命名規約を把握し、クラス・関数・変数などを宣言する際に一貫して適用する
- 各変数・関数・クラスがどんな役割を担うのか、またブール(boolean)型であればどの条件で真(True)を返すのかが明確に分かるように命名する
すべてのデータは正規化して保存
- すべてのデータは一貫した形式に正規化して扱う
- 同じデータが二つ以上の形式を持つと、文字列表現が微妙に異なったりハッシュ値が変わったりするなど、捕捉が難しい微妙なバグが発生しうる
- タイムゾーンや文字列などのデータを保存・処理する際は、入力や計算の直後にUTC、UTF-8エンコーディングなど単一の標準形式へ変換する。そのデータを表すクラスのコンストラクタで最初から正規化を行うか、データを受け取る関数で直ちに正規化するのがよい
コードのロジックとデータを分離する
コードのロジックと無関係なデータは、条件分岐の中に直書きせず別のテーブルに分離する
例)上のコードより、下のように書くのが望ましい。1 2 3 4 5 6
string getMonthName(int month){ if(month == 1) return "January"; if(month == 2) return "February"; ... if(month == 12) return "December"; }
1 2 3 4 5
const string monthName[] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}; string getMonthName(int month){ return monthName[month-1]; }