はじめに
私は普段システムエンジニアとして Web アプリケーション開発を行っています。PJ にアサインされてから半年以上が経過し、もはやプログラミング言語を用いて機能を実装すること自体は当然のことと認識していますし、されています。
そこで最近は、機能実装した上でいかに良いソースコードを作成できるか、を意識して作業しています。
本記事では技術的なことではなく、考え方をまとめていこうと思います。
本筋に入る前に、今回の内容は以下の書籍に大きく影響されています。併せてご参照ください。
- リーダブルコード
- プリンシプル オブ プログラミング
修正しやすいソースコードの作成を目指す
修正しやすいソースコードの作成を最終目標としています。ソフトウェアは修正されるのが前提なところがありますので、最初から修正しやすいように設計できれば他メンバーにとっても将来の自分にとっても運用・保守の面でメリットになります。
修正しやすくするために必要なことは読みやすいことと人に説明しやすいことだと考えています。以降、詳しく説明していきます。
読みやすく、人に説明しやすくする
ソースコードは他のメンバーも読むことを踏まえると、ソースコードはドキュメントと捉えることができます。ドキュメントである以上、読みやすく書くことは努力義務でしょう。ここでは具体的にどうすれば読みやすくなるか、私なりの考えを記していきます。
章、節を意識してロジックを区切る
大抵の書物は「1章、2章、・・・」のような章があり、その中に「1節、2節、・・・」のような節があります。なぜなら、見出しをつけることでその中に書かれている内容を把握させやすくするからです。
ソースコードも同じような構造を取るとロジックの全容を把握しやすくなると思います。つまり、以下のような構造で書くと良いでしょう。
↓↓↓
main()
- function1()
- sub_function1()
- sub_function2()
- ...
- function2()
- sub_function1()
- sub_function2()
- ...
...
メソッド名を章や節のタイトルと捉えて見出しをつけることで、大まかな流れと、メソッド名で行われていることを知ることができます。本を書くイメージでコードを記述すると良いでしょう。
日本語でコメントアウトを書く
コメントアウトで記述するのは「なぜそう書いたか、逆になぜ別の方法で書かなかったか」など、Why や Why not の内容を書いた方が良いと読んだことがあります。
ところで、プログラミング言語はアルファベットで記述されているので、英語が得意でない人にとって「英語↔︎日本語」変換は脳のリソースを割きます。そこで私はその手助けとして、「何をしているのか」の What も日本語でコメントアウトしています。
また、アルファベットだらけのソースコードの中に紛れる日本語は目に留まります。サリエンス効果と呼んだりもして、これが日本語という言語障壁のメリットだと感じています。上記の 章、節を意識してロジックを区切る と併せて、見出し部分を日本語で記述すれば可読性を大きく高められると思います。
↓↓↓
# 〜をする
main()
# 〜をする
- function1()
- sub_function1()
- sub_function2()
- ...
# 〜をする
- function2()
- sub_function1()
- sub_function2()
- ...
...
逆に、英語圏に比べて日本語ではアクセスできる情報がかなり少ないことが知られています。これは日本語という言語障壁のデメリットだと感じています。
プログラムは上から下に向かって読むことを知る
川の水が上から下へ重力に沿って流れるのと同様に、プログラムも上から下に向かって読み進めます。当たり前な話ですが、だからこそ下から上へ読むときは大変疲弊するのでこういうのは極力なくすことが大事です。今回は以下の2パターンについて言及します。
1. for や while で次のイテレーションが始まるとき
仕様上、必ず起きる事象です。ですが、ブロック内の処理が長く、下から上へ視線を動かす距離が長いと読者は疲弊します。なので、1回の処理は最長でも画面に収まる程度にしています。どうしても長くなるときは 章、節を意識してロジックを区切る で言及した、ロジックごとに区切る方法で実装すると良いでしょう。
2. if の条件を確認しに行くとき
if 内の処理が長く、途中でその処理が行われる条件を確認しにいくときに起こり得ます。例えば以下のような場合です。
↓↓↓
if 条件文:
...長い処理...
elif 条件文:
...長い処理...
else:
...長い処理...
if ~ else: まで画面が見切れるぐらい長くなると、else を読んでいるときにどんな条件でここに辿り着くのか忘れる場合があります。一応 Visual Studio Code だと画面上部に if の条件文が書かれることもありますが、前後の文脈から条件文を理解する必要があるので結局読みにくいことには変わりません。
こうなるときは例に漏れずブロックの中を機能ごとにメソッドに区切ることをお勧めします。if 文全体がまとまって見えると処理の流れとフローチャートを把握しやすくなります。
↓↓↓
def function1():
...
def function2():
...
def function3():
...
def main():
if 条件文:
function1()
elif 条件文:
function2()
else:
function3()
if 文の乱用を避ける
体感バグの8割が if 文に起因しています。なので、仕様的に if で条件分岐する必要がある場合を除いて、可能な限り if 文を用いない方法でコーディングしています。例えば、上記の 2. if の条件を確認しに行くとき で記載した if 文ですが、条件がごとにメソッドにするほど処理も変わるのであれば、呼び出し側を変えたほうが良いと思います。すなわち、上の例だと py ファイルを分けても良いでしょう。
↓↓↓
def function1():
...
def main():
function1()
def function2():
...
def main():
function2()
def function3():
...
def main():
function3()
一方で、汎用メソッドは処理を1箇所にまとめる必要があります。
このバランスをどう取るのか、自分の中で基準が曖昧で迷い中です。
また、変数の型を適切に設定できれば if 文が要らなくなる場合があります。
↓↓↓
data_list = None
...何らかの処理で data にリストが入る...
if data_list:
for data in data_list:
...
とするよりは、
↓↓↓
# 初期値を空リストにする
data_list = []
...何らかの処理で data に要素が入る...
for data in data_list:
...
のように、初期値を適切に設定できれば if 文が必要なくなります。
上のコードは、仮に data_list に何も要素が入らなくなっても for 文が1回も実行されないだけでエラーにはなりません。
インデントは少なくする
インデントを少なくし、深いネストは避けた方が良いと言われています。画面右側が窮屈になって読みづらくなるのもあると思いますが、個人的には処理の流れを追いにくいのが主な理由だと思います。
↓↓↓
for elem1 in list1:
...処理1...
if 条件文:
...処理2...
for elem2 in list2:
...処理3...
...処理4...
else:
...処理5...
...処理6...
...処理7...
処理1から処理7まで記載してみました。処理3まではまだ追えると思いますが、処理4から怪しくなり、処理5までいくと条件文を確認しに一旦画面を上にスクロールし、処理6でインデントが下がったら何のブロックの中かを確認しにまた画面を上にスクロールしてようやく全体を理解できます。しかし、このソースコードは理解するのに体力と時間を使い、修正するまでのハードルが高いです。
pythonの場合、インデントの深さがブロックの長さになるので、処理6のインデントが左右に1つずれると全体のロジックが変わります。
修正するときにインデントがズレるとバグを引き起こすので、ネストが深いと修正のしづらさをより一層引き立ててしまいます。
これを改善するには、データの構造を工夫して変えて if 文の使用を避けたり、2つある for 文を分離できないか検討したりすると良いと思います。for 文を分離できないなら最低限ネストが深い箇所を短くするようにしましょう。
↓↓↓
def function(elem1, elem2):
...
for elem1 in list1:
for elem2 in list2:
function(elem1, elem2)
...
または、
↓↓↓
for elem1 in list1:
...
for elem2 in list2:
...
処理が簡潔になるならデータ構造を複雑にしても良いと考えています。このように、インデントを少なくできるような工夫ができないか、常に考えるようにしています。
車輪の再発明をしない
要は、標準ライブラリにせよ自作メソッドにせよ、既にあるロジックを再生産せず使い回すということです。そのためには実装したいロジックが他の箇所で使われていないか調査すると良いです。同じロジックが2箇所以上に書かれていると、その内容を修正する必要が出たときに2箇所以上変更しなければならなくなります。DRY 原則にもあるように、メソッドを使い回すことは保守性を高めることになります。
既存の組み込みメソッドの利用は if 文の乱用を避ける、インデントは少なくする の観点でも重要だと思います。例えば、文字列の中に特定の文字を挟みたいとします。
↓↓↓
text = "あいう"
new_text = ""
for i in range(len(text)):
new_text += text[i]
if i < len(text) - 1:
new_text += ","
print(new_text)
# 出力:あ,い,う
確かにこれでも動きますが、このコードは以下のメソッドを利用すると簡単に記述できます。
↓↓↓
text = "あいう"
new_text = ",".join(text)
print(new_text)
# 出力:あ,い,う
このように、既存メソッドを上手に活用すれば、if 文もインデントもなくすことができます。join() は有名で上の例ではピンとこないかもしれませんが、他に知らないメソッドもあると思います。なので私の場合、書いたコードを生成AIに渡し、リファクタリングをしています。結構な頻度でより良いコードを返してもらえますし、未知のメソッドに出会うことで学習にもなります。
自作メソッドに関して、使い回すことも大事ですが、使い回されることを意識して作成することも大事です。単一責任の法則にあるように、1メソッド1機能に留めると再利用性が高まると思います。
ソースコードを説明してみる
作成後のソースコードの概要と手順を人に説明すると仮定します。その際、説明に詰まったら恐らくそこが改善点です。まどろっこしくないか、回りくどくないか、他に良い方法があるよとツッコミされないか、これらを意識し、手順の各ステップが論理的につながるようなアルゴリズムを構築しています。
これに関して、ラバーダックデバッグというやり方があります。ぬいぐるみなどに実際にソースコードを説明してみて、改善点を自覚するといったデバッグ方法になります。併せてご参照ください。
↓↓↓
簡潔で論理的であること
最後に抽象的な内容ですが、上記を意識したソースコードは簡潔で論理的になれると考えています。全体の流れを把握しやすく複雑な構文を作らず、フローチャートを想像できるようなコードを書けるよう日々精進しています。
ところで、コードが短ければ簡潔であるとは限らないとも思っています。例えば、以下のメソッドは何をしているか、戻り値は何かイメージできますでしょうか。
↓↓↓
def function1(n):
return "EOvdedn"[n%2::2]
function1(1)
function1("2")
これは恐らく偶奇判定で最も短いコードですが、これが偶奇判定であることや戻り値が Even
か Odd
であることが一目ではわかりにくいです。多少長くしてもコードと開発者の意図が伝わるように書く必要があるので、私なら以下のようにします。
↓↓↓
def is_even_or_odd(num):
if num % 2 == 0:
return "Even"
else:
return "Odd"
または、
↓↓↓
def is_even_or_odd(num):
even_or_odd = {0: "Even", 1: "Odd"}
return even_or_odd[num % 2]
このメソッドが偶奇判定だと分かれば "2"
を引数に入れるとエラーになることがわかり、例外処理の追加修正がしやすくなるでしょう。
さいごに
一度書いたコードが未来永劫変わらないなんてことはないでしょう。仕様変更・機能追加によって、他メンバーや将来の自分が該当コードを修正することになると思います。そのとき修正してもらうにはまず読みやすいものにする必要があります。作成したソースコードは他人から見て恥ずかしくないか何度も確認し、その時のベストなやり方を常に追求していくのが最終的に高品質なソフトウェアにつながると考えています。