🍦

早期リターンを書こう

2021/10/15に公開1

自己紹介

メディアエンジンの岩元(github)と申します。新卒から10年程度はメーカーの社内SEでCOBOLや色々な言語で書きつつ社内のいろいろなシステムに関わり、色々回り道をしてメディアエンジンにジョインました。なかなかバグを作ることの才能に恵まれているため、修正しやすいコードの書き方を覚えました。

はじめに

多くの方は早期リターンまたはガード節と呼ばれるコーディングテクニックについてご存知だと思います。読みやすいコードを書く人が常に早期リターンを使うというわけではありませんが、必要な箇所では必ず使っています。

基礎的かつベテランでも活用できていない場合もあります。大事な事と思いますので、まだ目にしていない方のために書きました。

早期リターンとは

簡単に言えば return を使うことで if などによるネストを減らすテクニックです。例を見たほうが早いと思います。

早期リターンの例

ユーザーがシステムを利用可能か調べる isActiveUser という関数があったとします。

適用前

ユーザーには、利用開始日と利用期限または停止フラグがあるとすると下記のようなコードになります。今回は例としてjavascriptで書きます。(改善後を見れば良いので、条件式は読み飛ばしてください。)

const today = new Date(2021, 10, 9)

function isActiveUser(user) {
    if (user != null) {
        if (user.startDate <= today && (user.endDate == null || today <= user.endDate)) {
            if(user.stopped) {
                return false;
            } else {
                return true;
            }
        } else {
            return false;
        }
    } else {
        return false;
    }
} // isActiveUser -> 15行

const startDate = new Date(today);
startDate.setMonth(startDate.getMonth() - 1);

const user = {
    name: "iwa", 
    startDate: startDate,
    endDate: null,
    stopped: false
};

console.log(isActiveUser(user)); // -> true

この条件式を見て、どのような条件であればシステムを利用可能としているのか分かりづらいです。

適用後

これを早期リターンを用いて書くとこうなります。

function isActiveUser(user) {
    if (user == null) { 
        return false;
    }
    
    if (today < user.startDate) { 
        return false;
    }
    
    if (user.endDate != null && user.endDate <= today) {
        return false;
    }
    
    if(user.stopped) {
        return false;
    }
    
    return true;
} // isActiveUser -> 18行

これなら条件を難なく読み下すことができるでしょう。
(行数は若干伸びましたが・・・)

書き方のコツ

例はブール値を返すものですが、もっと複雑なロジックがある場合も同様に行います。

  • 初心者の方は条件式は反転させる訓練をする(<= の反転は > など)
  • 異常系の処理はできるだけ手前でリターンする
  • 逆に正常系の処理が最後に残るようにしたほうが慣例的にわかりやすい
  • if が多くなってもできるだけ条件は分けて書いた方が読みやすい
  • どうしても条件が複雑な場合は条件判断のメソッド抽出を行う
  • return の前に書くべき処理が多い場合は、メソッドを抽出してできれば1行にする
  • 読みづらくなければ if ~ return は1行にまとめて良い(コーディング規約で許される場合)

例えば例のものに適用するとこの様になります。(return の値を編集するメソッドはかけませんでした。)

function isActiveUser(user) {
    if (user == null) return false;
    if (today < user.startDate) return false;
    if (terminated(user)) return false;
    if (user.stopped) return false;

    return true;
} // isActiveUser -> 8行

function terminated(user){
    if (user.endDate == null) return false;
    if (today < user.endDate) return false;
    
    return true;
} // terminated -> 6行
// 計15行 = 8行 + 1行 + 6行

terminated は条件判断のメソッドに更に条件判断を足しているので過剰な感じがありますが、ユーザークラスを定義してその中での処理であれば違和感が無いかも知れません。

利点

早期リターンにはネストが減ること以外にも様々な利点があります

orand

早期リターンにはネストが減ること以外にも様々な利点があります。

処理が軽くなる:

ロジックの整理がしやすいのもありますが余分なロジックを実行する前にロジックを出ることができます。ループを使う処理などでは特に効果があります。

行数が減って見通しが良い:

今回、リファクタリングの途中で若干行数が増えましたが、一般的に早期リターンで書くと一般的に行数は減ります。行数で読みやすさを測るのはあまり良いとは言えませんが、たくさんスクロールが必要なメソッドより、スクロールしなくても読めるメソッドの方が良いです。デバッグの簡単さは、効果の中でもわかりやすいものの一つです。

テストが書きやすい:

複合的な条件の場合、組み合わせてテストしないと安心できませんが早期リターンで書かれたソースは後ろから順に条件を網羅すればわかり易くかけます。いわゆる条件網羅が簡単でテストカバレッジが高くなります。(テストは正常系から書いたほうがわかりやすいです。いつか書きたい題材です。)

条件の追加がやりやすい:

適切な部分に if を差し込むだけなので条件の追加が簡単です。逆にネストされた条件式の適切な部分を探すのは結構たいへんです。

メソッドの目的が明確になる:

早期リターンが使えない場合の多くがメソッドの分割が甘い場合です。メソッドの分割方法には色々とありますが、早期リターンを使うためには戻り値が明確である必要があります。

戻り値が明確だと、メソッドの目的も明確になります。

コーディングの基礎力が上がる:

メソッドの分割はコーディングの基礎です。メソッドの目的を明確になると、適切な命名ができるようになります。また、オブジェクト指向プログラミングの利点を享受するには、メソッド分割の能力が必要とされます。
これは異論があるかも知れませんが、個人的には再帰処理も早期リターンを使わずに書くのは難しいです。

異論・反論

コーディングスタイルに関する話題は反発も多いです。実際やってみせると納得してもらえることがほとんどです。

これまで、見聞きした異論反論を並べてみます。

「一箇所しか使わないメソッドを定義すると見づらい」:

メソッドの分割が嫌いな方の意見です。メソッドの分割に慣れていないことが考えられます。またメソッド名と戻り値が揃っていないプログラムを多く見てきた方だと思われます。

「returnが複数あると見づらい」:

ネストが深くない場合は早期リターンを使わなくてもよいと思います。ネストが深い場合、 return がひとつだろうと複数であろうと見づらいです。各所に returnを仕組んで早期リターンへのリファクタリングをしてください。きちんとリファクタリング(2段階までのネストが目安)できれば元よりわかりやすいコードになっているはずです。

「早期リターンしなくても良いように事前に引数のチェックをするべきだ」:

そういうコーディングスタイルもありだと思います。ただ、それはこの話題とは別の話です。早期リターンで書いてみてそれを事前チェックに書き換えて比較してみればわかります。

「異常系より正常系を先に書きたい」:

すごくわかります。しかし、そのためにネストして書くと条件の結果が離れて終い異常系がすごく読みづらくなります。このデメリットは正常系が読みやすいメリットを上回ることが多いです。また、結局 if と条件が正常系の前に残ります。
メソッド抽出を駆使して条件を含め1行で解決したいものです。

「動いているコードは触れるべきでは無い」:

多分別の話題ですが、そのコードが動いているにも関わらず変更の度に何度もバグを出してませんか。早期リターンとメソッドの抽出でリファクタリングして潜在バグを一層して、再発を防止した経験がいくつもあります。

「リソースの開放漏れを起こす」:

確かにそういう場合には注意が必要です。幸いそういうケースは多く無いのでそちらで使いましょう。
また、言語によっては try ~ final のようなリソースを使うのに良い書き方が存在します。抽象クラスや関数渡しなどのテクニックも存在します。これらを使うと早期リターンを使ってもリソースの開放漏れは起こりません。

「コーディングルールで決まっているって言われた」:

しょうがないです。別の職場で別件ですが私も似たような経験があります。ルールの決定権者にそれ言われると説得が不可能なんですよね。(私の場合は決定権者が誰なのか、何階層離れているのか不明だった。)

構造化プログラミングでは、途中リターンは禁止されている」:

全くの誤解です。 return が存在しなかった言語での問題で、サブルーチンの最後尾への goto を使うことでサブルーチンの独立性を担保するテクニックです。現在はその心配は無くなりました。

おわりに

なんだかんだ言って、その時々またはチーム毎の正解がありますので、ある種のテクニックとして理解いただければと思います。

最後に、弊社ではエンジニアやデザイナーなどの職種で積極的に採用中です!

弊社チームの紹介ページがあるのでぜひ、見に来てください!
https://mediaengine.notion.site/ba128c5708fc480198f5d8c9440a7062

Discussion

shirouzushirouzu

主旨には全く賛同ですが、「構造化プログラミングでは…」について「全くの誤解です。return が存在しなかった言語での問題で…」は少し違うのではと…。

"The most common deviation from structured programming is early exit from a function or loop. "
https://en.wikipedia.org/wiki/Structured_programming#Early_exit

"Strong adherents of structured programming make sure each function has a single entry and a single exit (SESE). "
https://en.wikipedia.org/wiki/Return_statement#Multiple