[ Home on 246net ] [ Javaプロジェクト規約 ]
TAKAHASHI, Toru 2010年10月24日着手
![]() |
![]() |
![]() |
![]() |
プロジェクトとして共通に定義しておくとよい規約の一つ、コーディング規約についての規定サンプルです。 | ![]() |
![]() |
![]() |
![]() |
コーディング規約をどこまで細かく規定するか、については、常に論争となる点です。そこで、まず前提条件を明確化し、不要な論争を避け、有意義な議論ができる土台を定義します。
理想的なソフトウェア開発組織は、同じ開発文化を共有する開発者チームが継続していろいろな仕事をこなす形態です。
しかし、ソフトウェア開発組織を継続して保有(常備)するためには、定常的にコストがかかります。そのため、中にはソフトウェア開発組織の質よりも常備コスト削減を優先し、開発案件が発生したときに臨時でソフトウェア開発組織を編成し、案件終了時にソフトウェア開発組織を解散する、というやり方を取る会社があります。
継続するソフトウェア開発組織であれば、ソフトウェア開発能力はメンバー間の暗黙知による共有により十分期待できるのですが、臨時編成するソフトウェア開発組織では、能力はまちまち、暗黙知は共有できない、という状況ですから、形式知によって開発のやり方をそろえる必要があります。
コーディング規約の必要性、内容については、この開発組織の編成方法の違いによって変えていく必要があります。前者は、暗黙知が中心なので、明文化する規約には、細かな規定をつらつら書くよりも、理念、目標、目的、指針を示すのがよいでしょう。一方、後者は理念を掲げてもその場限りの組織ですし、抽象的な内容は解釈がばらつきますから、具体的な規定をしっかり明示するのがよいでしょう。
ここでは、開発案件ごとに臨時編成するソフトウェア開発組織を前提とします。
なお、臨時編成の組織で開発効率が高いかどうかは別な場での議論です。
理想的なソフトウェア開発組織では、開発者を新たに採用する場合、その人物の持つ技術、資質、モチベーションが採用担当者だけでなく同僚となる開発者からも十分に吟味され、十分と判断された者のみが新たに加わります。
しかし、プロジェクトごとに人を集めるやり方で、お金(時間当たりの単価)や会社間の付き合いで決まるような場合、開発者の技術・モチベーションはピンきりとなってしまいます。
後者の場合、コーディング規約を渡した場合のこまった反応として
といった対応が往々にして見受けられます。本来、規約は資料を渡すのみではなく、時間をかけて教育をして、最初の段階ではこまめにチェック(レビュー)をするものです。しかし、そのようなコストを認めないプロジェクト管理者のもとでは、そうもいきません。
結果として、コーディング規約の目的・心を理解して適用できる技術・資質・モチベーションのある人は(コーディング規約がなくても)良いコードを書くし、そうでない人は(コーディング規約があっても)悪いコードを書く、ということになります。
ここでは、技術・モチベーションがピンきりの開発者がいることを前提とします。
コーディング規約を読みもしない人、読んでも分からない人、などが混じったプロジェクトで生成するコードの一貫性をそろえ、品質を(ある程度は)確保するために、以下の方針で規約を準備します。
たとえば、コードのインデント・フォーマットといったスタイルについては、統合開発環境(NetBeansなど)のソースコード整形機能を使えば一貫性を保てます。
スタイルを含め、命名やコーディング指針は、静的検証ツールでチェックすることで問題個所を提示し、ツールは可能な限り全員が常に使用し、コンパイルの都度かソースコードを記述するそばからチェックするのがよいやり方です。
ただし、それぞれの規定の作成理由を合わせて記述します。
静的検証ツールのうち、全員が常時使用し、コーディングの都度チェックできるものを以下に列挙します。
ツール名 | 解析対象 | 概要 | 備考 |
Checkstyle | ソースコード | 主に命名・フォーマット・コメントの検証 | |
FindBugs | バイトコード | 既知のバグデータベースに合致する疑わしいコードの検出 | |
PMD | ソースコード | 命名、設計ルール、バグが疑わしいコード、重複(コピペ)コードの検出 |
バグと疑わしきコードの検出は、FindBugsが一日の長があるように思います。そこで、FindBugsを主の静的検証ツールにしたいのですが、ソースコードレベルの検証ができないため、CheckStyleと併用するのが、今の時点での判断です。
なお、PMDの持つ重複(コピー&ペースト)コードの検出は使い方を学んでいないので、今後この成果によってはPMDも併用という可能性もあります。
なお、ルールについては、
一貫性を重視するため、命名、例外、アクセス修飾子の使用などは、既存のJava(Java標準API)に合わせます。
たとえば、継承を前提として設計していないクラスの宣言に final 修飾子を付けるというプラクティスがあります。継承に絡んだバグを防ぐ意味ではよいプラクティスですが、一貫性の観点では、final を付けていないクラスを見た場合、継承を前提に設計されていると判断してしまいます。しかし、Java標準APIには使用されていないので、困惑が生じます。そこで、このプラクティスは採用を見送ります。
そこで、規約の規定は極力既存のJavaに合わせるものを採用します。
守らなければならないこと(規約・規定)を作成するのは、法律の文面を作成することに近く、実に困難なことです。また、そのような文面は、可読性が悪く(誤解のないよう念を入れて記述せざるをえないため)、読まれない規約の一因になります。
そこで、逆にやってはいけないことを列挙するという方法が効果的です。やってはいけない例を記述する際は、端的に一例を示せばよく、抜け・漏れ・誤解のないよう万全を期す文章でなくてもかまいません。また、読み手も、自分の習慣がNGに記載されているとぎょっとするので、効果的です。
ファイル・メソッドの行数、桁数、引数の数、結合度、サイクロマティック複雑度、などのメトリックス値について、静的検証ツールで検証できます。しかし、これをエラー同様必達事項とするのは、本質を見失った修正に走らせる危険があります。たとえば、
「行数を減らせばいいんだろう!じゃぁhogeメソッドの真ん中でぶった切って、hoge2メソッド呼ぶようにすれば簡単だ。あっ、ローカル変数がメソッド分けたから使えない。そうだ、全部フィールドにしてしまえ。さすがEclipse、リファクタリングメニューにローカル変数をフィールドに変換なんて便利なのがある。」
といった具合です。
メトリックスは、1つの指標だけで判断するものではなく、いろいろな指標を集めて総合的に判断するためのものです。
そこで、メトリックスは、コーディング規約で閾値を決めてそれを越えると指摘として扱うのではなく、計測をして品質指標として出力するものとします。
項目 | 検証ツール | ルール名 | 適用 | 備考 |
---|---|---|---|---|
ファイルの末尾は改行で終了する | CheckStyle | NewlineAtEndOfFile | A | 旧POSIX(IEEE Std 1003.1-2001)規定 改行コードがOSデフォルトと異なるときは要カスタマイズ |
改行コードはLFとする | A | NetBeansデフォルト | ||
文字コードはUTF-8とする | A | NetBeansデフォルト | ||
インデントレベルは4とする | CheckStyle | Indentation | B | NetBeansのソース整形で自動的に適用されるため、検証ツールでのチェックはしない。 ソースコードリポジトリにコミットする前に必ずソース整形を実施する。 |
1行には1ステートメントとする | Checkstyle | OneStatementPerLine | B | |
for初期化子が空のときは空白を入れない | CheckStyle | EmptyForInitializerPad | B | |
for列挙子が空のときは空白を入れない | CheckStyle | EmptyForIteratorPad | B | |
メソッド名と開き丸括弧の間に空白・改行を入れない | CheckStyle | MethodParamPad | B | |
トークン(配列初期化子の開き波括弧、排他論理和、前置--、前置++、.、!、単項-、単項+)の後ろに空白を入れない | CheckStyle | NoWhitespaceAfter | B | |
トークン(セミコロン、後置--、後置++)の前に空白を入れない | CheckStyle | NoWhitespaceBefore | B | |
行の途中で改行時、演算子の直前で改行する | CheckStyle | OperatorWrap | B | |
開き括弧の直後、閉じ括弧の直前に空白を入れない | CheckStyle | ParenPad | B | |
キャストの開き括弧の直後、閉じ括弧の直前に空白を入れない | CheckStyle | TypecastParenPad | B | |
TABコードを含まない。(インデントは空白で行う) | CheckStyle | TabCharacter | B | |
カンマ、セミコロン、キャスト演算子の直後に空白(改行)を置く | CheckStyle | WhitespaceAfter | B | |
トークンの前後に空白を入れる | CheckStyle | WhitespaceAround | B | |
開き波括弧は行末に置く | CheckStyle | LeftCurly | B | |
else, try, catchの前の閉じ波括弧は同一行に置く | CheckStyle | RightCurly | B | |
do, else, if, for, whileのブロックには{}が必要 | CheckStyle | NeedBraces | B | |
ジェネリクス記号(<>)の前後は空白を入れない | Checkstyle | GenericWhitespace | A | NetBeansのソース整形は不完全 |
不要な丸括弧 | Checkstyle | UnnecessaryParenthes | A | |
項目 | 検証ツール | ルール名 | 備考 |
意図が明確に表れる名前をつける | Clean Code | ||
エンコーディングを使わない。エンコーディングとはハンガリアン記法、メンバープレフィックス('m_'や'_'など)、インタフェースの'I'など | Clean Code | ||
すべての名前は英語でフルスペルを基本とし、略語*1は使わない。ID、連番はもってのほか。パスカル/キャメル形式において頭文字語*2(例:HTML)は2文字目以降を小文字とする(getHtml)。 | 型の命名と変数の命名で略語の扱いを変える(型の略語は厳しく制限、変数名はスコープに応じて緩く) | ||
大文字・小文字の違いで名前を区別しない | |||
定数の命名は大文字英数とアンダースコア、先頭は英字 | CheckStyle | ConstantName |
static finalフィールド |
ローカル変数(finalを含む)は英数字のキャメル形式、先頭は英字 | CheckStyle | LocalFinalVariableName LocalVariableName |
|
ローカル変数のスコープが狭い(10行以内でネストしたブロックを含まない)場合は、略語の使用を許可する | |||
for文のループカウンタは、ネストごとにi, j, k, …を使う | |||
イテレータは、itを使う | |||
引数は英数字のキャメル形式、先頭は英字 | CheckStyle | ParameterName | |
フィールド名は英数字のキャメル形式、先頭は英字 | CheckStyle | MemberName StaticVariableName |
非staticなfinal含む |
メソッド名は英数字のキャメル形式、先頭は英字 | CheckStyle | MethodName | |
インスタンスを生成するメソッド名は、create+クラス名とする(要検証) → Java標準APIは、newInstance か? |
|||
型変換メソッド名は、to+クラス名とする | JLS 6.8.4 | ||
アクセッサメソッド名(ゲッター)は、get+属性名とする。但し戻り値型がbooleanの場合はtrue/falseがわかる名前とする | |||
ミューテータメソッド名(セッター)は、set+属性名とする。 | |||
パッケージ名は英小文字と数字とアンダースコア、先頭は英字。サブパッケージ名の重複は可 | CheckStyle | PackageName | デフォルトから変更^[a-z]+(\.[a-z][a-z0-9]*)*$ |
クラス型名は英数字のパスカル形式、先頭は英字。その内容(責務・役割)を表す名詞や名詞句とする。 | CheckStyle | TypeName | JLS |
例外クラスは末尾がExceptionで終わる | |||
インタフェース型名は英数字のパスカル形式、先頭は英字。抽象的な型として用いるときは、説明的な名詞や名詞句が適しており、振る舞いの場合は形容詞を用いる(Runnable, Cloneableなど) | JLS | ||
インタフェース名に接頭辞Iを付けない | |||
インタフェース実装クラスの末尾にImplを付けない | |||
型変数名は、簡潔(可能なら1文字)な大文字英字を使用する。基準例 ・コンテナ型の要素型にはE、マップの場合はキーの型にK、値の型にV ・任意の例外型にX ・特に区別しない場合はT、複数使用するときはアルファベット順でTの近傍の文字を使う |
JLS | ||
data, process, make, value,
btn(button), cntrl(control), cnvrt(convert), pnl(panel), stmt(statement), sts(status), trnsctn(transaction),
ans, app, addr, attr, buf, com, con, creat, dep, gen, prop, res, ret, val,
項目 | 検証ツール | ルール名 | 備考 |
パッケージのJavadocをpackage-info.javaに記述する | CheckStyle | JavadocPackage | |
protectedまたはpublicメソッドはJavadocコメントを記述する | CheckStyle | JavadocMethod | |
デフォルト、protectedまたはpublicクラスはJavadocコメントを記述する | CheckStyle | JavadocType | |
protectedまたはpublicフィールドはJavadocコメントを記述する | CheckStyle | JavadocVariable | |
CheckStyle | JavadocStyle | ||
項目 | 検証ツール | ルール名 | 備考 |
import文でワイルドカードを使わない | CheckStyle | AvoidStarImport | |
sunパッケージをimportしない | CheckStyle | IllegalImport | |
重複したimport、無駄なimportはしない | CheckStyle | RedundantImport | |
未使用なimportはしない | CheckStyle | UnusedImports | |
修飾子の指定順序は、public, protected, private, abstract, static, final, transient, volatile, synchronized, native, strictfpの順 | CheckStyle | ModifierOrder | |
重複した修飾子、無駄な修飾子は記述しない | CheckStyle | RedundantModifier | |
メソッド途中にブロック(制御構文ではなく単独の{...})を設けない | CheckStyle | AvoidNestedBlocks | |
空のブロックを書かない | CheckStyle | EmptyBlock | |
finalを使い、継承される想定をしていないクラスを明示する | CheckStyle | FinalClass | |
finalを使い、オーバーライドさせないメソッドを明示する | |||
finalを使い、再代入しない変数を明示する | CheckStyle | FinalParameters | |
ダブルチェックロッキングイディオムを使わない | CheckStyle | DoubleCheckedLocking | |
セミコロンだけの行を書かない | CheckStyle | EmptyStatement | |
equals()をオーバーライドするときはhashCode()も合わせてオーバーライドする | CheckStyle | EqualsHashCode | |
ローカル変数の名前は同じクラスのフィールド名とかぶらない | CheckStyle | HiddenField | |
インスタンス生成は直接newするのではなくファクトリーメソッドを呼ぶ | CheckStyle | IllegalInstantiation | |
代入はトップレベルで行い、式の途中で行わない(for文を除く) | CheckStyle | InnerAssignment | |
マジックナンバーは使わない(-1, 0, 1, 2を除外) | CheckStyle | MagicNumber | 除外 |
switch文には必ずdefault句を記述する | CheckStyle | MissingSwitchDefault | |
throws句に冗長な例外の記述はしない(2回記述、継承関係にある型、非チェック例外) | CheckStyle | RedundantThrows | |
booleanの評価式では、冗長な記述をしない(== true、|| true、!false) | CheckStyle | SimplifyBooleanExpression | |
booleanをリターンするときは、冗長な記述をしない | CheckStyle | SimplifyBooleanReturn | |
継承を考慮した設計をしているか | CheckStyle | DesignForExtension | |
staticメソッドとstaticフィールドだけからなるクラスはコンストラクタを公開しない | CheckStyle | HideUtilityClassConstructor | |
インタフェースは型を定義するために使う。定数を定義するためには使わない | CheckStyle | InterfaceIsType | |
フィールドはprivate。static finalのみpublic可 | CheckStyle | VisibilityModifier | |
配列型変数宣言で角括弧は型名に付けて書く | CheckStyle | ArrayTypeStyle | |
コメントに特定のキーワードを書いた箇所をチェックする。("TODO"、"FIXME") | CheckStyle | TodoComment | デフォルト"TODO:"から変更 |
long型のリテラルは小文字のlではなく大文字のLを付与する | CheckStyle | UpperEll | |
よくある慣習では、ファイル先頭コメントには、そのファイル名、システム名やプロジェクト名、サブシステム名、そのファイルで定義される型名(クラス・インタフェース・列挙型)、著作権、適用するライセンスなどを書くことになっている。
こうしたファイル先頭コメントの例を次に示す。
/* * TemperatureSensor.java * * Created: Sat Oct 06 02:24:16 2001 * * Project: Weather report * System: Weather Monitoring System * Subsystem: Sensor Subsystem * * Contains: TemperatureSensor class * TemperatureSensorChangeHandler class * * Copyright 2001 by Example Company, Ltd. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * under the License. */
しかし、本当に必要なのだろうか?ここで書くとされている項目について検討をしてみる。
上記検討から提案するファイル先頭記述は、コメントではなく適切なpackage文があればいい。
package com.example.wether.monitor.sensor;
予想される反論
作成者・修正者を記載するか否か?
数年後、ソースファイルに記載されている作成者/修正者に問い合わせることができるなら記載してもよいだろうが、会社として開発しているなら、個人に問い合わせることは考えられない。記載の必要はない。誰がいつ何を修正したかを管理するのは、ファイル上の記述ではなく構成管理である。
1ファイル1クラス(型)編成を採用する場合の、ファイル内宣言順序を検討する。
package com.example.wether.monitor.sensor; | 最初の行はpackage文を記述、ファイル先頭コメントは不要 |
import java.util.List; import java.util.logging.Logger; |
次はimport文の記述。 単一の型インポート宣言および単一のstaticインポート宣言のみを使用し、オンデマンドの型インポート宣言およびオンデマンドのstaticインポート宣言は使用しない。 staticインポート宣言を乱用しない(最低限に留める) |
/** |
クラスのドキュメンテーション・コメント |
public class TemperatureSensor implements Sensor { | クラスの定義 |
private double temperature; |
フィールドの定義 |
public TemperatureSensor() { : } |
コンストラクタの定義 |
インデントをTAB文字と空白文字とどちらで行うかは、コーディング規約の論争の火種の1つです。論争になる場合、前提を明確にして整理しないと意見が対立したまま収束しないので、まず、前提を明確にします。
インデントに空白を使うと、どのエディタ、Webブラウザ、コマンドプロンプト/ターミナル上でのファイルを標準出力、diff/patch/mergeツール上の扱い、いずれにおいても問題は生じません。
一方、インデントにTAB文字を使う場合は、以下の問題が生じます。
注記) TAB文字を8空白相当、と記述している場合は、TAB文字が機械的に8空白文字に展開されるのではなく、8の倍数の桁まで右に空白文字を入れることを意味する。4空白相当のときは、4の倍数の桁となる。
予想される反論とその想定問答
マジックナンバーは避けるべしと思っているが、座標計算での90.0や180.0は、定数にした方が可読性が悪い(そもそもなんて変数名にするんだろう、NINTY_DEGREEかDEGREE_90か、どちらにしても90の方が明瞭簡潔だし保守性も差がない(そもそも変更があるなら物理法則がひっくり返るときだ)。
マジックナンバーを避ける場合を上げると