多くのプログラミング言語において、関数(メソッド)の利用は不可欠です。そのような背景に加え自由度が高いもあり、関数の引数はバグの温床となりがちです。
本記事では、筆者の10年以上のプログラミング経験で得た知見の一つとして、引数に起因するバグを少しでも減らすプログラミングのコツを解説します。
尚、「引数」(Argument)はプログラミングにおいて関数を呼び出すときに与える値を指す用語です。次のコードにおいてvalue1とvalue2が引数に該当します。
//value1とvalue2が「引数」です。
int Addition(int value1, int value2) {
return value1 + value2;
}
//引数が指定された関数は次のように使用します。
var result = Addition(2, 3);
print(result); //5
◇目次
引数のチェック:関数の外側の処理は信用せず、関数内で引数をチェックする
引数のチェック(内容が正しいか?)については「関数の外側でチェックしているので、関数内では引数の中身が正しいかチェックしない」という発想はなにかとバグの元となりがちです。
なぜなら、関数の外側に仕様変更が起きた時、関数の中身まで配慮して変更されるとは限らない、または考慮が漏れる可能性があるためです。外側で引数のチェックすることも場合によっては必要ですが、だからと言って関数内でのチェックを省くのは危険といえます。
具体例を考えてみます。ユーザー登録用の関数を構築する際、ユーザー名とパスワードの組み合わせを配列で受け取る設計としてみます。
//例として考える関数:ユーザー登録用処理
//(与えられたユーザー名とパスワードでユーザーデータを作成する。複数同時登録)
void UsersSubmit(string[] userNames, string[] passwords) {
//登録処理
}
この関数において目的から考えると、配列userNamesと配列passwordsの要素数は一致している必要があります。要素数チェックをせず実行してしまうと、一部のユーザー登録が漏れてしまう可能性があり、ビジネス的に致命的なバグとなってしまいます。
当然、この関数を呼ぶ側では、要素数が一致するようにロジックを組むでしょう。しかしこの関数を実装する側から見ればそれはスコープの外であり全幅の信用を置いていいものではありません。
そこで、外側でチェックしているか否かに関わらず、次のように関数内でもチェック処理を入れるのが安全な実装といえます。
//ユーザー登録用処理(複数同時登録)
void UsersSubmit(string[] userNames, string[] passwords) {
//【ここが重要】引数のチェック
if (userNames.Length != passwords.Length) {
throw new Exception(“要素数が一致しませんでした。”);
}
//登録処理
}
このように、引数のある関数を実装する場合は、引数の中身のチェックを呼ぶ側に依存しないようにすることが大切です。
引数に異常があれば、勝手な判断をせず例外を投げる
上でも例示したように、引数に異常を発見した場合は関数の目的を正常に達することができなくなるので、処理を中断するべきです。こう言ったケースで利用されるのが「例外のスロー」です。
例外がスローされると、例外は上層方向、つまり関数から見れば関数の呼び出し側に波及します。波及した結果、最終的にキャッチされなければクラッシュして処理が中断されることになります。投げた例外がその後どう扱われるかは関数から見れば気にするべきことではありません。(尚、当然ながら投げた例外がどう扱われるかは、チーム内で確認しておきましょう。)
//異常を検知したので例外をスロー
if (userNames.Length != passwords.Length) {
throw new Exception(“要素数が一致しませんでした。”);
}
大切なのは、引数の異常により処理が正しくできないことをきちんと通知することです。逆に最悪なのは、例えば「ユーザー名とパスワードの数が合わないので、少ない方の要素数で登録してしまおう」などと勝手な判断で処理してしまうことです。
呼び出し側としては、与えた引数が全て登録されることを直感的に期待しているはずです。それが叶わない場合はきちんと処理を中断して通知することが大切です。例外の運用については先行記事に詳細を記述しています。あわせてご参考ください。
クラスオブジェクトの引数を極力避け、言語標準の型を引数とする
バグを減らすには、その関数が単体でテスト可能であることが重要であり、さらに言えば「テストのしやすさ」を意識して設計する必要があります。クラスオブジェクトの引数は、このような特性に悪影響を与えます。
尚、「クラスオブジェクトの引数」とは次のように引数にインスタンスを受け取る関数を意図しています。
//ユーザークラスオブジェクトを受け取る関数に極力置き換える
void AlterUser(User user) {
//何らかの処理
}
//ここでUserクラスは例えば次のような定義されています。
class User {
int Id { get; set; }
string Name { get; set; }
}
逆に「言語標準の型を引数に取る関数」は次のような数値型や文字列(いわゆるプリミティブ型)、リスト型や配列などといった、プログラミング言語に標準で実装されている型を引数にとる関数を意図します。
void AlterUser(int id, string name) {
//何らかの処理
}
引数にクラスオブジェクトを取る関数を作ることによるデメリットは、具体的には次のような事項が挙げられます。
- テストする際にその引数用にオブジェクトを再生する必要があり手間(そのオブジェクトが大型だったり他のクラスに依存していたりするとかなりの手間)
- 引数のクラスオブジェクトの機能によって関数の実行成否が左右される可能性があり、純粋なテストができない
- 引数のクラスオブジェクトの仕様変更により関数の修正が必要になったり関数内でバグが起きたりする
このようなデメリットを希釈する手法に依存性注入(インターフェースを用いた依存性希釈手法)などはありますが、それでもインスタンスプロパティやインスタンス関数に依存することには変わりありません。尚、依存性注入についての詳細は先行記事をご参考ください。
以上より、引数の型については、代替可能な限りインスタンス型よりも言語標準の型を選ぶべきと言えます。
関数内外での多少の二度手間は気にしない
ここまで書いてきたことを実践すると、少なからず関数内外で二度手間が生じることになります。例えば、関数外(呼び出し側)でチェックしてることを関数内でもチェックしたり、呼び出し側にあるインスタンスを使わず内部でデータをフェッチしたりなどが考えられます。
本記事で紹介したのは「バグを減らす」ことを目的としています。ソフトウェアが継続的に変更されていく中では、バグが生じる可能性をできるだけ下げるメリットと、その二度手間に伴うコーディング工数やスループット負担は如何か?を天びんにかけて考えれば、大抵の場合は二度手間をかけたほうがマシなのではないでしょうか。各プロジェクトの特性も踏まえてご判断いただければと思います。
今回は以上です。お目通しありがとうございました。
記事筆者へのお問い合わせ、仕事のご依頼
当社では、IT活用をはじめ、業務効率化やM&A、管理会計など幅広い分野でコンサルティング事業・IT開発事業を行っております。
この記事をご覧になり、もし相談してみたい点などがあれば、ぜひ問い合わせフォームまでご連絡ください。
皆様のご投稿をお待ちしております。