
やってはいけないJavaプログラミング
|
●スコープ
ソースコード中には多くの「名前」が現れます。プログラマが人間にとってわかりやすく付けているそれらの名前を、コンパイラが論理的に一意と解釈するために、名前の重複を許さない範囲がスコープです。プログラマが名前をつけられるものには、パッケージ名、クラス(インターフェース)名、フィールド名、メソッド名、変数名、ラベルがありますが、これらはそれぞれ別の名前空間をもつため、同じスコープには入りません。たとえば、リスト9はクラス名が重複しているためコンパイルエラーになりますが、
リスト9
package example;
public class Scope {
}
class Scope {
}
リスト10はコンパイルエラーになりません。
リスト10
package Scope2;
public class Scope2 {
public Scope2() {
}
public Scope2 Scope2(Scope2 Scope2) {
return new Scope2();
}
}
リスト10で重複しているように見える名前はすべて別の名前空間に属しているからエラーにはならないのですが、こういった命名は混乱を招く元でしかないので避けなくてはなりません。命名の問題については、後述の「コーディング規約」を参照してください。
コンパイラが名前の重複を検出したときに、常にコンパイルエラーを出してくれるならば問題はないのですが、サブクラスがスーパークラスのフィールドと重複する名前のフィールドを持とうとした場合、スーパークラスでフィールド定義をfinalと修飾していた場合でも、コンパイルエラーは発生せずにサブクラスに同名のフィールドが定義されます。
リスト11
package example;
class Super {
protected final int member = 1;
}
class Sub extends Super {
protected int member = 2;
void printMember() {
System.out.println(member);
}
}
このコードでは、Sub#printMember()は2を出力します。 サブクラスからスーパークラスのフィールドにはsuper.フィールド名、あるいは((スーパークラス名)this).フィールド名と修飾することでアクセスが可能です。逆にスーパークラス中からサブクラスで定義している同名のフィールドにアクセスすることはできません。
リスト12
package example;
class Super2 {
protected int member = 1;
void printMember() {
System.out.println(member);
}
}
class Sub2 extends Super2 {
protected int member = 2;
}
この例ではnew Sub2().printMember()は1を出力します。このような例を回避するためにも、同一クラス内でフィールドへアクセスする際にもgetterメソッドを利用するほうが望ましい(注8)といえます。
リスト13
package example;
public class Super3 {
protected int member = 1;
public int getMember() {
return this.member;
}
void printMember() {
System.out.println(getMember());
}
class Sub3 extends Super3 {
protected int member = 2;
public int getMember() {
return this.member;
}
}
また、例外として定数フィールドは隠蔽できないと考えるべきです。定数はフィールド=インスタンス変数ではなくクラス変数であり、クラス変数に対しては常にインターフェース名でアクセスすべきだからです。ポリモフィズム(多態性)を活用した設計を行っていれば、スーパークラスないしインターフェースに対してプログラミングを行っているはずで、その場合には、具象クラスで定義している定数はアクセスされることがありません。具象クラス側で値を変える可能性があるのであれば、同様に定数を取得するメソッドを導入しなくてはなりません。
バグの元
バグを生む原因は、言語に対する理解不足の他にもたくさんあります。たくさんあるバグの中でも見つけ難いバグを埋め込みやすい設計・プログラミングについて見てみましょう。
●リソース
アプリケーションプログラムは、OSやミドルウエアの提供する様様なリソースを利用して処理を行います。メモリリークについては既に触れましたが、プログラム中で利用するリソースはメモリだけに限ったことではありません。ここでリソースとは、特にメモリやファイル識別子、データベース接続といった、OSやミドルウエアからプログラムが確保する、有限の資源を指します。
リソースは有限ですから、アプリケーションが確保するばかりで解放しなければ、いつか枯渇します。結果としてそのアプリケーション自身や、場合によっては同時実行中の他のプログラム、最悪OSにまで悪影響を与え、クラッシュさせてしまうこともあります。
◆データベース
リソースに関してJavaでやってはいけないプログラミングとは、「取得と解放を別のオブジェクトの責務とすること」です。
【図1】のシーケンスでは、アプリケーションがデータベースへの接続を取得し、ユーザの操作に応じて各コマンドA、Bを実行しています。アプリケーションでは個々のコマンドが実行する処理の内容を理解している必要がなく、個々のコマンドではデータベース接続に必要なユーザ名やパスワードなどを知る必要がない、というフレームワークになっています。
こうしたフレームワークを導入することで新しいコマンドの追加が簡単になってよいのですが、これは非常にバグを作り込みやすい設計です。アプリケーションクラスでデータベース接続を解放していない以上は、コマンドクラスが解放する必要があるのですが、そのルールが不明確なため、コマンドBクラスで解放しない場合にリソースリークになってしまいます。このフレームワークではこうしたバグを防ぐことはできませんから、こうしたフレームワークを作ってはいけません。
このケースでは、接続の解放を個々のコマンド実装クラスの責務とするのではなく、アプリケーションクラスで必ず接続を解放するようにするとよいでしょう(注9)。その場合、「コマンド実装クラスで接続の解放を行ってはならない」というのが設計上のルールになりますので、次のようなクラスを導入します。

リスト14
import java.sql.*;
public final class CannotCloseConnection implements Connection {
private Connection conn;
CannotCloseConnection(Connection conn) {
this.conn = conn;
}
public void close() {
throw new RuntimeException("Close by command.");
}
public void clearWarnings() {
conn.clearWarnings();
}
public void commit() {
conn.commit();
}
public Statement createStatement() {
return conn.createStatement();
}
// :
// :
// 以下、Connection のすべてのメソッドを conn に委譲。
}
close()以外java.sql.Connectionへ委譲するクラスです(注10)。これを利用したシーケンスは 【図2】のようになります。「コマンド実装クラスではデータベース接続をcloseしてはいけない」という設計ルールを検出できるようになるわけです。フレームワーク流行りの昨今ですが、生産性の向上だけでなく、このように「やってはいけないプログラミング」を検出する仕組みを盛り込むことで品質の向上にも寄与するというわけです。

(注8)このアプローチの適用を広げていくと「Template Methodパターン」に行き着くでしょう。
(注9)あるいはコマンドのスーパークラスになる抽象クラスで、「Template Method」にしてもいいでしょう。
(注10)Decoratorパターンになります。一般的なDecoratorパターンでは機能を追加するところ、このクラスではclose()機能を制限していますが、「close()が呼ばれたらRuntimeExceptionをthrowする」という責務を追加したと考えます
