概要
この記事では、エンジニアにとって重要な、
「良いコードとはなにか? どうすれば良いコードが書けるのか?」について、
基礎の考え方から、私なりに頑張ってまとめて記載してみようと思います。
ターゲット
新卒~1年目のエンジニア向けにわかるように、なるべくかみ砕いて説明をしようと思います。
第1章:良いコードとは?
まず初めに、良いコードとは何でしょうか?
ここでは会社に所属するエンジニアの視点から考えてみましょう。
会社に所属するエンジニアに求められるのは、 利益のあるソフトウェア(ゲーム) を作ることです。
言い換えれば、良いコードとは ソフトウェア(ゲーム)が生み出す利益を最大化するためのコードと言えます。
ここから、推論を進めてみましょう。
⇒ 良いコードとは?
⇒ ソフトウェア(ゲーム)が生み出す利益を最大化するコード
⇒ 利益を最大化するためには?
⇒ 少ないコスト(時間・人件費)で要件を実現できるコード
⇒ コストを最小化するためには?
⇒ 要求を効率的に満たす実装を迅速に提供できるコード
つまり、良いコードとは、要求を効率的に満たす実装を迅速に提供できるコードです。
要求を満たす実装を素早く提供できるコードにするには?
私は、要求を効率的に満たす実装を迅速に提供できるコードにするには下記が大切だと考えています。
- コードの可読性:他の人が理解しやすいコード
- 拡張性と修正の容易さ:将来の変更や追加がしやすいコード
- 実行速度:高速に動作するコード
- 正確性:正確な結果を返すコード
上記性質をすべて満たせるコードは存在しないことがあります。
例えば、高速なコードを求めると大抵の場合、難解なコードになり、理解しやすいコードにはならなかったり。。
要求に応じて、重視するものを選択するのもエンジニアの仕事です。
コンピュータシステムに関する評価指標にRASISという指標がある。
- Reliability(信頼性)
- Availability(可用性)
- Serviceability(保守性)
- Integrity(完全性)
- Security(機密性)
この中でも、今回は他の人が理解しやすいコードの視点で話します。
理解しやすいコードとは?
まず、理解しやすいコードという表現は少し曖昧かもしれませんので、別の言い方にします。
理解しやすいコード ⇒ 認知負荷の低いコード
認知負荷の低いコードとは、コードを読む際に脳のリソースを余分に消費しないコードのことを指します。
エンジニアにとって脳のリソースは非常に貴重なのです。
認知負荷が高くなってしまうコードの例を挙げてみましょう。
認知負荷の高いコードの例
- 読むごとに多くの疑問や懸念が湧いてくる
- 内容を理解するには深く読み込まなければならない
- 冗長な処理が多い
- 様々なパターンを想定する必要がある
2,3,4章では、上記のようなケースを避けるための方法を紹介します。
第2章:認知負荷を減らす手段(哲学編)
目次
- まず相談する
- 客観的な視点を持つ
- コードは体系的な知識の集まりと考える
- What,How,Why,Where,Whenを説明できるコードにする
- 文脈を考える
- 適切な分割単位を考える
- 循環依存を回避する
- トレードオフを意識する
まず相談する
認知負荷の低いコードを書く際に困る状況は、大抵以下のパターンです。
① 自分の経験が不足している場合
② 視野が狭く、良いアイデアが浮かばない場合
③ 判断に必要な情報が欠けている場合
④ やりたいこと自体が本質的に難しい(誰が取り組んでも難しい課題)
ほとんどのケースは①②③に該当すると思いますので、
10分以上詰まったら、迷わず相談しましょう!
相談することで学びが得られるだけでなく、独力で考えるよりも質の高いコードにつながることもあります。
客観的な視点を持つ
認知負荷の低いコードを作成するためには、客観的な視点が欠かせません。
なぜなら、直近で書いたコードが自分にとって理解しやすいと思っても、
それは、そのコードの意図や実装の背後にある理由がまだ記憶に新しいからかもしれません。
ですから、 「初めてその箇所のコードを読む人でも意図や理由が理解できるコード」 になっているかどうかを、客観的な視点で評価することが重要です。
しかしながら、経験が乏しい場合は、自力では客観視が難しいこともあります。
その場合、やはりまずは相談することが良いでしょう。
経験を積むことで、徐々に客観的な視点を持つ能力が向上することでしょう。
コードは体系的な知識の集まりと考える
コードはただ動けばよいというわけではありません。
分かりやすいコードを書くためには、コードは知識の集合体だと理解することが大切です。
コードは、特定の課題(仕様)を解決するために、機械に指示を出す手段として記述されるものです。
したがって、コードは、
「何をするのか(What)?どのようにするのか(How)?なぜその方法を選ぶのか(Why)?どこで使用されるのか(Where)?いつ使用されるのか(When)?」
といった構造を持った課題解決手段の知識の集合体と捉えることができます。
つまり、構造を持った知識として、現実世界の情報を分かりやすく記述するためのアプローチが適用できます。
以下はその例です。
- (文章):最初に結論を述べることで理解が容易になる
- (コード):関数名に実行時の振る舞いを反映させることで理解が容易になる
- (文章):同じテーマの知識は近い場所に集約することで理解が容易になる
- (コード):関連する機能をクラスにまとめることで理解が容易になる
このように、コードが知識の集合体であると理解することで、
より分かりやすいコードの構築において柔軟なアイデアを導くのに役立つでしょう。
What,How,Why,Where,Whenを説明できるコードにする
前述のとおりコードは知識の集合体なので、
エンジニアが他人のコードを理解するためには、そのコードのWhat, How, Why, Where, Whenを理解する必要があります。
What, How, Why, Where, Whenを容易に理解できないコードは、
難解で認知負荷の高いコードになり、把握するのに時間がかかります。
What, How, Why, Where, Whenが明確に伝わるコードを心がけましょう。
例えば、バグが発生したり・新機能を追加する必要があり、既存のコードを修正する必要があるとします。
この時、
「そのコードがどういうものでどう処理されているのか?(WhatとHow)」
がわからないとコードの処理内容が頭に入ってきませんので、どう弄っていいかわかりません。
「なぜそのコードにしたのか?(Why)」
がわからないと、勝手にコードを弄るのは怖いと思ってしまいます。
「どこでどのタイミングで使われているのか?(WhereとWhen)」
がわからないと修正による影響範囲がわかりません。
上記がわからないコードは、時間を掛ければそのうち理解できますが、
著しく、エンジニアの生産性を下げてしまう認知負荷の高いコードとなるでしょう
どうWhat,How,Why,Where,Whenを説明するか?
とはいえ、What,How,Why,Where,Whenはどうやって説明しましょうか?
これはずばり、命名です。
ファイル名、クラス名、関数名、変数名。
あらゆる命名で、What,How,Why,Where,Whenを説明できるよう心がけましょう。
命名で伝えるのが難しい時は、コメント文を添えたり、文脈(後述)を利用することも検討しましょう。
命名が難しい場合、コードの分割単位が適切でないことが多いです。
歪に分割されたり、整理されていないごちゃごちゃの知識は、
説明が難しいです。
命名が難しい場合には、コードの構造を見直し、適切な分割単位を検討することが大切です。
つまり、適切な分割単位と適切な命名がある場合に、What,How,Why,Where,Whenが理解しやすいコードになるのです。
各要素を命名やコメント文で伝える時のポイントをまとめました。
- What
コードが何をするのかを明確に示すことが重要です。関数やクラスの役割、処理の目的などがこれに該当します。
- How
コードがどのように動作するのかを示すことも大切です。アルゴリズムや処理の手順がこれに該当します。
- Why
コードの設計や実装の背後にある理由を説明できることは重要です。なぜそのアプローチが選ばれたのか、その背景や意図が他のエンジニアにも伝わるように工夫しましょう。
- Whereと When
コードがどこでどのタイミングで使用されるのかを明確にすることも大切です。これによって、変更が及ぼす影響や、コードの利用方法が理解しやすくなります。
文脈を考える
コードを記述する際、文脈というのは無視できない要素です。
変数や関数などの命名や、処理の配置場所は、そのコードが存在する文脈によって影響を受けます。
私たちが日常的に行う情報のやり取りでも、同じ単語でも文脈によって意味が変わったり、
文脈があるからこそ省略されたりすることがあります。
例えば、「エンジニア」という言葉は複数の意味を表す用語ですが、
ソフトウェア開発の分野では「ソフトウェアエンジニア」を指し、
自動車業界などでは「メカニカルエンジニア」を指すこともあります。
このように文脈は知識を相手に伝える上で非常に重要な考え方ですので、
コードでも文脈が非常に重要です。
文脈は、前述した What, How, Why, Where, Whenの要素を説明するための命名にも大きな影響を与えます。
文脈によっては、全ての要素を命名で全部説明する必要が無いケースがあります。
逆に、文脈を活用して命名を簡潔に保つことを考えましょう。
文脈によって命名の細かさが変わる例
class Monster
{
// ここでは「Hp」とはモンスターのHPを保存する変数ですが、
// 文脈でわかるので、「MonsterHp」とは命名しない
public int Hp;
}
// Hpを総合管理するようなクラス。
// この文脈では何のHPなのか明確に区別するため、詳細な命名にする。
class HpManager
{
public int MonsterHp;
public int PlayerHp;
}
また、文脈によってコードの配置場所が不適切になるケースも考慮しましょう。
例えるなら、江戸時代の世界に自動販売機があるようなものです。
文脈によって処理の記述箇所が適切ではない例
class Monster
{
// モンスターの文脈なのに、突然プレイヤーのHPについての情報が出現する
public int GetPlayerHp();
}
【客観的思考について】
客観的思考とは、
「あなたが知っている文脈を相手が知らないと仮定した時に、その相手がどのように思考するか」をシミュレートする行為です。
つまり、文脈を意識できるようになれば客観的思考も上達していきます。
適切な分割単位を考える
1つの処理が長すぎると、
その処理の理解に時間がかかり、また、修正による影響範囲の特定が難しくなり、
認知負荷の高いコードになります。
そのため、クラス・関数・変数等は1つの独立して意味を持つ知識の単位で分割することを心がけましょう。
細かく分割すれば良いということではありません。
意味がぶつ切りなってしまう分け方は、コードが読みずらくなり、認知負荷が高まります。
分割単位についての考え方は、
単一責任原則や、モジュール強度・モジュール結合度といった、
ソフトウェア設計の原則を参考にすることをおすすめします。
循環依存を回避する
知識は、最も小さな単位の知識(下位の知識)があって、
それが組み合わさり、次第に大きな知識(上位の知識)を形成していくものです。
このため、一般的に知識の構造は、上位の知識が下位の知識に依存する構造となります。
コードも同様で、上位のコード(例えばMain関数など)は下位のコードに依存する形をとります。
ここでの依存とは、データの取得や設定、関数の呼び出しなどで他の部分を参照することを指します
上位と下位のコードが循環した(ループした)依存関係に陥っている場合、
それは良い設計のコードとは言えません。
このような循環依存が存在すると、
コードの各部分の責任と役割が曖昧になるからです。
上位のコードの知識の詳細は下位のコードが担っているはずなのに、
下位のコードが上位のコードを参照していたら、いつまでたってもその知識を完全に把握することが出来ず、無限ループに陥るからです。
これでは、コードを正しく理解することが出来ません。
バグが起きたときも厄介です。
上位を修正したら、下位も影響を受け、下位を修正したら、上位も影響を受けるので、
無限にバグが発生する可能性が生まれ続けます。
なので、依存関係は必ず上から下に流れていくように設計しましょう。
「クラスA→クラスB→クラスC→クラスD→クラスA」みたいに、うっかり循環するケースもあるので、
初心者はよくやりがちなミスです。
トレードオフを意識する
本記事は、主として理解しやすいコードの実現を目指していますが、
必ずしも理解しやすいことが絶対の価値というわけではありません。
最も重要なのは、納期を守りつつ以下の要素をバランス良く満たすコードを作成すること、
そして、ソフトウェア(ゲーム)が生み出す利益を最大化するためのコードにすることです。
- コードの可読性:他の人が理解しやすいコード
- 拡張性と修正の容易さ:将来の変更や追加がしやすいコード
- 実行速度:高速に動作するコード
- 正確性:正確な結果を返すコード
理解しやすいコードのみを追求しすぎて、
納期が遅れたり、要件を満たせないコードになることは避けるべきです。
したがって、コードを書く際は、常にメリットとデメリットを考慮し、柔軟に実装方針を選択するよう心がけましょう。
私は、思想が偏らないように、常に中立の立場で物事を見つめることが重要だと考えています。
終わりに
これをやれば絶対に上手くいく方法、というのはありません。
良いコードとは常にバランスとの戦いです。
なので、固定観念に捉われず状況に応じて柔軟なコードを書くことを心がけましょう!
おすすめの書籍
今後書くかもしれない章
- 第3章:認知負荷を減らす手段(設計原則編)
- DRY原則
- YAGNI原則
- 驚き最小の原則
- 単一責任原則 ~ 名前は責務を表す
- モジュール強度・モジュール結合度
- KISS原則
- 関心の分離と集約パターンについて
- 第4章:認知負荷を減らす手段(Tips編)
- 命名
- 宣言的なコード
- 早期リターン
- クラスの分け方について
- 値の書き換え責任について
- 150行超えているコードは注意
- そのクラスは、どこに属しているか、それによって依存関係を決める
- 命名は文脈を考えて付ける
- 処理分岐は削れないか?if文は難しい
- 可能ならできる限り制約を付ける
- 関数をむやみに分割しない、粒度の均一性や凝集性を考慮する
- クラスの変数を参照する必要のないメソッドはstaticにする
- 大きなPRはなるべく作らない
- 第5章:デザインパターン解説
- ストラテジーパターン
- FactoryMethod パターン
- Fluxパターン
- Adaoterパターン(委譲パターン)
- 集約パターン