こんにちは、テクノロジーイノベーショングループ所属、社会人歴4年目エンジニアの王紹宇です。大学ではC/C++使いでしたが、最近はPythonとJavaScript に注力しています。
現在(2019年6月)、とある業務システムの開発チームに所属し、ある実験的な機能を素早くリリースしてはユーザからのフィードバックを得て改善、それをまた次回以降のリリースに入れる、といった業務システムでは珍しくアジャイル的なスタイルでここ1~2年ほど過ごしています。採用技術は主にReact/Next.js/Go/GCPです。
チームメンバーはフューチャーに新卒入社してから1~5年目という、若手中心でガンバっています。
この若いチームでの開発経験を通して、こうすればもっとソースコードが綺麗になると感じたことを3点まとめます。
ソースコードを綺麗にする3つのポイント
📖 綺麗なエッセイを読んだら目に優しく、心も癒やされますよね。
💻 ソースコードもエッセイのように美しくすると非常に価値が上がると思います。
本エントリーでは、将来的にどこかに展開しやすい可読性が高いコードを綺麗であり高価値であるとします。
可読性が低いと、例え自分のコードであったとしても時間が経ってから読み返すと「これ、自分が書いたの?」「思い出さない、さっぱり意味分からない」となり時間の浪費に繋がるかもしれません。また、他人の書いたソースコードであれば、なおさらその意図を理解することに時間がかかります。後から直すのも大変です。
👉 最初のコードから創意工夫し綺麗な状態にすることが重要です。バグの発生も抑えられ、例えバグが発生しても簡潔なコードであれば比較的容易に対処できます。将来の自分にとっても、他人にとっても時間の節約とメンタルケアに繋がるかもしれません。
さて、下のコードは綺麗といえるでしょうか。さっそく一例をあげます。
(本文の例は JavaScript っぽく書いていますが、原理原則は言語に問わない内容にするつもりです)
err = checkError(); |
短いコードで、ぱっと見ると「よくやるパターンではないか」「簡潔で問題がなさそう」と思われるかもしれません。しかし、実は突っ込みポイントは複数あります。
え、どこでしょう? 答えは最後まで読んでいただいたら、文末に開示します。
(注意)コーディングスタイルはの答えは1つだけではありません。これから述べるのはあくまで個人の見解に過ぎなく、ご意見・指摘は大歓迎です。
では、簡単にソースコードを綺麗にするテクニックを紹介します。
ポイント1 実態と名前の違いに気をつける
基本すぎるだろうと思われるかもしれませんが、ロジックの実態と、関数などの名前が乖離しはじめてしまうことがあります(実際に何件かありました)。
ここで言いたいことは、驚き最小の原則 に尽きると思いますが、いくつか具体例とともに紹介します。
1.1 中身が推測できる関数名をつける
例えば、getXXX()
やcheckXXX()
のような名前の関数は、読み手に副作用(side-effect)がないイメージを与えます。そのため、getXXX()
の中で write などの更新が行われると読み手にとって大きな驚きを与えてしまいます。
あるいは、checkXXX()というメソッドが、正常時の返り値として Boolean ではなく、オブジェクトを返すことも、戸惑わせるでしょう。なぜなら、check
(検査)は OK か NG かを返すのを期待する名前なので、オブジェクト本体を期待するわけではありません。
実態に則した名前をつけるとしたら、checkAndGet()
となるでしょう。
ただし、そうすると「単一責任原則」(次の「ポイント2 ロジックの複雑度を減す」の説明を参考してください)に違反になるため別々切り分けるほうが良いかもしれません。
場当たり的に対応するのではなく、その関数の関心事を見極めて適切な対応をしましょう。
1.2 パラメータや変数名には型や可視性などが推測できる名前をつける
パラメータや変数名は(特にweak-typed言語にといて)値の型まで推測できる名前が望ましいです。
文字列ならXxxStr
、xxxName
、数値ならxxxNum
、xxxCount
、ブーリアンならisXxx
、hasXxx
、リストならxxxList
、xxxArr
、マップならxxxMap
、xxxDict
などなどよく使われている表現を把握したほうが良いです。
オブジェクト指向言語のprivate
やprotected
のアクセス制限仕組みは、その文法がない言語にも応用できることがあります。
例えば、Python などの言語には、private なら__
(underscore 2個)、protected なら_
(underscore 1個)をつけるルールがあります。もちろんこの場合は、export _xyz
や import __utils__
のようなルールに反する定義はやめたほうが良いです。Pythonでは注意喚起のために、あえて違和感のあるfrom __future__ import xxx
を許していますが、もちろんそれを推奨するわけではありません。
変数名の悪い例を挙げますと、executedFlag
が0:NOT_EXECUTED
、1:EXECUTED
、-1:EXCEPTION
の取りうるのもよくないでしょう。flag
は 0/1、true/false の二値のイメージが強いので、本当に3値をとるのであればexecutedStatus
の方がまだ適切でしょう。
1.3 実態にあったコメントをつける
ソースを改修した際によくあるのが、コメントの更新を忘れることです。もちろんコメントも同期してアップデートしましょう。
次のコードの改善ポイントは何点あるでしょうか?
// check number positive, if not do nothing |
ポイント2 ロジックの複雑度を減らす
コードの複雑度を減らす技術はたくさんあると思いますが、実践しやすいと思われるコツを紹介したいと思います。
2.1 関数やモジュールの長さを減らす
言語や目的がそれぞれ違うので、関数やモジュールの長さ制限に明確なルールは存在しません。
ただ、参考にできる観点と原則があります。それは、1つの関数は1つの機能しか担当させないこと(単一責任原則(Single responsibility principle))、言い換えると、コメント一言で説明できるぐらいの量が適切ということです。
ある関数が複数タスクを担当していたとして、ポイント1の「名称には嘘ついていけない」という観点で素直に名称をつけたとするとcheckValidationAndDoTask1AndDoTask2AndSubmit()
的な名前になってしまいます。素直に、タスクごとに関数を分けたほうが分かりやすいでしょう。
1箇所で2つ以上のことをやっていると気づいたら、関心の分離(Separation of concerns)と言われるように、関数を切り分けるを考慮するべきです。そうすると、それぞれのコードが簡潔になることで結局は理解にかかる時間が元のコードよりも短縮できます。
理由は、人の脳のレジスタの容量が限られているので、一度処理できる情報量はごくわずかです。そして人間はトップダウンの視点からものの全体像を掴もうとします。細かいタスクのやり方(関心)を別箇所に分離できると、脳の負荷を下げることができるからです。
次のコードはあるデータモデルの初期化の例です。
いろいろなステップの実装を展開して、init
の一箇所に全部書く方法[BEFORE]もできますが、それより、[AFTER]のほうはステップごとに何をやっているのが一目瞭然です。
[BEFORE]
init() { |
[AFTER]
init() { |
2.2 ネストを減らす
if
else
while
switch
などの制御構文が積み重なるほど、インデントが増え複雑度が上がります。
これらの可読性を上げる定形のコツをまとめます。
- 2.2.1 異常系分岐(エラーハンドリング)を先に書き、早期リターンする
[BEFORE] 条件分岐間にどんな依存関係があるのか追うのが難しい
// operation 0 |
[AFTER] エラーチェックを同じインデントにそろえた方が分かりやすい
// operation 0 |
- 2.2.2 余計な else を省略
else の次は何もやらない場合、else 分岐さえ切る必要ありません。
[BEFORE] else を素直に書いた例
if (condition1) { |
[AFTER] elseを無くし、早期リターンする
if (condition1) { |
また、if (condition) return
と同じくif (condition) break
や if (condition) continue
の後ろの else も省略します。
[BEFORE]
while (condition1) { |
[AFTER]
while (condition1) { |
ポイント3 書き方の揺らぎを減らす制約ルールを作る
コードを綺麗に書くためは、世にでている規制のコーディング規約を導入するだけではなく、チーム内ルールを作り、各人の揺れを抑えるとお互いのコードが読みやすくなります。
(どこまでやるかの線引は難しいですが、相互にレビューをしていると慣習的なルールが作られることが多いと思いますので、まずそれを明文化しておくことは有益だと思います)
一例として、a<b
とb>a
は意味は同じなので、どちらでも書いても問題ありませんが、左は小さいもの、右は大きいものという原則に従い、<
<=
を使って、>
>=
を避けるという意見をよく耳にします。if (a>1 && a<100)
より、if (1<a && a<100)
(言語によってif 1<a<100
のような簡略の書き方もある)のほうが視覚的に分かりやすいでしょう。
ただし、単独なif (42<myNumber)
の使用は「ユーダ記法」(Yoda Condition)になってしまう欠点もあります。Python でユーダ記法で書いたら pylint の慣例違反のワーニング(misplaced-comparison-constant (C0122)
)が発動されます。
全ての状況で使える正解は無いですが、このようにコードレビューでお互いに異なる指摘をしてしまうのであれば、チームで方針を統一したほうがスムーズでしょう。
最後に、文頭に挙げた例について
[BEFORE]
err = checkError(); |
このコードの問題点が分かりましたでしょうか。
まずは、while 条件を見て、err
が真の時に正常処理のフローに入ることが分かります。// err means no error
のコメントをつければ良いでしょうか?
それでもまだ違和感が残りますよね。
その違和感の原因はcheckError
の返り値の内容を関数名から推測できないからです。
今の書き方では以下のような疑問がでてきます。
- 「check は単純に実行動作を表していて、何も返さないではない?」
- 「check の対象は Error なので、Error がある時は真を返す?」
- 「checkError の目的を考え、error のないことを望んでいるはずで、No Error の時は真?」
- 「Error 発生時、エラーの詳細を返したいので、Error のオブジェクト、あるいは Null を返すかも?」
曖昧過ぎますね。
このcheckError
関数の命名の改善案は3つあげられます。
- エラー有無を表現する Boolean を返す場合、
hasError
が良いでしょう - 1の反対の意味を示したい場合、
hasNoError
、否定形式を気になるならisValid
がよく使われています - エラー時には詳細のエラーオブジェクトを返し、正常時にはNullを返す場合、
getError
の方が直感的
上記を踏まえると次の用に修正できます。
[MIDDLE] 修正パターン (1)
hasErr = hasError(); |
[MIDDLE] 修正パターン (2)
isValid = hasNoError(); |
[MIDDLE] 修正パターン (3)
err = getError(); |
さらに、コードの重複も改善できます。これによって最初と最後2箇所同じコードが書かれていることで、メンテで修正した時にどちら片方を編集し忘れてバグが出てしまう不幸をなくせます。
次のコードは重複のコードを1箇所にまとめました。
[AFTER] 修正パターン1 (他パターンは同様なので省略します。)
while (!hasError()) { |
エラーオブジェクトの中身を取り出して、追加処理したい場合には次のようにも書けますね。
[FINAL] 修正パターン3
while (true) { |
ポイントまとめ
- コードを綺麗にするのは自分と他人の時間の節約に繋がります
- ソースコードを綺麗にするための3点
- 嘘をついてはいけない
- ロジックの複雑度を減らす
- 書き方の揺らぎを減らす制約ルールを作る
コーディングはキャンプに行くことと似ている点があると思います。キャンプ場にゴミがあったら、誰が捨てたかに関係なく全て持って帰りますよね?
チームのリポジトリに対しても同様に、チェックインする時にチェックアウトした時より少しでも綺麗にするようにしたら、確実に素晴らしいプロジェクトになるのではないでしょうか。