可読性について リーダブルコード Part2(ループとロジックの単純化)
- 29. ループとロジックの単純化まとめ
- 制御フローを読みやすくする
- 条件式をシンプルに
- 三項演算子は良し悪し
- 早期returnなどでネストを浅く
- 巨大な式を分割する
- 説明変数や要約変数で分割して名前に置き換える
- ロジックを単純にするため「反対」から問題を解決してみる
- 重複コードはまとめる(DRYの原則)
- 変数を読みやすさ
- 複雑な式を分割していなかったり、すでに明確である場合の一時変数
は不要である
- フラグを作ったら負け
- 変数のスコープを小さく
Editor's Notes
- 可読性についてPart2で、今回はループとロジックの単純化についてです。
- まずは前回のおさらいになりますが、リーダブルコードで一貫して言われていることは、「優れた」コードとは読みやすいコードであると一貫して言われています。続いて命名ですが命名はとても重要でどんな値が入るのか何をするのか明確で誤解の生じない命名にしようということ。次は一貫性のあるコードを書こうということで、例えば、ロジックをカテゴライズしてブロック化しようというもの。次はコメントすべきことと、そうでないもの、これは先ほど上記で申し上げたことがしっかりできていることが前提で、その他のコードでは読み取れない経緯や背景などを記入しよう、というのが前回のおさらいになります。
- 前回までは「表面上」の改善だったのですが、今回はより具体的にロジック部分の改善策になります。
大きく分けて3つあります。1つ目は「制御フローを読みやすくする」2つ目は「巨大な式を分割する」DRYの原則だったりです。そして3つ目は「変数の読みやすさ」についてです。
- まず最初は「制御フローを読みやすくする」方法についてです。こちらに関しては以下5つのアプローチがあります。1,~~。では、実際のコードを見ながら1つ1つ解説して行きたいと思います。ちなみにjavaScriptでは使用しない「goto文」や使用機会の少ない「do/while文」については省いています。
- では、条件式の引数の並び順についてです。いきなりですが、次のスライドでコードを写します。2パターンありますので、どちらが読みやすいか直感で答えてみて下さい。ではコードを写ます。
- はい、こちらのコードは2つとも実行処理は一緒ですが、比較対象を左右入れ替えただけになります。
1つ目は、「hoge.lengthがMAX_LENGTH以下なのか」もう一方は「MAX_LENGTHがhoge.length以上なのか」になります。
皆さんはどちらでしょうか?単純な興味本意でどなたかに聞いてみたいのですが。恐らく1つ目が読みやすいと答える方が多いと思います。
- 実は先ほどの条件式の引数の並び順にはある法則があります。左側は「調査対象」で値が変化します。一方右側は「比較対象」で値があまり変化しません。この法則は文法と一緒で例えば「もしあなたが20歳以上ならば」という文法は自然ですが、「もし20歳があなたの年齢以下ならば」というのは不自然です。文法に置き換えてより自然な並び順を選びましょう。ちなみに定数を左側に記述しようという「ヨーダ記法」というものがありますが、こちらは左側に値が確実に入っている定数などを記述することによって、比較処理と代入処理の誤用回避や、左側に値がなかった時に落ちてしまうのを回避する目的があります。ただし、現在では言語仕様が整備されたり、読み手にとっては違和感の強い記法になるため、必要性が失われつつあるというのが現状のようです。
- 続いては、if/elseの条件についてです。条件は否定形よりも肯定形を使う方が直感的だと書かれていました。そして条件は複数ではなく、なるべく単純シンプルになるよう努めましょう。画像のように、条件が複数あると見辛くなってしまいますが、シンプルに努めることで可読性が増します。
- 続いては三項演算子についてです。こちらは意見が別れる所でして、賛成は1行にまとまるので良いという意見。逆に反対者は読みにくいしデバッグし辛いという意見で別れます。個人的には基本はif文を使用し、三項演算子によってシンプルになるのであれば使うという判断かなと思います。
実際にシンプルな例と可読性の悪い例を見てみます。
- こちらは三項演算子を使用しても良いかなと思う例です。12時以上かそれ以外かで格納する値が変わります。原文を踏襲しているので、マジックナンバーだとかは抜きにして、この例であればまだ可読性が保たれている例かなと思います。続いては可読性が悪い例ですが、実際に現場であったコードを拝借しています。
- こちらは実際にあったコードですが、三項演算子に更に短絡評価が混じっている例です。fromIsAccountがあれば、undefined、なければfrom.prams.groupIdを代入し、そうでなければfrom.query.group_idを代入するものです。無理やり1行で済ませようとしていて、正直可読性はよくないです。行数を不自然に短くするよりも、他の人が理解するのにかかる時間を短くする、ということを念頭に置くようにしましょう。事実同じようなコードからバグが発生してしまいました。
- 続いては早期returnです。これは、そのままですが、画像のコードは年齢によって「成人」か「未成年」かを返す関数です。早期returnすることにより、elseも使わず可読性とネストが浅くなっていることがわかります。
- 続いては先ほどとも関連しますが、ネストを浅くしましょうという内容です。例に上げているコードを見ただけで違和感を覚えますが、ネストの深いコードは常にどこにいるかを意識しないといけないし、カッコの範囲を意識しないといけなかったり、可読性を著しく下げてしまい、読み手に「精神的ストレス」を与えてしまいます。1つ断言できることはネストは悪だということです。ちなみに、ロジックを抽出し関数へ切り出したり、まとまった情報をオブジェクト化することは個人的にかなり有効だと思っています。
- 続いて第二章は「巨大な式を分割する」です。1つ目は、、、。
- まずは「説明変数」についてです。式を分割する簡単な方法に、式を表す変数を使う方法があり、こういった変数は「説明変数」と呼ばれます。最初のコードはifの条件の中に、line.splitといった形で「式」が記述されています。一方、その下のコードは「説明変数」を利用した例になります。lin.splitの処理を一旦「username変数」で受けて、その変数を元に条件を記述することにより、line.split~~~の箇所が何の値なのかがわかりやすくなっています。
- 続いては「要約変数」についてです。先ほどのように式を説明する必要がない場合でも、大きなコードの塊を小さな名前に置き換えて、管理や把握を簡単にする変数のことを「要約変数」と呼びます。画像のコードは「該当ユーザは文章を所持しているか?」という内容です。上2つのifは変数をそのまま使用していますが、下2つのifはどんな値を比較したものなのか、要約した変数へ代入することで「関数で参照する概念」を事前に伝えることができています。
- 続いてはド・モルガンの法則です。ド・モルガンの法則で有名なのは、図のような円と円で表し、「真と偽を入れ替える」ことができるというのを示したものになります。この法則を使えば次のように論理式を読みやすくすることができます。
- 画像の論理式は全て等価です。論理式を等価な式に置き換えるには「notを分配してand/orを反転させる」「もしくはnotをくくりだす」ということができます。要は中身を反転させていると考えると覚えやすいです。正直これだけだとパッとイメージしづらいと思うので、次のスライドでイメージしやすいコードを準備してみました。
- こちらはより実例に近いコードかなと思うのですが、wordがnullではない、かつ空文字ではない時、という判定があったとします。1つ目のコードは条件に一致した時に何もしない条件分になってしまっているので、条件を否定でひっくり返してelseなくした、というのが2つ目のコードになります。コード自体はシンプルになりましたが、条件が「nullでないかつ、空でない場合でない場合」というなんじゃこれ状態になってしまうので、このような「否定の否定の〜」のような状態の時にド・モルガンの法則が有効になります。3つ目のように更に否定で括って&&を||にします。そして否定が二つで打ち消しあって肯定になり、4つ目のように最終的にシンプルな形になります。3つ目を飛ばして2から4の方がわかりやすいかもしれませんが、先ほどのド・モルガンの法則が有効活用されている例になります。
- 続いて、短絡評価についです。短絡評価は左が真であれば、右は評価しないというものですが、1つ目のifのようなコードは可読性が著しく悪いです。それよりも下に記述されているコードの方が可読性が高いことは一目瞭然です。「頭のいい」コードを目指すのではなく「読みやすいコード」を心がけましょう。
- 続いては、複雑なロジックと格闘するです。複雑なロジックを抜け漏れなく且つ、シンプルに実装するにはどうすべきか?ですが、画像のようなコードでは誰も読んでくれないし、本当に正しいのか初見では判断が難しいです。そういった時は「反対」から問題を解決してみるという手があります。例えば配列を逆順にイテレートしてみるとか、データを後ろから挿入してみるなどです。画像の例だと、「2つの範囲が重なるのか」というコードなので、その反対は「2つの範囲が重ならない」になります。実際に画像のコードを反対にした場合、次のようなコードになります。
- 先ほどのコードは、「始点と終点が重なっているかどうか」というロジックだったのですが、その逆「重ならない」にすると、2つのパターンしかありません。「一方の終点が、始点よりも前にある」か「一方の始点が終点よりも後ろにある」かだけで、その他は全て重なっている、というロジックです。1度に1つずつしか比較していないので、コードがシンプル且つ、抜け漏れなく実装できました。
- 続いては「DRYの原則」です。DRYの原則とはDon't Repeat Yourselfの略で、「繰り返しを避けること」という意味です。重複したコードを抽出し、メソッドとして切り出したりすることですが、重複したように見えて実は重複していないというケースもあるので注意が必要です。例えば、記述コードはほとんど一緒だが、投げるAPIが全く異なっているなどといったケースは、APIの仕様が変更された際に、吸収しきれなくなるため、重複したように見えて実は重複していないケースとなります。
- 続いて第三章は「変数と読みやすさ」についてです。変数を適当に扱ってしまうと以下3つの問題が発生します。~~~~。では次ではこれらをどう対処するかみていきます。
- まず1つ目は「役に立たない一時変数は削除する」です。「巨大な式を分割する」で取り上げた「説明変数」や「要約変数」はなぜ読みやすくなったのかというと、変数が巨大な式を分割して、説明文のような役割を果たしていたためです。今回は「不要な変数」が使われていたら逆に読みにくくなってしまうので、削除しましょうという内容ですが、画像のようなケースでは変数が役に立っていません。理由は3つあります。1つ目は「複雑な式を分割していない」から。2つ目は「最初のコードだけで明確である」から。3つ目は「一度しか使っていないので重複コードの削除になっていない」からです。これら3つに当てはまるのであれば「役に立たない一時変数」であると言えます。
- 続いて2つ目は「中間結果を削除する」です。左のように削除すべきindex番号を一度変数に保持して、その値を元に削除する、というケースでは、右側のように中間結果を削除しタスクを早く完了させることで簡潔にすることができます。
- 続いて3つ目はいわゆるflagです。茶谷さんからはflag作ったら負けと教えられていますが、flagは「実行を制御するためだけの変数」であり「実際のプログラミには関係のあるデータ」は含まれていません。多くの場合、うまくプログラミンをすればフラグは削除できますのでflagを作らなくても良いように努めましょう。
- スコープの種類には大きく分けると、「グローバルスコープ」と「ローカルスコープ」があり、ローカルスコープには更に「関数スコープとブロックスコープ」が存在します。スコープが大きいと、例えばグローバル変数になると値がどこでも書き換えることが可能になり、値を追っていくのがとても難しく、バグの元になってしまいますので避けましょう。また、C言語だと関数やブロックの先頭で変数を定義したりする文化があるようですが、多くの言語では最初から全ての変数を知る必要はないので、変数の定義は変数を使う直前に移動すればスコープが縮まります。またグローバル変数でなくても、変数を操作する場所が増えると、現在値の判断が難しくなってしまうため、変数を上書きできないようにするということも有効です。JavaScriptだとconstがそれにあたります。
例題ですと、左側が「foundとiとelem」が何度も書き換えられていることがわかります。これらの変数の使方を改善すると右側のように、中間変数を早めに返すなどで改善が可能です。
- では、最後に「ループとロジックの単純化」についてまとめたいと思います。
Part2は以上となりますが、ここでせっかくなので次回予告をさせて頂きます。
- 次回予告!可読性について Part3、第三部「コードの再構成」とは!??というテーマで行う予定です。
- 只今準備中ですので、もう少々お待ち下さい!ということで終わりたいと思います。
ご清聴有難うございました。