独立性の高い機能を追加するときには


開発に参加したプロジェクトの中で、新たに独立性の高い機能を実装するときには、以下の点を心がけたいと思う。

・独立性の高い処理だけをプロトタイピングして、動作を検証する。

 この時点では、アルゴリズムの検証がしやすい言語とライブラリの組み合わせを使う。
 

 私の場合だとMATLABやpythonといったインタプリタ系の言語を使うことが多い。そのアルゴリズムが目的の動作をしていることをグラフィカルに検証できるライブラリを十分にもっていることが重要で、実際に実装するアルゴリズム自体よりも、その動作が期待したものであることを示すためのコードの方が圧倒的に多い。

・独立性の高い処理のうち、移植先で使う機能だけをターゲットの言語で再実装する。

移植先では使わず、テストや動作確認のための機能は、移植しなくてよい。

 私の場合だと、実装したアルゴリズムだけをターゲットの開発言語で行い、検証を楽にするツールはpythonなどのインタプリタ言語やバッチ、シェルスクリプトを使うことが多い。たとえば、1つのファイルに対する処理を行う機能をコマンドライン引数を受け取って処理をするように作っておき、それをインタプリタ言語内でsystem()関数で動作させるようにする。このようにすると、ターゲットの開発言語によるソースコードは簡潔にできるので、ソースコードを不必要にいじって、コードの信頼性を損なうことがなくなる。

・検証用の処理は無理に移植しない。

 本番の実装には必要ではない検証用の処理は、無理にターゲット言語に移植しない。

・独立性の高い処理は独立したファイルにする。

 独立したファイルにすることで、目的ごとに別ファイルになるので、その独立性の高い機能をいつ誰が更新したかがSVNで明確になる。また、ファイルごとの分担の所在が明らかになる。

プロトタイプ実装で、実際にアルゴリズムの有用性を確かめていくなかで、プロトタイプ実装をメンテナンスしやすいように、リファクタリングをしていく。プロトタイピング実装の初期版では、動作の検証をするためのコードとアルゴリズム自体とが混在になっている。それを分離していき、不必要なライブラリのimport, #includeをなくしていく。そうすると、何が独立性の高い処理であって、どう設計すべきなのかが見えてくる。
その後。プロトタイピング実装のリファクタリングで検証した設計を、ターゲット言語で実装する。そうすることで、いきなりターゲット言語で設計を行うよりも自信をもって設計を進めることができる。

・プロトタイピング言語では不要で、ターゲット言語では重要な部分を反映させる。

C++言語では、関数の引数にconst修飾子がつけられるものにはつける。
引数に渡す変数は参照渡しをする。値渡しをすると不必要な大量のデータのコピーを生じるので、参照渡しにする。ポインタ渡しにすると参照渡しよりも注意深いコーディングが必要になるので、参照渡しで十分なときは参照渡しにすることが多い。

・ターゲット言語での関数・メソッドの戻り値を何にするか?

MATLABやPythonのようなインタプリタ言語では、関数が複数の変数を返すことができる。そのため、引数には入力値、出力値は必ず戻り値にするコーディングが可能になっている。C/C++言語では、関数やメソッドの戻り値が単一のデータ構造に限られるので、ポインタ渡しや参照渡しで関数やメソッドの引数で出力値を受け取ることが多くなる。そうはいっても、出力をcv::Mat型の関数の戻り値で受け取るようにすると、利用するコードの記述が簡単になる利点が多い。
cv::Mat somefunc(const cv::Mat &img);
で宣言してあると、
cv::imwrite("junk.png", somefunc(cv::imread("lena.jpg"));
のような記述が可能になる。
その分、1行での処理の内容が濃くなり、コードの理解がしやすくなる。
メモリの確保と解放の部分をほとんど意識しなく済むようになり、アルゴリズムに集中できる。

従来のIplImageの場合だと、IplImage型のポインタ変数を宣言したときに、IplImage型のポインタがNULLである場合、領域の確保がしてある場合、領域に値が設定してある場合、既に存在するIplImage型への参照である場合などがあり、プログラムのあちこちに、処理が分散していると、IplImage型の領域の解放し忘れるパターンを生じていたりした。

・その独立性の高い処理に固有の名前空間を導入する。

固有な名前空間を使うことで識別子が他のソースコードとぶつかることがなくす。追加になったモジュールを使う側、モジュール自体の両方でusing 名前空間;を書かない。

・公開するインタフェースを絞り込む。

 追加モジュールの公開するインタフェースを絞り込む。
そのファイル内でしか呼び出されない関数にはstatic宣言をつける。

・公開するインターフェースに十分なドキュメンテーションコメントを書く。

私の場合 Doxygenを使ってドキュメンテーションコメントを書く。

・単体テストしやすくしておく。

 通常動作させるためのメインのプログラムの他に、テスト用のプログラムを生成するプロジェクトファイルを作っておく。追加実装の機能が意図した動作をするものであるかどうかのテストを、小規模なプログラムで動作できるようにしておく。
 必ずしも最初はCppUnitに代表される単体テストのフレームワークに載らないかもしれないが、それでも、通常動作のプログラムとは別の小規模なテストプログラムでテストできるので、プログラムのテストがしやすくなる。

移植先のインタフェースを見直す

移植先のソースコードのどの部分で、追加機能を使うのか、移植先のソースコードをじっくり見直そう。すんなり合うインタフェースがない場合には、1つの関数が、あまりにもたくさんのことをしすぎていて、「関数の抽出」を必要としている可能性がある。

こういったことで、追加機能の実装をトラブルなしに遂行したい。

--

付記:
快適なC++生活のためにスクリプト言語を使おう pythonで検証してからのC++実装