嘘のコメントを減らす努力

見やすいコード表記について - タコブネ開発者ブログ と言う記事が目に留まったので今回はプログラミングのコメントについて.

プログラミングの可読性に関する議論は,個々の宗教に依るところも大きいので非常に難しいところです.前述した記事に書かれていた内容に関しては,その多くは「プロジェクト内で統一されていれば(どちらの表記が可読性に優れているか等の議論は)どうでも良い」程度の感想しかないなのですが,一点だけ気になった部分がありました.

以下,少し長いですが引用します.

SampleDTO sampleDto = (SampleDTO)testTableDAO.getPrimaryKey(dataDto.getId(),dataDto.getSubId(),dataDto.getStartDate(),dataDetailDto.getLineNo());

この長文について「1文が長過ぎるので読みやすくしてくれ」と言われた場合の、あまり好ましくない変更事例は以下になります。

SampleDTO sampleDto = (SampleDTO)testTableDAO.getPrimaryKey(
                              dataDto.getId(),
                              dataDto.getSubId(),
                              dataDto.getStartDate(),
                              dataDetailDto.getLineNo());

次も好ましくありません。変数を増やす事で、バグの温床を築いているようなものです。

String id = dataDto.getId();
String subId =dataDto.getSubId();
java.util.Date startDate =dataDto.getStartDate();
String lineNo =dataDetailDto.getLineNo();
 
SampleDTO sampleDto = (SampleDTO)testTableDAO.getPrimaryKey(
                              id,
                              subId,
                              startDate,
                              lineNo);
// もしくは
SampleDTO sampleDto = (SampleDTO)testTableDAO.getPrimaryKey(id,subId,startDate,lineNo);

プログラムの流れを考慮するなら、こう書くのが望ましいでしょう。

// ------------------------
// testTableからデータ取得
// ------------------------
 
SampleDTO sampleDto = ( SampleDTO ) testTableDAO.getPrimaryKey( dataDto.getId(),dataDto.getSubId(), dataDto.getStartDate(), dataDetailDto.getLineNo() );

極端に長い場合を除き、1列で記述できるものは半角スペースを利用して1列で記述します。またgetPrimaryKeyのパラメータが知りたい場合は、F2キーを押して、Eclipseの機能から参照して確かめます。このためにJavadocは必須です。

細かいパラメータを並べるのは、数行のプログラムならOKですが、実際にそのような機会はあまり無く、全体を流して読めるこの記述方法が重宝されるのです。

見やすいコード表記について - タコブネ開発者ブログ

上記の要求と解法を簡単に記述すると「引数が多すぎて概要の分かりづらい部分をコメントを利用する事によって解決する(読みやすくする)」と言う事になろうかと思います.

バグの温床となるコメント

さて,上記の一連の記述の問題点は,「無駄な変数を増やす事によるバグを警戒している割に,無駄なコメントを増やす事によるバグには無頓着である」と言う点です.プログラミングにおけるコメント論にも様々な議論がありますが,コメントを書く際に注意すべき点の一つに,実際のコードを修正した際にコメントの修正を忘れる事によって発生するコードとコメントの不一致,所謂「嘘のコメント」を(できるだけ)なくそうと言うものがあります.

よく「コメントをきちんと書け」ということを言われます。そのためか、初心者の書いたソースを見ると、必要以上に大量のコメントが書かれていることがあります。確かに全くコメントのないプログラムには読みにくいものが多いのは確かですが、何でもかんでもコメントをつけるのも同じく間違っています。余分なコメントは、プログラムとコメントの不一致、いわゆる「嘘のコメント」の原因になります。要するに「必要十分」なコメントを書くことが大切なのです。

省コメントのススメ - 職業としてのプログラミング

私は人の書いたコードを読むとき、普通はコメントが書いてあってもそれは見ない。中には、ひどいコードもあり、コメントを見ないと理解不能と言うコードもあるが、それでもコメントは見ないで理解しようとする。コメントを信じたばかりに、ひどい目にあった経験が何度もあるからだ。コメントはプログラマが書く物で、プログラマの文章能力は人による差が激しい。修正変更が繰り返されたようなコードは、たいていはコメントとコードが不一致になっている、またやっている内容を理解する時、コメントを読むよりコードを読むほうが早いことが多いと言うことも多い。

コメントが不要な分かりやすいコード

例えば,上記の指針でプログラミングを行う場合,TableDAO クラスの getPrimaryKey() メソッドを呼ぶ際にはほぼ「xxxTableからデータ取得」と言うコメントが付与される事になる事が予想されます.この際,もし getPrimaryKey() メソッドに何らかの修正が施され,処理の概要が「xxxTableからデータ取得」とは微妙に異なるものになってしまった場合,これまでに書いた全てのコメントを修正しなければならない可能性が出てきます.しかし,多人数でプログラミングを行う場合,自分が作成した(あるいは修正した)メソッドが実際にどこで使われているかを把握する事は難しいため,上記のような指針でプログラミングを行うと「嘘のコメント」を残す可能性が非常に高くなります.

蛇足ですが,上記のダメな例として「変数を定義する」と言うものが挙げられていましたが,例えば,バグの温床の理由が「変数への再代入によって発生するバグ」であるならば,final 修飾子を付けて再代入をコンパイルエラーにするような対策を行う事もできます.ローカル変数の final (C++ での const) に関する議論も様々で個人的にはそこまで強く勧めると言う訳でもないですが,少なくとも変に無駄なコメントを書くよりは幾分かマシだろうとは思います.

final String id = dataDto.getId();
final String subId = dataDto.getSubId();
final java.util.Date startDate = dataDto.getStartDate();
final String lineNo = dataDetailDto.getLineNo();

// 上記の変数への再代入を行おうとするとコンパイルエラーとなる.

SampleDTO sampleDto = (SampleDTO)testTableDAO.getPrimaryKey(id,subId,startDate,lineNo);

プログラムで表現できる事をコメントでやろうとしない

上記の例の解法を改めて見てみると,「複雑なメソッド呼び出しとなっている部分の概要を伝える」と言うものになります.これはコメントを使わずとも実プログラム上で表現する事ができます.

/**
 * TableDAO オブジェクトから該当データ(プライマリーキー)を取得する.
 * データを取得する際に使用する情報は,引数に指定された DataDTO オブジェクトの
 * Id, SubId, StartDate, および DataDetailDTO オブジェクトの LineNo である.
 *
 * @param table データ取得元のテーブル
 * @param data データを取得する際に使用する情報 (Id, SubId, StartDate) を保持しているオブジェクト
 * @param detail データを取得する際に使用する情報 (LineNo) を保持しているオブジェクト
 * @return TableDAO オブジェクトから該当するデータを返す
 */
private SampleDTO getDataFromTable(TableDAO table, DataDTO data, DataDetailDTO detail) {
    return (SampleDTO)table.getPrimaryKey(data.getId(), data.getSubId(), data.getStartDate(), detail.getLineNo());
}

// testTableDAO.getPrimaryKey(dataDto.getId(),dataDto.getSubId(),dataDto.getStartDate(),dataDetailDto.getLineNo());
// と記述されてあった部分を以下のように書き換える.
this.getDataFromTable(testTableDAO, dataDto, dataDetailDto);

// あるいは,testTableDAO, dataDto, dataDetailDto が全てクラスのメンバ変数であるならば,
// いっそ下記のような形でも良いかもしれない.
// this.getDataFromTestTable();

ブラックボックス化したい部分に対して新たなメソッドを定義する事で,元の部分には「コメントで説明する」代わりに「プログラムで説明する」事ができるようになります.プログラムで記述していれば,呼び出し元のメソッドに何らかの変更があった場合でも,例えば Java であれば Eclipseリファクタリング機能を使用する事により割と楽に名前 (getDataFromTable 部分) を変更する事ができるだろうと思います.また,バラバラに散っていたコメントをメソッドのヘッダ部分一つにまとめる事ができたので,コメントとプログラムの整合性を管理するための手間も減る事となります.

尚,上記の方法だと TableDAO.getPrimaryKey() メソッドが複数のクラスで(頻繁に)使用されるような場合には,呼び出し元のクラスの数だけ getDataFromTable() メソッドが生成される事になるのであまりよくありません.その場合には,TableDAOWrapper のようなラッパクラスを作成して TableDAO クラスの各種メソッド呼び出しを適度にブラックボックス化していく等,まぁやり方はケースバイケースになるかと思います.

コメントは難しい

ある程度プログラミングを行ってきた経験として痛感する点として「コメント(を書く事)は非常に難しい」と言うものがあります.プログラミングのコメントにおける理想としては「必要なコメントのみを過不足書く」と言うものですが,どれが「必要なコメント」かを見極めるのは非常に難しい作業です.私自身も,自分がメインに使っているプログラミング言語*1に関しては,コード部分の指針みたいなものはある程度持てているのですが,コメント部分に関しては今でも常に悩みます.

「何が必要か何が不必要かなど(特に初心者には)判別できるわけがないので取りあえず思いつく限りコメントを書け」と言う主張は,一理あるようにも見えます.しかし,先にも述べた通り,不必要なコメントは「嘘のコメント」を容易に生み出し,「嘘のコメント」はバグの温床の一つとなります.そのレベル(取りあえず思いつく限りのコメントを書くレベル)でしか判別できない所謂「初心者」が,コンパイラユニットテストの助けも期待できない(IDE によってはある程度助けてくれるのか?)コメントとコードとの整合性をきちんと管理できるかと問われると疑問符が付きます.

その意味では,コメントに助けを求める前に,まず,コード上のみで(可読性等も含めて)何とかできないかを模索すると言う姿勢が(特に初心者と呼ばれるうちは)大切なのではないかなと思います.

*1:ちなみに Java はまったくと言って良いほど触った事のない素人です.