チームでプロジェクトを実施する場合も、個人で組む場合も、ソースの書き方は統一しておいたがいいです。その方が、ソースが綺麗で見やすいですし、書くときにも迷わなくて済みます。
変数名などの命名に始まり、インデントやブロックなどのフォーマット、コメントなど、プロジェクトが始まる前に統一して決めておきます。インデントなどのフォーマットは、Eclipseの自動フォーマット機能を使うのが便利です。納品前には必ず整形しましょう。
ここでは、具体的にコーディング規約をこうすべきということは述べません。その代わり、経験上気づいた点を適当に列挙していきます。
一般的な原則については、リファクタリングのやり方に従うのがよいです。
クラス名、メソッド名は具体的に書きますが、変数名(クラス変数、メンバー変数、ローカル変数)は、意味がわかる範囲で短くします。ただし、決まりきったもの、Connection, Exceptionなどは短くていいです。また一時的な意味しかないカウンタなども1文字でよいです。しかし、カウンタがfor文の外で使われたり、あちこちで使われるとなると、意味がわかるものにします。
短すぎても長すぎても可読性を損ないます。要は把握しやすさがポイントです。
ある変数名を適切につけたつもりでも、それと似たような意味の変数が必要になったとき命名に困ります。xxx2とかいう変数名はできれば避けたいところです。名前の衝突を避けるためには、一つのメソッド内に登場する変数の数が少なくなるように、メソッドを機能単位で短く切り出すことです。あるいはクラスを分割します。クラスが変われば同じメンバー変数名が使えますし、メソッドが変われば同じローカル変数名が使えます。
違うメソッドで同じ役割をしている変数には同じ変数名をつけていると、コピー&ペーストがしやすく汎用的になって便利です。可読性を高めるためにもそのようにした方がよいです。コピー&ペーストはよくないと言われますが、骨組みやJSPなどはどうしても同じ部分が出てくるのでやはりコピー&ペーストをすることになります。
開発を開始する前に、変数・命名規則を作ります。以下の点を考慮して決めます。
・長さを調整するために、省略形を使うか、使わないか。また連番を振って自動的に命名するか。
・区切りを_にするか大文字にするか
・英語名で統一するか日本語のローマ字音写を使うか
DBのカラム名については、長すぎるのは推奨できません。プロジェクトにもよりますが、頻繁に入力するものは短い方が望ましいです。
また、変数命名の仕方として、型やスコープ(クラス、インスタンス、ローカル、パラメータ)で接頭辞をつけるという方法もあります。
例) ResultSet cResultSet クラス名の頭に小文字をつける。 resultSet クラス名の頭を小文字にする。 resultset 全部小文字にする。 rs rset 省略する 原始型の場合 int iArg char cArg String sArg Object oArg long lArg byte btArg boolean bArg 一時変数 temp- work- メンバー変数 mArg arg_
こういう指針は、プロジェクト依存で程度問題です。。人によってセンスが違うので、どれが正しいとは言えません。大方の意見を総合すればある範囲に収まりますが、プロジェクトに参加したメンバーが恐ろしく偏った感覚の持ち主がそろっていることもあります。あるいは変わったセンスのリーダーに押し付けられることもあります。
統一性を出すために、変なルールでも従ったほうがいい場合もあるし、あまりにも変すぎる場合は、自分の関わったパートだけまともにするという方法もあります。ただし、自分のセンスは正常だと全員が全員思っているのでこれまた問題を引き起こすのですが。
画面にID番号をつけるのは、古い技術者であればよく行います。プロジェクト略称+連番にします。
といったことがあげられます。基本的にDBで使うIDと同様のメリットがあります。一方欠点としては、
といった点が指摘できます。さらにこの画面IDをクラス名につけるプロジェクトもあります。E504Servlet, E504.jsp, E504Model, E504HomeBeanなど。これには賛否両論があり、古くからの技術者はこれが当たり前、オブジェクト指向から入った技術者はこれは全くの論外で、適切な名前をつけるべきという意見です。名前の衝突についてはパッケージで適切に分離すれば問題にはなりません。結構似たようなクラスが出てくると命名に困るのですが、この辺はオブジェクト指向をする者としては何とか考え出す必要があるでしょう。
抽象化のところの話でも述べたように、可変性を持たせるために、可能な限り、値の設定を外出しにします。プロパティファイルやデータベースに格納したり、またJSPやPLSQLのように変更が即座に反映されるものが望ましいです。なるべくコンパイルせずに反映できるようなものが望ましいですが、できないなら、なるべく変更箇所が一箇所で済むように、値の指定を特定しやすいConstantクラスとか上位のクラスに設定します。
設定ファイルはできれば、同じ場所で、修正が容易になるようにするのが望ましいです。かつ柔軟なカスタマイズが可能にします。デフォルト値は設定ファイルのコメントの中に記述して置くようにします。また設定ファイルの中でパス指定が多い場合は、設定ファイルの中でBaseディレクトリを指定しておいて、各プロパティの中で相対パスで指定できるのが望ましいです。
しかし可変性も度を過ぎると、スクリプト言語を作ることになります。柔軟にできる代わりに、そこでのデバッグはソースコードよりも難しくなったりします。またそのスクリプト言語自体のテストも厄介です。
設定ミスは、エラーにするか、警告にするか、デフォルトを使用するか、無効にするかを決めておき、無視する場合もログには出力されるようにします。
ソースの中のコメントは重要です。
・メソッド定義については必ず、javadoc形式に従って書きます。
・また、一見してわかり辛いロジックを書いている場合、何をしているのかコメントを書いておきます。
・あえて複雑なロジックを組んでいる場合には、その理由を記述しておきます。そうでないと、後で読んだ人が無駄な処理をしていると判断して安易に書き換えてしまうかもしれません。
修正履歴の記入は以下のタイミングで行われることがあります。
・開発時点
・結合テスト後
・リリース後 保守・機能追加時点
基本的に、最後のリリース後にのみ行い、正規に客にリリースしたものを基準にします。もし開発時点や結合テスト時点のものがあるなら、それは単なる開発者のメモ書きに過ぎないので、リリースの段階ですべて削除します。折角、綺麗なソースでも、修正コメントが多いと読みづらくなります。人によっては、開発中・単体テスト中に書き加えますがこれは全く無駄です。
必要なのは既存のものに誰がどんな修正を加えたのかをはっきりさせることです。これはテスト済みのソースに対して必要なことで、それによって動作が変われば、仕様や設計が変わることにもなりかねないからです。その違いを仕様や設計に反映させるために必要なことです。つまり、あとで見る人間は仕様書や設計書を読んでからソースを見るが、その際に、食い違いがソース上はっきりしているのが望ましいのです。あとは、修正によって不具合が出た場合に、どこの修正で起きたのかサーチしやすくしたり、容易に元に戻せるようにすること、それからテストのポイントを絞れるなどのメリットがあります。
こういうコメントは、ソースファイルとは別個に記述するように規約を設け、開発環境を統一するか、あるいはコメントにランクを設け、非表示にできるようなエディタがあれば望ましいと思います。
ログは、アプリケーションが正常な動作をしているのを確認するためと、障害時に障害箇所を特定するのに必要なものです。
・ランク付け
ログの出力をランク付けし、デバッグ用と運用用に分けます。それぞれさらにランクを分割し、7段階ぐらいにするのが望ましいです。例えばApacheのログレベルは、emerg、alert、crit、error、warn、notice、info、debugというふうになっています。
もっとも出力される数が多いレベルでは、
がいいでしょう。詳細なステップをたどることができます。
ログは、たとえIDE(開発環境)でデバッグ実行することができたとしても、結合テスト時にはIDEを使って実行することはしないので、必ず出力できるようにする必要があります。
・例外時のトレース出力
Exceptionは丸めないことです。可能な限り出力することが必要です。完全に翻訳可能なもの、エラーの原因が完全に把握できるものはよいですが、そうでない限り生で出すのがよいのです。他のExceptionを拾って、それを隠蔽して自作のExceptionを投げるのは好ましくありません。意図的に隠す意味があるのかどうか検討しましょう。自作のExceptionは、他のExceptionによるのではなく、独自のエラー判定によって投げるのがよいです。どうしても丸める必要があるなら、デバッグオプションで丸めたExceptionも出力されるようにするのがよいです。
また、例外を無視できる場合でも、何らかの異常を示唆するものは、エラーを返さなくても、ログには例外のトレースが出力されているべきです。この際、無視できる旨、ログにいっしょに出力されているのが望ましいです。でないと対応すべき例外と勘違いしてしまいます。例えば、仕様上、数字が入力されていない場合、0とするとするような場合、
try { idx = Integer.parseInt(s); } catch (Exception e) {}
こういう場合は不要ですが、仕様上、SQL文の発行に失敗しても無視するとなっていたとしても、ログにはそれを記述した方がいいのです。
エラーが発生した場合は、無視できる場合を除いて基本的にユーザにはエラーを知らせるべきです。エラーが発生したにもかかわらず正常終了にしてしまうと、ユーザ側で気をつけなければならないことになります。ただ、エラーメッセージは、内部の実装を隠すために、あるいはコンピューターに詳しくないユーザのために、平易で、心配させないような文言にする必要があります。
・必要な値は出力する
また、エラーの原因が特定しやすいように必要な値は出力します。SQLExceptionなら発行したSQL文はその際、もしくは発行時常に出力します。設定ファイルの記述ミスなら、ファイル名と問題個所を出力するようにします。
ログにnullとしか出力しないのは最低です。e.getMessage()はあまり有益な情報を与えてくれません。スタックトレースがあればどこでNullPointerExceptionが発生しているかわかります。
また、開発中は仕方ありませんが、運用中にNullPointerExceptionが出力されるような事態は防ぐべきです。それはNullチェックを行っていないか、ロジックが不完全(初期化を行っていないなど)であることが原因しています。
原因不明のエラーの大半は、適切にログが出力されていないことが原因しています。ただし、自分では作っていない部分、OSから出てくるものに関しては、それ以上追求は難しいものですが、自分で作る部分についてはきちんと作る必要があります。ファイルかコンソールに出力する必要がありますが、コンソール出力の場合は、デーモン起動した場合に隠されてしまいます。
通常のアプリケーションでは、例外処理はコードの大半(場合によっては8割も)占めます。適切に例外処理が行われているアプリケーションは、起こりえるあらゆる事態を想定したよいアプリケーションということができます。逆に、何か不具合が起きるたびにプロセスが落ちるようでは、安心して業務に使うことができません。
だからといってあらゆるエラーをキャッチして、ログに出力はするものの、そのまま継続運用するのも望ましいとは限りません。異常を起こして、他のメモリ領域を破壊したり、ファイルを壊してしまい、他のプロセスにも影響を与え、業務に悪影響が出るような場合は、異常終了した方がいいでしょう。
また原因不明なエラーが起きた場合、つまりユーザの操作ミスではない場合、速やかに対応するため、例外を管理者に通知するような仕組みが必要です。ログの監視ソフトを使うか、自前でメール送信するかなどですが、前者の場合監視ソフトに合わせた形のフォーマットで出力する必要があります。
長すぎるメソッドは、途中でどのようにローカル変数が書き換えられるか見えてきません。後の方のブロックが参照したときにはnullになっていたということもあります。
べた書きになっている長いメソッドは、たらい回しにされているメソッドを追いかける必要はなく、頭から読んでいけばいいので、読むのはある意味楽で、ローカル変数を共有でき、メンバー変数や引数、オブジェクトを減らせるといった利点があるのですが、反面、冗長(同じロジックが頻繁に繰り返される)、ローカル変数が共有されてその影響度がわかりにくい(=グローバルと同等になる)、などの欠点もあります。
ただし、何でもかんでもモジュール化するのは望ましいとはいえません。というのは、ノードが離れれば離れるほど、値の検査が必要となるからです。
連接で文が続いている場合は必要のない値の検査が、関数・メソッドとなるとどこから呼ばれるかわからないため必要となります。それがprivateメソッドの場合はまだコントロールできてもpublicメソッドとなるとどこからでも呼ばれる可能性があります。さらにネットワークごしとなると、タイミングも図りにくくなるため、同期を取るために様々なチェックが必要になります。
例えば、以下の連接がある場合、2行目を実行するときに、sがnullであるかどうかをチェックする必要はありません。
String s = "xxx"; int len = s.length();
しかし、これがメソッド化されて切り離されている場合、
String s = "xxx"; method1(s); public void method1(String s) { int len = s.length(); }
他のところからもこのメソッドを呼ぶ可能性があると、sのnullチェックは必須となります。privateやprotectedのように限定されている場合は、assertを使ってNot Nullが条件であることを明示し、プログラミング最中にエラーに気づくようにさせますが、publicの場合は基本的にassertだけで済ませるべきではありません。外部に公開しているインターフェースの場合は、チェックを厳密にします。
一般的に分散すること、長い関数は分割することが推奨されていますが、分割にはこのようなデメリットがあります。チェックが増えればそれだけ試験工数が増えることを意味するので注意が必要です。
中には、1メソッドは、1画面に収まる長さ(25行)にすべきだという人もいます。そうでないと悪いソースだとみなします。こういう頭の固い人の言うことは無視しましょう。25行では大したロジックは書けませんし、コメントを入れたり、switch文を使う場合、すぐに長くなってしまいます。こう言うとその人は、そういうソースを書かなければならない構造にしていること自体がおかしいとあくまでも短いメソッドにこだわります。一画面に収められれば、一目で把握できるため、いいにこしたことはありませんが、長いという理由だけでは分割する理由になりません。
細かい点がいろいろあります。(あまりまとまっていません)。
・構造化プログラミングを保つために、最後でreturnするようにするかどうか。
途中でreturnすると出口がどこにあるか見通しが悪くなります。かといって、if文で分岐させて、最初にはじかれるものが、最後のreturn文で帰るというのも見通しがよいともいえません。条件が多いと、if文のネストが多くなり見づらくなります。そのメソッドに入ってくる条件になければ、if文単体で戻してしまった方がいいのです。
構造化定理では、入口・出口がそれぞれ一つである場合、GOTO文を使っていたプログラミングを連接・選択・反復を使って置き換えることができると主張していますが、実際にプログラムを組むとき、GOTO文はご法度ですが、出口を一つに限定する必要はありません。
private String chageData(String s) { if (s == null) return null; ...... }
・不正の判定にExceptionのスローか返り値か
Javaでは例外処理が容易になっているため、戻り値の値を判断するよりも例外を使ったほうが簡単に見えます。しかし、複数の不正値の判定がある場合、Exceptionを使うよりも、返り値で判断した方がいい場合もあります。
・机上デバッグをする
頭の中で、いろいろな値をシュミレーションします。結構面倒ですが、最低でもnullチェックはしましょう。どこかでオブジェクトがnullになって、NullPointerExceptionが発生しないかどうかをチェックするのです。渡されたオブジェクト、参照しているオブジェクトがnullの場合正しく処理されるかどうかを確認します。nullチェック、ゼロ(レングス、カウント、値)チェックはどんな場合でも必要です。そういうくせをつけるようにします。値は常にあるとは限りません。値がない場合どういう対処をするか必ず組み入れるのです。
ただだからといって、ありえないエラーチェックは必要ありません。例えば前の文で、オブジェクトをnewしていながら、次の文でnullチェックをする必要はないのです。
・メソッドを記述する際、実行するときの前提条件をコメントに記述しておきます。
引数の値だけではなく、メンバー変数やデータベースの値なども関係する場合は記述します。あるロジックを実行する際、変数やデータストアがどういう状態になっていないといけないか、意識しておく必要があります。
モジュールの独立性を高めること
あるモジュールが正常に動作するための前提条件、つまりそれ以外は動作が保証できないもの、が多い場合その独立性は低いと言えます。
これは単純に結合度の問題ではありません。例えば、引数がNULLではないこと、データベースのどこのカラムに正しい値が入っていること、呼び出す他のモジュールが正常な値を返すことなどが、ここで言う条件になります。もしこれらの条件が満たされない場合でもきちんとエラーハンドリングができているなら、これは独立性が高いと言えます。一般の結合度は、他のモジュールをどれだけ必要としているか、他のモジュールもアクセスする共通変数を持っているかなどで決まりますが、ここではそうではなく、そのモジュールが参照する変数に対する強度です。
外に開かれているモジュールは独立性が高い必要があります。クラス内のprivateメソッドなど呼ばれる条件が決まっているなら、低くて構いません。しかしもしクラスが肥大化し、複数の人間がそのクラスに対して手を加えるようになると、モジュールの動作を正確に定義できる必要があります。
・ソースにデバックコード、デバックメソッドを残さないようにします。
一時的にデバッグし、値の変化を調べるために、ログを仕込むことはあっても、デバッグが終わったら取り除きます。必要なトレースは、正規のログとして仕込んでおきます。規定以外の、デバッグ出力、コンソール出力は削除するのが原則です。
・使っていないロジックは削除、最低でもコメントアウトします。
余計なメンテナンスが発生します。長々としたメソッドを解読してから使われていないことが判明するのは馬鹿らしいものです。後任者に無駄な時間を取らせないようにしましょう。
・INSERT文には必ず列名をつける。SELECT文にもつける。
これはデーブルの定義が変わり、列が追加になったときにSQLエラーが発生するのを防ぐためです。
・データの生存期間に配慮します。
永続的データは、DBやファイルに保存します。ただし、DBに保存する情報も種類によって生存期間を設けます。マスターテーブルは永続的ですが、トランザクションやログは期間を決めて削除し、論理削除したマスターテーブルのレコードも期間を決めて削除します。
一時的データは、メモリに格納されます。これにもスコープがあり、サーバー起動期間(クラス)、インスタンス、メソッド(ローカル)、セッション、リクエスト、といった生存期間があります。これらの生存期間はデータの共有とも関係してきます。
・データの共有について配慮します。
データの種類によって、共有の度合いが異なります。共有範囲が広ければそれだけ密な結合をしていることになります。グローバル変数が嫌われた理由はどこからでもアクセスできてしまうからです。DBは当たり前のように使われますが、グローバル変数以上に共有範囲は大きいです。
共有されるもの | 共有する側 | |
---|---|---|
引数 | 呼び出し側と呼び出される側 | |
メンバー変数 | 同じオブジェクト内、もしくはオブジェクトへの参照を保持しているもの | |
クラス変数 | 同じJVM上 | |
セッション | 同一セッション上 | |
リクエスト | 同一リクエスト上 | |
コンテキスト | 同一サーバ上 | |
共有メモリ | 参照を持つもの | |
リモートオブジェクト | RPC呼び出しをする者 | |
ファイル | アクセスするもの | |
DB | アクセスするもの |