プログラム開発でのボトルネックになるのはプログラマにとってのソースコードのわかりやすさだ。他のメンバーがこれまでに書いてきたコードをメンテナンスし、機能を追加し、しかも高速化したいときに、一番のボトルネックは、既存のソースコードのわかりやすさだ。
####わかりやすさが必須な理由
わかりやすくなければ、設計の妥当性を検証できないし、テストすることもできない。既存のルーチンを置き換えることができる場合の判断基準は何なのか、計算結果の精度がどうなると改良なのか、どういう結果ならば高速化を受け入れることができるのか、それらを判断するには、そのルーチンが何をしているのか、(何をすることが期待されているのか)のわかりやすさだ。それが満たされない限り、既存コードは何をしたかったのかを解読するために、1行も書けずに苦しむことになる。
####わかりやすければできること
・テストの作成を指示できる。
・コードの高速化が必要ならば、その関数の高速版の開発を第三者に依頼できる。
コードを書くのに忙しい人であればあるほど、わかりやすいコードを書くべきです。わかりやすいコードならば、他の人の協力を得られます。
####わかりやすい関数に必要な条件
・関数の引数は少ないほどよい。
・正しく使うための引数の前提条件がないこと(少ないこと)。
・引数の状態による場合わけがないこと。
・参照渡し(&)では実現できないことを実現する場合に限って使うポインタ渡し(*)
・引数に配列を渡す場合には[]を使うこと。
double sum(double data[]);//推奨
double sum(double *data);//推奨しない
・関数の仮引数はin,outの区別がつきやすい記述をしていること
copyObj(const dataType &src, dataType &dst);//推奨
copyObj(dataType *data1, dataType *data2);//推奨しない
####目的と意図とがわからないのが一番つらい
何を実現したいのかということは、本来は業務の中で伝えられているはずなのに、十分に伝えられていないことが、えてしてあります。ソースコードの中の記載だけではわからない開発の目的・意図を知るために必要な情報を、ソースコード中のコメントに記載しておきましょう。使用したアルゴリズムが記載されている論文の情報、部署内の開発仕様書の情報、プログラムを理解するうえで必須の情報を記載しておきます。(しかしながら、古くなったコメントは、置き去りにされ嘘をつくようになるなどの問題もあります。)
そのコードを理解する上で重要な部分は、誤解を生じにくくするための記述をしておきましょう。
例:フーリエ変換の定義
数学でよく用いられるフーリエ変換(フーリエ積分)には、数通りの定義がある虚数の指数関数の符号の+/-、積分の規格化の係数を、フーリエ変換と逆フーリエ変換でどのように定義するかである(一方の係数を1とする流儀が2つ、両方に同じように分配する流儀)。そのため、6通りのフーリエ変換の定義が存在する。そのため、自分が今使っているフーリエ積分がどの定義になっているのかを明確にして、つじつまのあう一貫性のある処理をすることです。
(フーリエ変換のコードを利用するプログラマは少ないので、何を言っているのかわからない例になってしまった。(m_ _)m)
例:標準偏差の定義
「標準偏差」として言っている値がサンプル数nで割ったものか、n-1で割ったものか、どちらの値を求められていて、どちらの値を実装しているのかは確認が要ります。サンプル数が1桁などと極端に少ないときは、この間違いが判断ミスにつながる可能性が無視できません。本来、不良として除外しなければならなかったものを良品として受け入れてしまうことにつながりかねません。統計に詳しい人に聞いてみてください。
例:「控除」という言葉には、意味合いの違う使い方が多すぎる。ウィキペディアの控除の項目を読めば、そのことが確認できる。幸いそのような分野のシステムを組んだことがないが、そのような分野では、何から何を差し引く行為なのかを意識して注意深く理解する必要があるでしょう。実現しようとしていること、ソースコードの記述の両方についてのわかりやすさが、プログラムを開発する上で必須であることがわかります。
####聞き取りをしよう
そのコードを書いた人が、まだそばにいるという場合には、聞き取り調査をしよう。その関数のドキュメンテーションコメントを書き加えているときに、意味がわからないことがあったら、可能な限り聞き取りをしよう。そのときには、相手への敬意を保ち続けるように心がけよう。前にコードを書いた人は、おそらく多忙な状況の中で、とにかく動くプログラムを書き上げることに力を尽くしたはずだ。それが異常に深いインデントのコードであれ、1000行を超える関数であれ、とにかく目的の処理に対して動くプログラムを書き上げたのは大変な労力だったはずた。聞き取り調査ができる幸いな状況にあったら聞き取り調査をしよう。それが既にあなたがコードを読んで理解したことの再確認であっても、聞き取って確認することは価値がある。
####コードの断片を動かしてみる
そのコードが何をどう処理するのかわからないときは、その関数を呼び出して実行してみるだけのプログラムを作ります。オープンソースのライブラリの場合でも、そのライブラリ関数は実際にどのような結果を返すのか、どのように使うものかは、コードを眺めているだけではわからないときがあります。
ですから、引継いだコードの場合でも、そのようなアプローチをすることで理解を助けることは可能です。
追記:コードの断片を動かしてみて明らかになること
コードの断片を動かしてみると、引数の組み合わせではエラーを生じることがあります。その知見も重要なことです。呼び出し側が作り出す引数の組み合わせでは、そのようなエラーを生じる組み合わせを生じるかどうかを確認しなければならないことがわかります。
####そのコードはわからなくても、何をすべきかがわかる場合
#####・手作業によるトレース
引継いだコードの該当の箇所がわからなくても、そのプログラムは何をすべきかがわかっている場合には手があります。どのようなデータの入力に対して何をどう処理して、どのような結果を出力すればよいのかを、手作業を含めて実験してみることです。そうすることで開発すべき内容についての理解が深まります。
#####・別言語・別ライブラリによる実験
開発すべき内容についての理解は、別の言語とライブラリを使って実験した方がよい場合もあると感じます。動作検証がされていない(たぶんうまくいくはずの)自動化された効率のよい気のきいたシステムよりも、手作業でのデータの入力があっても、素性のしれたライブラリを使って実験をしてみる。
何をすべきかがわかる場合には、このような手法を試みることで、対象の作業の意味がわかってきてソースコードが理解できるようになります。
私が多用するのはOpenCVなので、Pythonからimport cv2としてOpenCVのpythonバインディングを利用しています。OpenCVを利用したコードの断片を、pythonで記述しなおして動作させることで、その意味の理解を助けています。別言語や別ライブラリを用いて、実験してみることが目的のコードの理解につながることがあります。
わかりやすさを優先することです。わかりやすさを実現することで、何を実現したいのか、それにはどのような方法を選ぶことができるのかが、メンテナンスを引継ぐ人の理解を助けます。その結果、効果的な書き換えが可能になり、計算対象に固有の特徴を使うことで、計算量を数分の1に減らすこともあります。
とにかく、プログラムのわかりやすさを優先に開発しよう。プログラムのわかりやすさは、プログラムの開発を楽にして、プログラムの開発を効率的にします。
####入出力の変数のassert()は関数の仕様を明示的に説明する
assert()を使って関数の入力値・戻り値をチェックする。
そうすると、プログラムのDebugモードでの実行時のチェックになるだけではなく、この関数は、どういうデータが入ってくることを想定しているのかを、ソースコードの読者にヒントを与えていることでもある。assert(img.channels()==1)と書いてあれば、カラー画像ではなくグレースケール画像を想定していることを明示していることになる。
####std::vector<>で表現できることにポインタは用いない
vector型の場合、要素の追加方法も、要素が最後であるかどうかの判定方法も標準化されていて、誰が書いたコードでも同じように理解できます。しかしながら、ポインタを用いて実装している場合には、最後の要素が特定の値になっていることで、それが最後の要素であることを判定している場合があります。そのコードを書いた人は、最後の要素であることを示す特定の値が何であるかを知っていますが、そのコードを初めて読んだ人は、その特定の値を知りません。
(注意:ポインタを使うのが悪いといっているのではなく、そのデータ構造を利用する側が、むき出しのポインタ演算をしないと使えないインタフェースになっていることを嘆いているのです。ポインタの値をインクリメントするコードが、ソースコードのあちこちにあってカプセル化されていないことが、のぞましくないのです。)
std::vector<>型とそのmethodを用いて記述すれば、何をしようとしているのかは一目瞭然となる。それと同等の機能を独自実装で、独自の関数名で行っているときに、その機能を正しく理解するのは、理解するための不要なコストを生じさせてしまう。
(STLのメソッドを使っているだけならば、単体テストすべき項目も大幅に減る。独自データ構造を"効率のため"に利用している人の場合、そのために開発に発生する手間と、開発者にとってのわかりやすさを損ねて、開発効率を損なっていることにまだ気づいてくれていないことが多いのが残念な限りです。)
・c = a > b ? a : b;
よりは、c=std::max(a, b)の方が理解しやすい。
・c = a > b ? a - b : 0;
よりはc=std::max(a-b, 0);の方がわかりやすい。
if( (a < x) && (x < b)){}
の方が
if( (x > a) && (x < b)){}
よりも私には理解しやすい(数直線がイメージできるせいだろうか)。
否定を含む表現はわかりにくくなりがちなので、否定を使ったほうがわかりやすい場合を除いて、なるべく否定を使わないようにする。
プログラムの全体構造と、個々のモジュールの役割、それぞれの関数の機能が十分にわかりやすくなっていれば、プログラムの開発を加速する方法はいくらでもあります。実行速度を改善することも、その仕様が明確になっていればいくらでもあります。
大規模システム開発に失敗した事例の記事には、その開発について十分明快な仕様をfixすることができなかったことが書かれています。
まとめ:
わかりやすいコーディングが、プログラムの開発を加速します。
参考情報
「プログラミング作法」第1章 スタイルをぜひ同僚にも勧めてください。文法的に正しいコードを書けば十分ではなく、一貫性・慣用句などをわかりやすさに必要なことを述べています。
追記(2024.09.08):意図が不明なものは改善できない。
- 何をしたいのか意図がわからないものは改善できない。
- コードを読解していけば、どのように動作するものかは解読できる。
- 解読したとおりに動作しているかの確認をするためにtestを作ることもできる。
- しかし、それがその開発に着手した人の意図どおりかは知りようがない。