私はシステムの専門家が生産性を最大なものにすると固く信じている。アプリケーションは存在する期間の大半が開発ではなく保守期間であるとの認識から、私のコードを開発しやすくするだけではなく、保守しやすく、拡張しやすくするのに役立つ事柄に非常に興味を持っている。忘れてはならないことは、今日書いたコードが今から何年もの後まで使われ続け、きっと他の誰かによって保守され、拡張されていることである。コードは可能な限りきれいに(clean)そして理解しやすいように作る努力をしなければならない。なぜならば、これらのことは保守と拡張を容易にするからである。
この章では4つの話題について扱う。
メソッドは正しくフルスペルの英語で、先頭の単語が小文字で,先頭以外の各単語の頭は大文字となるように命名しなくてはならない。先頭の単語は能動態の動詞とするのが一般的な実践である。
openAccount(), printMailingLabel(), save(), delete()
この規約によってメソッド名を見ただけで、目的が分かるようになる。この規約によって名前が長くなるため開発者のタイプ量が増えるが、コードの理解しやすさが向上することによって埋め合わせ以上のものがもたらされる。
フィールド(クラスの属性・プロパティ)に値を設定したり(Setter)読み出したり(Getter)するためのメソッドに関する命名規約であり、詳細は後の章で述べるが、ここにまとめを載せる。
フィールドの値を返却値とするメソッドである。フィールド名の前に'get'を付け加える。ただしフィールドの型がbooleanであるときは、getの代わりに'is'を付け加える。
getFirstName(), getAccountNumber(), isPersistent(), isAtEnd()
この命名規約に従うことで、メソッドはフィールドのオブジェクトを返却していることが明確になり、boolean型のgetterについてはtrueかfalseを返却することが明確になる。この標準のさらなる利点は、JavaBeans開発キット(BDK)のgetterメソッドの規約に則っていることである。不便な点は、せいぜい'get'が余分なタイプ量を必要とすることくらいである。
英語表現としては、'is'ではなく、has や can とすることが正しい文法である命名がある。例えば、hasDependents()とか canPrint() とかがそうである。ただし、JavaBeansの仕様ではhasやcanをアクセッサメソッドとは認識しない点が問題である。その場合、isBurdenedWithDependents()や isPrintable() のような命名に変更する方法もある。
フィールドの値を変更するメソッドであり、mutatorとも呼ぶ。フィールドの型にかかわらずフィールド名の前に'set'を付け加える。
setFirstName(String aName) setAccountNumber(int anAccountNumber) setReasonableGoals(Vector newGoals) setPersistent(boolean isPersistent) setAtEnd(boolean isAtEnd)
この命名規約に従うことで、メソッドはフィールドのオブジェクトを設定していることが明確になる。この標準のさらなる利点は、JavaBeansのsetterメソッドの規約に則っていることである。不便な点は、せいぜい'set'が余分なタイプ量を必要とすることくらいである。
オブジェクトを生成するときに必要な初期化作業を行うメソッドであり、常にクラス名と同じ名前になる。例えば、CustomerクラスのコンストラクタはCustomer()となる。大文字小文字も同一である。
Customer() SavingsAccount() PersistenceBroker()
この命名規約はSunによって設けられ、厳格に従わねばならない。
クラス同士の結合を極小化するというよい設計を行うために、メソッドの可視性を設定するときは可能な限り制限を強くする。publicにする必要のないメソッドはprotectedにする。protectedにする必要のないクラスはprivateにする。
可視性 | 内容 | 使用方法 |
---|---|---|
public | どのオブジェクト/クラスからでも呼ぶことができる | そのクラスのクラス継承階層に属さない外のオブジェクトから呼ばれる必要があるときに定義する |
protected | このクラスおよびサブクラスから呼ぶことができる | クラス階層の中で必要とされる振る舞いを提供するときに使う |
private | このクラスの中から呼ぶことはできるがサブクラスから呼ぶことはできない | クラスに特有の振る舞いを提供するときに使う。しばしばリファクタリングの結果作られ、ある特定の振る舞いをクラス内にカプセル化するときに使用 |
可視性が指定されない。デフォルトあるいはパッケージ可視と呼び、時にフレンド可視と引用される。メソッドは同一パッケージに属する他のクラスにはpublic同様だが、パッケージ外のクラスにはprivateと同様である | 興味深い特徴だが注意深く使うこと。顧客といったビジネス概念を実装するクラス集であるドメインコンポーネント(Ambler、1998b)を構築した際に、コンポーネントのパッケージ内のクラスだけがアクセスできるよう制限するために使用した。 |
メソッドのドキュメントの仕方は、理解しやすいかし難いかによって、保守性と拡張性の決定要因となる。
全てのJavaメソッドはメソッドコメントと呼ばれる何らかの種類のヘッダを含むべきである。ソースコードの先頭に、メソッドを理解するのに必須となる情報を書く。この情報の内容として書くべき項目例を以下に示すがこれだけに限らない。
第三者がそのコードを再利用できるか否かを判断しやすいように、メソッドが何をするのかドキュメントする。第三者がそのコードがどういった文脈で使われるか理解しやすいように、メソッドがなぜそうしているのかドキュメントする。 また、そのようなドキュメントがあれば第三者が新たに変更が必要となるかどうか判別しやすくなる。(新たに変更する理由は、最初にコードを記述したときの理由と矛盾が生じた結果である)
引数を使用しているならば、メソッドに渡さねばならない引数が何であってどのようにメソッド内で使われるかを記述する。この記述は他のプログラマーがメソッドにどんな情報を渡せばよいか知るのに必要となる。これには1.4.2節で論じたjavadocの@paramタグを使用する。
返却値があるならば、メソッドが返す返却値/オブジェクトを他のプログラマーが適切に使うことができるように記述する。これには1.4.2節で論じたjavadocの@returnタグを使用する
メソッドの未解決の問題点をドキュメントし、他の開発者がそのメソッドの制約を理解できるようにする。もしそのバグが1つのメソッドに収まらないものであるなら、メソッドドキュメントではなくクラスドキュメントの方に記述する。
メソッド内で発生するすべての例外をドキュメントし、他のプログラマーにどの例外を補足しなければならないか分かるようにする。これには1.4.2節で論じたjavadocの@exceptionタグを使用する
メソッドの可視性を選んだ理由が他のプログラマーにとって疑問になるかもしれないと思うなら、その決定理由をドキュメントしておく。 例えば他のオブジェクトからは呼ばれないメソッドをpublicな可視性にしたときかもしれない。 こうすることにより、あなたの思考が他のプログラマーにも明白となり、なぜあなたが疑問を生じることをしたのか悩まずにすむ。
メソッドがあるオブジェクトを変更するとき、例えば銀行口座のwithdraw()メソッドが口座残高を変更する場合、このことを記述する。これによって他のプログラマーがそのメソッドを起動すると対象オブジェクトにどのような影響を及ぼすかを正確に知ることができる。
メソッドを変更するときはつねに、いつ変更したか、誰が変更したか、なぜ変更したのか、誰が変更要求を行ったか、変更結果の試験は誰が行ったか、いつ試験して製品に組み込む検証を行ったかを記述する。 この履歴情報は、コードを保守し拡張する責務を帯びた保守プログラマーのために重要である。 注記:この情報は本来ソフトウェア構成管理・履歴管理システムに属するもので、ソースコード上に書かれるものではない!こうしたツールを使わないのならば、ソースコードに書く。
コードがどのように動作するかを理解する簡単な方法の1つが使用例を見ることである。 メソッドをどのように使用するか1つか2つの例を書くことを考慮する。
事前条件はメソッドが正しく機能するための制約であり、事後条件はメソッドの実行が完了した後に真となる特性もしくは表明である(Meyer,1988)。 事前条件と事後条件によってメソッドを書いているときに下した仮定を記述し、メソッドをどのように使用するのか正確な範囲を定義する。
並行性は大部分の開発者にとっては新しい複雑な概念であり、並行プログラミングの経験を積んだプログラマーにとってもせいぜいなじみはあるものの複雑な問題である。 要するに、Javaの並行プログラミング特性を使用したならば、徹底的にドキュメントする必要があるということだ。 Lea氏は次のように提唱している(1997)。クラスが同期メソッドと非同期メソッドを両方含んでいる場合、他の開発者がそのメソッドを安全に使えるようにするために、メソッドが想定している実行時のコンテクストをドキュメントする。とりわけそのメソッドが制約無しにアクセスされる必要がある場合はそうである。 Runnableインタフェースを実装するクラスにおけるフィールドを更新するSetterメソッドがsynchronizedでない場合は、なぜそうしたのか理由をドキュメントするべきである。 最後に、メソッドをオーバーライドまたはオーバーロードし、その同期性を変更するときは、なぜそうしたのかドキュメントする。
ドキュメントする際に重要な点は、コードが明瞭になることだけを記述することである。上記に挙げたすべての項目をそれぞれのメソッドに記述しないこと。なぜならば、すべての項目がどのメソッドにも適用できるわけではないからである。それぞれのメソッドについて上記の項目のいくつかをドキュメントする。9章で、上述のリストをサポートするjavadocのタグをいくつか提案する。
メソッドドキュメントに加えて、メソッド内にドキュメントを書く必要もある。その目的はメソッドを理解・保守・拡張しやすくすることである。
メソッド内ドキュメントに使うコメントには2種類ある。C風コメント(/* ... */)と単一行コメント(//)である。1.4.1節で述べたとおり、ビジネスロジックを記述するために使うコメント種類と不必要なコードをコメントアウトするために使うコメント種類を十分考慮する。私の推奨は、単一行コメントをビジネスロジックの記述に使うことである。単一行コメントは、行全体をコメントするときにもコードの後ろに記す行末コメントにも使えるからである。C風コメントは不要なコードをコメントアウトするときに使う。一つのコメントで複数行をコメントアウトできるからである。さらに、C風コメントはドキュメント化コメントに大変類似して見えるので、この使用は混乱を招き、コードの理解容易性を減少させてしまう。それゆえ、なるべく控えめに使うことにしている。
内部コメントとして書くべき項目例を以下に示す。
比較文、ループなどの制御構造が何をしているのか記述する。制御構造の中のソースコードをすべて読まなくても、代わりに1,2行のコメントを見てすぐに何をしているか分かるようにする。
コードのある部分を見れば、何をしているのかは理解できる。しかし、コードがなぜそのように書かれたのかを理解できることはほとんどない。 例えば、あるコードを見て注文合計の5%割引が適用されていることは簡単に分かる。それは実に簡単だ。難しいのは、「なぜ」その割引が適用されるかを理解することである。 何らかのビジネスルールがあって割引を適用しているのは確かなので、コード中でそのビジネスルールについて記述し、他の開発者がなぜコードがそうなっているのか理解できるようにする。
4章ではるかに詳しく論じるが、メソッド内で定義されるローカル変数はそれぞれ単一行で宣言し、その使用方法を記述するコメントを行う。
コードを書き直せない、あるいは書き直す時間がないときは、メソッド内の複雑な部分を完璧にドキュメントする。 経験に基づく大まかな指針として、コードが明白でないならばドキュメントする必要がある。
コードの文が必ず定義された順序で実行されねばならないならば、その守らねばならない事項をコメントに記述する。 コードが機能していないことを調べるためだけにコードに単純な修正を加え、それから何時間も費やして単にコードの順序が違っていたことを見つけるほど最悪なことはない。
しばしば制御構造の中にある制御構造の中に制御構造があることを見つけるだろう。大抵はこのようなコードは避けようとするが、こう書くことがよい場合もある。このとき、どの閉じ括弧'}'がどの制御構造のものなのか混乱してしまうという問題が生じる。ある種のエディターは、括弧の対応を自動的に示してくれる機能を持っているが、すべてのエディターが持っているわけではない。そこで、閉じ括弧にインラインコメントとして、//end if, //end for, //end switch,...のように書くことによってコードが理解しやすくなる。
しかし、どちらかを選択するとしたら、私は洗練されたエディタを使う方を選ぶだろう。
この節では、質の低いコーダーとプロの開発者との違いをもたらす技術について論じる。
「ドキュメントする価値のないコードは維持する価値がない(Nagler,1995)」ことを覚える。この文書で提案している規準、指針を適切に適用するならば、コードの品質を大いに向上することができる。
メソッドの読みやすさを向上する方法の一つが段落化であり、言い換えればコードブロックの単位でインデントすることでもある。括弧('{'と'}')で囲まれるコードはブロックを形成する。基本となる考え方は、ブロック内のコードは1単位(4)のインデントを行う。一貫性を維持するため、メソッドとクラスの宣言をカラム1から開始する。(NPS,1996)
Javaのしきたりでは、開き括弧は以降のブロックの主体となる行に置かれ、閉じ括弧は第1レベルインデントに置かれる。Laffra氏(1997)によって指摘された重要な点は、組織において一つのインデント流儀を決定し、それに従いつづけることである。私のアドバイスとしては、Java開発環境が生成するJavaコードと同じインデント流儀を適用することである。
コードを段落化する際に、単一の命令を書くのに複数の行を必要とすることがある。例を以下に挙げる。
BankAccount newPersonalAccount = AccountFactory. createBankAccountFor( currentCustomer, startDate, initialDeposit, branch);
2行目と3行目を1単位のインデントにしたことにより、それらが見た目で継続している行であることに気づくだろう。2行目の最後のカンマによって次にパラメータが続くことにすぐに気づく。
幾行かの空白行をコードに使うことで小さな、把握しやすい部分に分解でき、非常に読みやすくなる(NPS,1996; Ambler,1998a)。Vision2000チーム(1996)は、1行の空白行を使うことでコードを制御構造等の論理的なグループに分け、2行の空白行でメソッドの定義を分けることを提唱している。空白がなくては読みずらいし理解しずらい。以下のコードにおいて、カウンタと合計の計算行との間に空白行を追加したことによって2番目のものが読みやすく改善されたことに気づく。演算子の前後とカンマの後にある空白が追加されたことによってコードの読みやすさが向上したことが分かる。
counter=1; grandTotal=invoice.total()+getAmountDue(); grandTotal=Discounter.discount(grandTotal,this);
counter = 1; grandTotal = invoice.total() + getAmountDue(); grandTotal = Discounter.discount(grandTotal, this);
私は常に、他のプログラマーが30秒以内でメソッドを読むことができ、何をしているか、なぜそうなっているのか、どのようにしているかを完全に理解できるべきだと信じている。もしそうできなかったとしたら、そのコードは保守するにはあまりに難しいため改善の必要がある。30秒でである。Stephan Marceau氏によって提唱されているよいルールに、メソッドが画面に表示しきれないならば、それはおそらく長すぎる、というものがある。
1行では1つのことだけをする(Vision,1996;Ambler,1998a)。パンチカードの時代に遡れば、1つの行に可能な限りの機能を詰め込むことに意味はあったが、私が最後にパンチカードを見てから15年以上もたっていることだし、この詰め込み方法は考え直した方がいい。1つの行に1つより多いことをさせようとする試みはすべてコードの理解をより難しくする。なぜこうするのか。コードを理解容易にして保守と拡張を容易にしたいからである。メソッドが一つのことを実行し、その1つだけを行うようにするのと同様に、1行のコードでは1つのことだけをする。さらに、画面で見られるようにコードを書く(Vison,1996)。インラインコメントを使っているときも含め、編集ウィンドウを右にスクロールしなくても済むようにする。
コードの理解性を改善する本当に簡単な方法は、正確な演算順序を指定するように括弧を使うことである(Nagler,1995;Ambler,1998a)。コードを理解するためにその言語の演算順序を知らねばならないとしたら、何か重大な誤りがある。大部分がANDとORをいくつか別の比較と一緒に使ったときに生じる問題である。上述の、"簡潔に、一行にはひとつのコマンドを書く"ルールを使用していれば、この問題は生じない。
本節では、何年かの間に見つけたソースコードの品質を向上させる指針をいくつか記す。
以下の2つのコードを比較すれば、anObjectを含むステートメントがひとまとまりになっている下側の方が理解しやすい。コードは同じものだが、読みやすくなっている。(注:もしaCounterがmessage3()のパラメータとして渡されるとしたら、この変更はできない)
anObject.message1(); anObject.message2(); aCounter = 1; anObject.message3();
anObject.message1(); anObject.message2(); anObject.message3(); aCounter = 1;
以下のコードを考えてみる。パッと見は両者はどちらも等価だが、上側のコードはコンパイルできるが下側はコンパイルできない。何故か?2番目のif文が比較ではなく、代入文であり、0のような定数に新たな値を代入することができないからである。このようなバグをコード中から見つけるのは困難である(少くとも洗練されたテストツールなしでは)。比較文の左側に定数を置くことによって、同じ効果を達成できコンパイラは比較ではなく代入を誤って使ってしまったことを検出する。
if (something == 1) {...} if (x = 0) {...}
if (something == 1) {...} if (0 = x) {...}