「長いメソッドを作るのではなく、短い沢山の小さなメソッドを作ろう」というのがこの章の趣旨です。長いメソッドは、コードの重複が増えるし、再利用しづらいし、クラスが解読しづらく保守しづらいからです。また、パフォーマンスのボトルネックを調べるのも困難です。なぜなら、多くのプロファイラはメソッド単位に実行時間を計測するからです。
| 解決すべき問 | クラスをメソッドにどう分割するか |
| 解決 | 小さなメソッドを作成し、それぞれメソッド名が表現する単一のタスクを果たす |
| 分類 | 振舞い |
| 関連 | Method Comment、Intention-Revealing Method Name |
このパターンがEssential Java Style中で最重点パターンだそうです。Composed Methodを使い、保守しやすい良いクラスを構築しましょう。
クラスを開発する時に念頭に置くことは、「まず動かす、次に正しくする、最後に速くする」("make it run, make it right, make it fast")です。この書で紹介しているパターンはいずれも「正しくする("make it right")」ためのパターンです。開発はこの3つの段階に沿って進めます。
動かすことはどんなソフトウェア開発でも最も重要な目標。初期のクラス開発では、
この3.の段階で出来たメソッドはちゃんと動くけど、簡単には保守したり拡張することができないでしょう。たいてい長いメソッドになっています。それは悪いこと?コードは読みやすいですか、空白・インデントは一貫していますか、段落分けはありますか、長すぎませんか、一つのメソッド内で多くさんのタスクを行っていませんか。継承、再利用が困難で、コードのあちこちに重複した部分が散在してしまいます。
小さなメソッドで構築すると、読みやすく、置き換えやすく、拡張しやすく、テストしやすいクラスになります。
長いけれど動くメソッドが出来上がったら、次は正しくする番です。正しくするには、この本にあるパターンを適用して、クラスを出来るだけ読みやすく保守しやすくします。その第一歩がComposed Methodです。
public void createCustomerCSV() throws IOException {
String tempFilename = null;
for (int i=0; i<1000; i++) {
String filename = "temp." + i;
File file = new File(filename);
if (!file.exists()) {
tempFilename = filename;
break;
}
if (tempFilename == null) {
throw new IOException(
"Could not create temp file");
}
}
FileWriter output = new FileWriter(tempFilename);
try {
Iterator iterator = customers.iterator();
while (iterator.hasNext()) {
Customer customer = (Customer)iterator.next();
output.write(customer.getName() + ",");
output.write(customer.getCity() + ",");
output.write(customer.getCountry() + "\n");
}
} finally {
output.close();
}
}
このメソッドは3つの異なるタスクを実行しています(実際のコードは略)。
public void createCustomerCSV(String filename) throws IOException {
FileWriter output = new FileWriter(filename);
try {
Iterator iterator = customers.iterator();
while (iterator.hasNext()) {
writeCustomer(output, (Customer)iterator.next());
}
} finally {
output.close();
}
}
public String getTemporaryFilename(String prefix) throws IOException {
for (int i=0; i<1000; i++) {
String filename = prefix + "." + i;
File file = new File(filename);
if (!file.exists()) {
return filename;
}
}
throw new IOException(
"Could not create temp file");
}
private void writeCustomer(FileWriter output,
Customer customer) throws IOException {
output.write(customer.getName() + ",");
output.write(customer.getCity() + ",");
output.write(customer.getCountry() + "\n");
}
メソッド名が何をしているかを示しているので理解しやすくなりました。見てすぐにそのメソッドが何をしているか理解できないなら、そのメソッドは多くのことをし過ぎなのです。
出力ファイル名を柔軟に選べます。例えばテンポラリファイル名のロジックだけ入れ換えることができます。
出力フォーマット変更は、writeCustomerメソッドを修正するだけです。
願わくはこの項目が必要にならないことを。でもいずれは誰かが遅い!と不平を上げるでしょう。結局Javaなのだから。
性能の問題にぶち当たった時に犯してはならない誤りは、どこに問題があるかを仮定することです。大抵それは間違っています。仮定せずに、ツールを使って測定しましょう。「正しくされ」たプログラムなら、すぐに性能のボトルネックがどこか検出できます。長いメソッドばかりのプログラムでは、どこがボトルネックか検出することが困難です。
性能を向上するための修正はコードを理解しずらくするので、なぜそうなのかをコメントを加えます。保守性が悪くなるので、性能向上の変更は最後の最後にします。
| 解決すべき問 | インスタンス生成をどのように表すか |
| 解決 | インスタンスを生成する有効なコンストラクタを提供し、無効なオブジェクトを生成するコンストラクタは提供しない |
| 分類 | 振舞い |
| 関連 | なし |
コンストラクタを作成しない場合、Javaでは引数なしのデフォルトコンストラクタが提供されます。明示的にコンストラクタを作成すると、引数なしのコンストラクタは利用できなくなります。
クラスのインスタンス生成時にインスタンスが有効な状態になっているべきです。もし引数なしのコンストラクタが有効なインスタンスを生成できない場合、有効なインスタンスを生成するのに必要な情報をコンストラクタの引数で渡せるコンストラクタを提供します。
Manager manager = new Manager();
manager.setName("Blow, Joseph");
manager.setSSN("999-13-1349");
manager.setContractedRate(40);
public Manager(String name,
String ssn,
int contractedRate)
public Manager(String name,
String ssn,
int contractedRate,
boolean hasGoldenParachute)
これは割と自然な考え方です。ただ、JavaBeans規約に従うクラスを作成する場合は、必ず引数なしのコンストラクタが必要になるので、このパターンが適用できないです。
| 解決すべき問 | Constructor Methodの引数をどのようにインスタンス変数に設定するか |
| 解決 | Direct Value Accessを適用する。複数のコンストラクタが共通にパラメータを設定する必要があるときは、private setメソッドを作成する。 |
| 分類 | 振舞い |
| 関連 | Indirect Variable Access、Direct Variable Access、Default Parameter Values |
もしIndirect Variable Accessを適用していると、コンストラクタ中でsetterメソッドを使用することになります。オブジェクト生成時と生成後とでは、属性へ設定する条件が異なることもあるので、コンストラクタ中ではsetterメソッドを避け、Direct Variable Accessを使う。
複数のコンストラクタがあったり属性の設定に複雑な操作が必要なときは、可視性をprivateとするConstructor Parameter Methodとしてsetという名で提供する。
public Manager(String name, String ssn, int contractedRate) {
set(name, ssn, contractedRate);
}
private void set(String _name, String _ssn, int _contractedRate) {
name = _name;
ssn = _ssn;
contractedRate = _contractedRate;
baseSalary = contractedRate * 2000;
}
引数の変数名の最初に_を付けているのは、この本の命名規則です。インスタンス変数にDirect Variable Accessで値を設定するときに使います。
| 解決すべき問 | 引数にデフォルト値をどうやって設定するか |
| 解決 | 必要な引数の組み合わせだけオーバーロードしたメソッドを作成し、引数の少ないメソッドからより多い引数のメソッドへ委譲する。 |
| 分類 | 振舞い |
| 関連 | Constructor Parameter Method |
int setCursorTo(int x, int y) {
:
}
int setCursorTo(int x) {
return setCursorTo(x, 1);
}
int setCursorTo() {
return setCursor(1, 1);
}
引数のうち、省略可能なものと必須なものとを区分けする。最初に、全ての引数を持つメソッドを記述する。その時必須引数を左側に置き、続いて省略可能な引数を並べる。次に、省略可能な引数を一つずつ減らしたメソッドを記述していく。Javadoc表示上、ソースリスト上は引数の少ないものを先頭にする。
| 解決すべき問 | オブジェクトの生成を簡潔にするには |
| 解決 | 引数を初期値として新しいオブジェクトを生成するメソッドを提供する |
| 分類 | 振舞い |
| 関連 | Converter Method |
文章クラス(Sentence)があったとし、これに単語(Word)を結合する状況を考えます。
new Sentence(existingSentence, word);
普通はこのようにコーディングするでしょう。
public Sentence cat(Word word) {
return new Sentence(this, word);
}
とメソッドを提供すると、
existingSentence.cat(word);
と書くことができます。
| 解決すべき問 | あるオブジェクトを別なオブジェクトへ同じプロトコルで変換するには |
| 解決 | 可能ならConverter Constructor Methodを使え、そうでない時はasTargetClass()メソッドを作り、targetクラスのインスタンスを生成しreturnする |
| 分類 | 振舞い |
| 関連 | Converter Constructor Method、Shortcut Constructor Method |
クラスAからクラスBへ変換したいとき、クラスAに変換メソッドを追加してしまいがちです。でも、クラスAからクラスBへの依存性が発生してしまいます。
GruntクラスからManagerクラスへ変換する場合、
public asManager() {
initialContractedRate = Math.round(getSalary() / 2000);
return new Manager(name, ssn, initialContractedRate, false);
}
とGruntクラスに書いてしまいがちですが、GruntクラスがManagerクラスへ依存してしまいます。Managerクラスのコンストラクタが変更されたらGruntクラスも変更しなければならないし、新しいクラスContractorを追加したら、asContractorメソッドをあちこちに追加してまわるはめになってしまいます。
ここで、もし変換先クラスを変更することが可能なら、Converter Constructor Methodパターンを使ってください。それが出来ないときだけ、Converter Methodを使います。
public asManager() {
return new Manager(this);
}
| 解決すべき問 | あるオブジェクトを別なオブジェクトへ異なるプロトコルで変換するには |
| 解決 | 変換元オブジェクトを引数にとるコンストラクタを変換先クラスに設ける |
| 分類 | 振舞い |
| 関連 | Converter Method |
コンスラクタを下記のように作ります。
public TargetClass(SourceClass sourceParam)
| 解決すべき問 | オブジェクトの属性を評価するには |
| 解決 | 問い合わせのような名前のメソッドを提供する。評価する属性の接頭辞にbe動詞かその派生を置く(isOpen、hasDependents、wasDeletedなど)。戻り値はbooleanとする |
| 分類 | 振舞い |
| 関連 | Enumerated Constants |
たいていの属性は、真偽値(boolean)や少ない範囲の列挙値で定義されます。オブジェクトの型によらず同じように評価したい場合に使います。
public boolean wasHiredByeCEO() {
return wasHiredByeCEO;
}
public boolean isFullTime() {
return status == FULL_TIME;
}
public boolean isPartTime() {
return status == PART_TIME;
}
クラスを利用する側のコードは、
is (employ.isFullTime()) { ...
と書けるので、読みやすくなります。
列挙の種類が2、3個をはるかに超えるときや今後増えることが予想されている場合は、Enumerated Constantsパターンを使います。
| 解決すべき問 | 関連するオブジェクトをどうやって順序つけるか |
| 解決 | Comparableインタフェースを実装し、Collections.sort(List)を呼ぶ。特殊な順序に並べるときは、Collections.sort(List、 Comarator)を呼ぶ。 |
| 分類 | 振舞い |
| 関連 | Equality Method |
Java 2から導入されたコレクションAPIですね。
| 解決すべき問 | メッセージの流れをどのようにスムーズにするか |
| 解決 | 引数のクラスに新しいメソッドを追加する。新たなメソッドは元の受取手を引数に取り、元の受け取り手にthisを引数としたメッセージを送り返す。 |
| 分類 | 振舞い |
| 関連 | なし |
例えば以下のコードは非常にきれいで見て分かりやすいです。
list.add("string 1");
list.add("second string");
list.add("3rd string");
list.remove("second string");
開発者の多くは見た目の理解しやすさが持たらす価値を割引いて考えがちですが、コードを読みやすくする点で非常に効果的なのです。
Reversing Methodは見た目を高めるパターンですが、コードの振舞いを変更するのでこの章に含まれています。
canvas.moveTo(3, 4); canvas.drawLineTo(7, 11); bulletArt1.drawOn(canvas); canvas.drawLineTo(12, 13); bulletArt2.drawOn(canvas);
ここで、canvasがメッセージの受け取り手となっているコードの中に、別なオブジェクトがメッセージの受け取り手となる個所があります。
Reversing Methodを適用し、canvasオブジェクトのクラスにdrawメソッドを実装し、メッセージの受け取り手を逆にします。
public void draw(Art art) {
art.drawOn(this);
}
canvas.moveTo(3, 4); canvas.drawLineTo(7, 11); canvas.draw(bulletArt1); canvas.drawLineTo(12, 13); canvas.draw(bulletArt2);
| 解決すべき問 | たくさんのコードがたくさんの引数や一時変数を共有するメソッドをどうやってコーディングするか |
| 解決 | メソッドの直後にインナークラスを定義し、元のメソッド中の一時変数をインスタンス変数として定義し、一時変数を一つのコンストラクタの引数で渡す。computeメソッドを定義し、元のメソッドの処理を実行する。Method Object中にComposed Methodを適用する。 |
| 分類 | 振舞い |
| 関連 | Composed Method、Parameter Object、Collecting Temporary Variable |
長くてみにくいメソッドを整理するためのパターンです。Composed Methodを適用して小さなメソッドに分解した結果、たくさんの一時変数をメソッド間で受渡ししなくてはならないことがあります。これらをインスタンス変数にしてしまうのは避けたい所です。そんな時には、普通Parameter Objectパターンを使いますが、それでも複雑な場合にはこのMethod Objectパターンを適用します。
void longComplex() {
:
}
何十行もある(もしかしたら何百行!)メソッドlongComplexがあったとします。まず、Composed Methodパターンを適用して、小さなメソッドに分解した場合を考えます。
void longComplex() {
simpleA(a, b, c, ...);
simpleB(a, b, c, ...);
simpleC(a, b, c, ...);
:
}
単一のタスクを処理するメソッド群に分解されますが、一時変数を引数や戻り値としてメソッド間で受渡しています。この結果、コードの読みやすさがあまり変わらなかったり保守性がそれほど向上しなかったりします。
そこで、長くてみにくいメソッドをインナークラスのメソッドとして定義します。
void longComplex() {
Calculator cal = new Calculator(a, b, ...);
cal.compute();
}
:
class Calculator {
:
void compute() {
a();
b();
c();
:
}
:
}
インナークラスCalculatorに、長くてみにくいメソッドを移行します。インナークラスでは、Composed Methodを適用して小さなメソッド群に分解します。次に、小さなメソッド間で受渡ししていた引数群をインナークラスのインスタンス変数に定義します。これで、すっきりしたコードになります。
| 解決すべき問 | たくさんのコードがたくさんの引数や一時変数を共有するメソッドをどうやってコーディングするか |
| 解決 | Composed Methodを適用する。メソッド間で受渡しする引数群を保持するインナークラスを定義する。分解されたメソッドの中からParameter Objectの保持するデータへ直接アクセスする |
| 分類 | 振舞い |
| 関連 | Method Object、Composed Method |
長くてみにくいメソッドを整理するためのパターンです。Composed Methodを適用して小さなメソッドに分解した結果、たくさんの一時変数をメソッド間で受渡ししなくてはならないことがあります。Cの構造体のように、これらをインスタンス変数として保持するインナークラスを定義します。各メソッドでは、このインスタンス変数を直接アクセスします。
void longComplex() {
mA(a, b, c, ...);
mB(a, b, c, ...);
mC(a, b, c, ...);
:
}
void longComplex() {
Params param = new Params();
mA(p);
mB(p);
mC(p);
:
}
class Params {
T a;
T b;
T c;
:
}
| 解決すべき問 | デバッグのためにオブジェクトの印字可能な表現をどうやって提供するか |
| 解決 | toString()メソッドをオーバーライドしオブジェクトをユニークに記述する簡潔な文字列を返却する |
| 分類 | 振舞い |
| 関連 | なし |
Objectクラスで定義されるtoString()メソッドは、オブジェクトのハッシュコードを16進表現文字列で返却します(例:Employee@fc825d21)。これはあまり使える内容ではないので、作成するクラスではtoString()メソッドをオーバーライドするべきです。ただし、toString()メソッドをユーザインタフェース用に使うことは勧められません。
public String toString() {
return getName() + " (" + getSSN() + ")";
}
コレクションに格納されるオブジェクトをデバッグするときは、クラス名も印字されると便利です。
public String toString() {
return getClass().getName() + " [" +
getName() + " (" + getSSN() + ")]";
}
規約として定めるのはよいことでしょう。規約をサポートするように、クラス名を印字する部分を括り出してもよいでしょう。
public String classTaggedString(Object object, String string) {
return object.getClass().getName() + " [" + string + "]";
}
このメソッドはどこに入れればいいでしょうか?よく使われるのが、デバッグ関係のユーティリティを収めるDebugクラスです。
public String toString() {
return Debug.classTaggedString(
this, getName() + " (" + getSSN() + ")");
}
| 解決すべき問 | メソッドにコメントをどのようにつけるか |
| 解決 | 必要な場合にだけ、メソッドの最初にJavadocコメントとは別に開発者用コメントを提供する。コードでは明白でない重要な情報を伝えるだけにコメントする。不明瞭なコードはComposed Methodといった他のパターンを使ってリファクタする |
| 分類 | 振舞い |
| 関連 | Composed Method |
コメントとコードとの間の正確性を保つ術はありません。不正確なコメントがある位ならば、コメントが無い方がずっとましです。Javadocはクラスの利用者へ、そのクラスの理解を助ける情報は提供しますが、保守に必要な情報までは提供しません。コメントするべき項目は以下のとおりです。
不明瞭なコードにコメントするより、コードをきれいに書き直します。
if (ftpResponse.charAt(0) == '2') {
// ftpの応答が200なら成功
これを、次のようにメソッドとして書き直します。
public boolean wasSuccessful(String ftpResponse) {
return ftpResponse.charAt(0) == '2';
}
すると、以下のようにコメントをつけなくても明確なコードになります。
if (wasSuccessful(ftpResponse)) {
| 解決すべき問 | メソッドをどんな名前にするべきか |
| 解決 | メソッドが何をしているかを名前にする。どのようにしているかではない |
| 分類 | 振舞い |
| 関連 | Composed Method、Getting Method、Setting Method、Query Method |
メソッドの命名は、クラス設計における最重要事項です。単一のタスクしか実行しない小さなメソッドに分割されていれば、正確に名前を付けられます。一般的には、メソッドが完遂する動作を表す動詞です。例えば、transmit()、encode()など。
メソッドの分類
略語は使わないようにします。コードを入力するのは1回ですが、コードが読まれるのは何百回かもしれません。入力の手間を削減することに価値はありません。読みやすくすることに価値があるのです。
if (employee.getTerminationDate() != null)
このコードは従業員が解雇されたかどうかを判定しています。これは読みにくいです。否定論理だし、解雇されたことを判定しているのか分かりかねます。きっとコメントを付けたくなるでしょう。しかし、それはComposed Methodを適用する状況の現れです。
また、従業員が雇用されているならば解雇日がnullであるという、ある特定の実装に依存しています。もしEmployeeクラスが変更され、9999年なら雇用されていることになったらどうでしょう?
public boolean isTerminated() {
return getTerminationDate() != null;
}
これはQuery Methodです。
オブジェクトは自分自身や他のオブジェクトとコミュニケーションしています。自分自身とのコミュニケーションとは、そのオブジェクト自身が持つ振舞いを起動することです。前章のパターン、特にComposed Methodを適用すると、オブジェクト内部のコミュニケーションが増えます。オブジェクトがコミュニケーションする仕組みは、「メッセージ」です。オブジェクトに何かさせるためにメッセージを送り、メッセージを受け取ったオブジェクトは適合するシグニチャを持つメソッドを起動します。前章のメソッドパターンは、特定の実装やクラスのコードを組織化するかについて扱いましたが、メッセージパターンではメソッドが互いにコミュニケートする方法を扱います。
| 解決すべき問 | 処理をどのように起動するか |
| 解決 | オブジェクトやクラスに関数呼び出しの形式でメッセージを送り、受け手にどのメソッドを起動するか決定させる |
| 分類 | 振舞い |
| 関連 | Choosing Message、Decomposing Message |
いわゆる関数のスコープは、大域かモジュール内かのどちらかです。オブジェクト指向では、クラスというスコープを持ちます。JavaやSmalltalkでは全てのメソッドがクラスに組み込まれます。ある機能を実行するには、メッセージをオブジェクトに送らねばなりません。ポリモルフィズムによって、メッセージを送った結果として実行されるサブルーチンがどれか動的に決定できます。
| 解決すべき問 | 複数の候補からどうやって1つを選んで実行するのか |
| 解決 | オブジェクトにメッセージを送り、そのオブジェクトのクラスに振舞いを決めさせる(ポリモルフィズム) |
| 分類 | 振舞い |
| 関連 | なし |
どの言語にも、つい乱用してしまう仕掛けが提供されています。Javaの場合、instanceofがその1つです。instanceofにも正しい使い方があります。それは例えばequalsメソッドの実装で引数が同じクラスであることを比較するときです。でも、instanceofを使う多くの場合は、オブジェクトを作り直した方がよいことを示しています。
double annualCompensation = 0;
if (employee instanceof Contractor) {
annualCompensation =
(Contractor)employee.getHourlyRate * 2000;
} else if (employee instanceof Grunt) {
annualCompensation =
(Grunt)employee.getMonthlySalary() * 12;
} else if (employee instanceof Manager) {
annualCompensation =
(Manager)employee.getContractRate() *
(Manager)employee.getContractPeriod();
}
ここでのinstanceofの使用は、switch文と同じです。こうしたコードの欠点は以下のとおりです。
Choosing Messageパターンは、オブジェクト指向3大格言の1つポリモルフィズム「1つのインタフェース、複数のメソッド」を亨受します。
// class Contractor
public double getAnnualCompensation() {
return getHourlyRate() * 2000;
}
// class Grunt
public double getAnnualCompensation() {
return getMonthlySalary() * 12;
}
// class Manager
public double getAnnualCompensation() {
return getContractRate() * getContractPeriod();
}
// 利用するコード
employee.getAnnualCompensation();
もはや、キャストは不要になり、if文の山も消え、instanceofも使う必要がありません。コメントするまでもない1行のメソッドになっています。
もし、Employeeオブジェクトにcompensation計算を組み込みたくなければ、デザインパターンで述べられているVisitorパターンを使うとよいでしょう。
| 解決すべき問 | 処理の一部をどうやって起動するか |
| 解決 | 自身(this)に複数のメッセージを送る |
| 分類 | 振舞い |
| 関連 | Composed Method |
古き良き機能分割は不滅です。良いOO設計の結果、サブシステム、オブジェクト、公開メソッドが得られます。Composed Methodでオブジェクトを単一のタスクだけを実行する小さな塊に分解します。しかし、制御とかドライバ的なメソッドはそれ以上分解できません(複雑なイベントの手順を扱っているため)。サブタスクを実行する一連のメソッドを呼んでいきます。
private void calculatePay(Employee employee, int payPeriod) {
calculateBiWeeklyPayAmount();
calculateFICATax();
calculateLocalTax();
calculateStateTax();
addOutOfStateAdjustments();
calculateFederalIncomeTax();
calculateMedicareTax();
subtractPreTaxDeductions();
subtractPostTaxDeductions();
}
| 解決すべき問 | 2つのオブジェクトが、片方の表現を隠蔽してどのように協調するか |
| 解決 | 責務を持つオブジェクトの中に表現を隠蔽する。クライアントオブジェクトが実装しなければならないインタフェースを生成する。クライアントオブジェクトは、責務を持つオブジェクトからインタフェースの1つのメソッドを通して呼ばれる |
| 分類 | 振舞い |
| 関連 | Mediating Protocol |
instanceof演算子が頻繁に使われているときと同じ様に、switch文が使われるときはたいていコードを再構成するべき現象です。あるクライアントオブジェクトが、利用するオブジェクトの属性に対してswitch文を記述しなければならないとします。他のクライアントオブジェクトも同じ様にswitch文を記述しなければならなくなります。これはコードの重複を意味し、避けるべきコーディングです。
public class BillingSystem {
public void bill() {
OrderType type = order.getType();
if (type.equals(OrderType.NEW)) {
// 新規注文の処理
} else if (type.equals(OrderType.CANCEL)) {
// 注文のキャンセル処理
} else if (type.equals(OrderType.ADD_SERVICE)) {
// サービス追加の処理
} else if (type.equals(OrderType.REMOVE_SREVICE)) {
// サービス削除の処理
} else if (type.equals(OrderType.MODIFY_SERVICE)) {
// サービス変更の処理
}
}
}
ここで、if文のネストはswitch文と同様です。
Orderクラスの属性typeによって処理を振り分けています。Orderを利用する他のクラスでも同様の処理を行う必要があります。コードの冗長が増すので品質劣化が生じます。Orderクラスに変更があると、全てのクラスを変更しなくてはならないので、システムに欠陥が生じるリスクがあります。
振り分けはプログラム中でたった1個所にします。それは属するべきものの中です。
public interface OrderProcessor {
void processOrderNew(Order order);
void processOrderCancel(Order order);
void processOrderAddService(Order order);
void processOrderRemoveService(Order order);
void processOrderModifyService(Order order);
クライアントは、BillingSystem以外にもいろいろいるので(例えばPricingSYstem)、名前は一般的なものにしています(抽象化)。
public void processBy(OrderProcessor processor) {
if (type.equals(OrderType.NEW)) {
processor.processOrderNew(this);
} else if (type.equals(OrderType.CANCEL)) {
processor.processOrderCancel(this);
} else if (type.equals(OrderType.ADD_SERVICE)) {
processor.processOrderAddService(this);
} else if (type.equals(OrderType.REMOVE_SREVICE)) {
processor.processOrderRemoveService(this);
} else if (type.equals(OrderType.MODIFY_SERVICE)) {
processor.processOrderModifyService(this);
}
}
すると、BillingSystemクラスのbill()メソッドは、たった1行のコードとなります。
public void bill() {
order.processBy(this);
}
BillingSystemクラスには、OrderProcessorインタフェースを実装するコードを記述します。
| 解決すべき問 | どのように受け手となる2つのオブジェクトのクラスに基づいて責務を委譲するか |
| 解決 | 引数にメッセージを送る。メッセージ名に自身のクラス名を付け、thisをパラメータとして渡す |
| 分類 | 振舞い |
| 関連 | なし |
Java/Smalltalk/C++といった多くのOO言語はシングルディスパッチを提供しています。これは、メッセージと受け取り手から実際に起動されるメソッドが決定されます。ダブルディスパッチはCLOS等の少数の言語で提供されています。メッセージ名と2つの受け取り手オブジェクトから起動されるメソッドが決まります。
Double Dispatchパターンの実装例は、デザインパターンの中のVisitorパターンで見ることができるでしょう。以下の簡単な例では、仮想のフットボールリーグのアプリケーションにおける選手の得点計算を見てみます。このアプリケーションでは、フットボール選手の階層として、Quaterback, Kicker, RunningBackなどを持ち、すべてPlayerを継承しています。選手クラスから得点を算出する機能を取り除くためにVisitorパターンを適用し、柔軟性と再利用性を高めます。
まず最初に、選手オブジェクトは"visit"可能でなくてはなりません。そこで、interfaceを定義します。
public interface Visitable {
void accept(Visitor visitor);
}
Playerクラスのサブクラスはこのインタフェースを実装し、acceptメソッドを提供します。ここがDouble Dispatchの肝です。Visitorは、Playerサブクラスに属さない機能をPlayerサブクラスに実装せずに済ませます。Double Dispathで得点を計算する責務をVisitorのサブクラスに委譲し返します。
public class Player implements Visitable {
// コンストラクタや他のメソッド等。。。
:
public void accept(Visitor visitor) {
visitor.visit(this);
}
}
次に、Visitorの階層を実装してきます。スーパークラスVisitorに抽象メソッドvisit(Visitable)を定義します。これはPlayerクラスのacceptメソッドから起動されます。
public abstract class Visitor {
public abstract void visit(Visitable visitable);
}
実際の処理は、Visitorのサブクラスで実行されます。
public class QuaterbackVisitor extends Visitor {
int totalScore = 0;
public void visit(Visitable visitable) {
Quaterback qb = (Quaterback)visitable;
totalScore += qb.getNumberOfTDs() * 6 +
qb.getNumberOfCompletions();
}
public int getTotalScore() {
return totalScore;
}
}
Visitorの使用例
public int totalQBScore(List qbs) {
QuaterbackVisitor qbVisitor = new QuaterbackVisitor();
Iterator iterator = qbs.iterator();
while (iterator.hasNext()) {
Quaterback qb = (Quaterback)iterator.next();
qb.accept(qbVisitor);
}
return qbVisitor.getTotalScore();
}
| 解決すべき問 | 2つのオブジェクトが相互作用を持ちながら独立性を保つにはどのようにコーディングするか |
| 解決 | オブジェクト間のプロトコルをリファインし、一貫した命名が使われるようにする |
| 分類 | 振舞い |
| 関連 | Dispatched Interpretation、Double Dispatch |
2つ以上のオブジェクトで相互にメッセージを送り合うと、相互依存性が生じ、相手なしに使用できなくなってしまう(強結合)。保守が困難になります。オブジェクト間は疎結合に保つ方がいいのですが、場合によっては相互結合を作ることもあります。Dispatched Interpretationパターンがその例です。そのときは、そのことを分かりやすくドキュメントします。最もよいドキュメントは(コメントではなく)プログラムコード自身に記述することです。Dispatched Interpretationの例にあるBillingSystemでは、相互結合で使用されるプロトコルがOrderProcessorインタフェースとして記述されています。このように相互に結合するオブジェクトでは、間にインタフェースを定義することによって相互のコミュニケーションを事前に定義します。
| 解決すべき問 | スーパークラスの振舞いをどのように起動するか |
| 解決 | メッセージの受け取り手としてsuperと明示的に指定する |
| 分類 | 振舞い |
| 関連 | Extending Super、Modifying Super |
superを使用すると、サブクラスとスーパークラスとが強結合となります。スーパークラスにある同名のメソッドを起動したいときに使います(サブクラスでそのメソッドをオーバーライドしていた場合)。メソッドにfinal宣言をすると、サブクラスではオーバーライドできません。
| 解決すべき問 | スーパークラスのメソッドにどうやって実装を追加するか |
| 解決 | スーパークラスのメソッドをオーバーライドし、その中でsuperを使ってスーパークラスのメソッドを呼ぶ |
| 分類 | 振舞い |
| 関連 | Super、Modifying Super |
スーパークラスのメソッドにある振舞いを拡張する主な理由は、コードの重複を避けるためです。
public class Manager extends Employee {
private int contractedRate;
private boolean hasGoldenParachute;
public Manager(String name, String ssn,
int _contractedRate,
boolean _hasGoldenParachute) {
super(name, ssn);
contractedRate = _contractedRate;
hasGoldenParachute = _hasGoldenParachute;
}
}
nameとssnは、スーパークラスEmployeeのコンストラクタを使って設定されます。
コンストラクタ以外では、デザインパターンのDecoratorでよく使われます。
public void execute(Transaction xn) {
log.add(xn);
super.execute(xn);
}
サブクラスでの拡張によって、トランザクション機構にチェック機能を追加しています。
| 解決すべき問 | スーパークラスを直接変更できないとき、どのようにスーパークラスの振舞いを変更するか |
| 解決 | スーパークラスのメソッドをオーバーライドし、superを指定して起動し、その結果を変更するコードを実行する |
| 分類 | 振舞い |
| 関連 | Composed Method |
このパターンは最後の手段として使うこと。このパターンが必要になるということは、スーパークラスをリファクタリングすべき事態を表しています。どうしてもスーパークラスを修正できないときに限って使うこと。
public Component createButtonPanel() {
JButton button = new JButton("OK");
button.setBackground(Color.yellow);
JPanel pane = new JPanel();
pane.add(button);
return pane;
}
ボタンの色が黄色のボタンパネルを生成するメソッドがあります。これをサブクラスで修青色に修正します。
public Component createButtonPanel() {
Component pane = super.createButtonPanel();
JButton button = (JButton)(((Container)pane).getComponent(0));
button.setBackground(Color.blue);
return pane;
}
本来は、スーパークラスにDefault Value Methodを適用してボタンの色を設定するようにリファクタリングするべきです。
| 解決すべき問 | 実装を再利用する継承以外の方法は何か |
| 解決 | あるオブジェクトから別なオブジェクトへ要求を委譲する |
| 分類 | 振舞い |
| 関連 | Simple Delegation、Self-Delegation |
著者は、オブジェクト指向の3大概念の中でもっともきらいなものと述べています。ちなみにオブジェクト指向の3大概念とは、カプセル化、ポリモルフィズム、継承を指します。継承は、オブジェクト同士で垂直方向(継承方向)に強い結合を持たらし、上位の変更が困難になります。
単に別なオブジェクトに操作を実行させるなら、Simple Delegationパターンを使います。別なオブジェクトから元のオブジェクトにメッセージが送り返される必要があるときは、Self-Delegationパターンを使います。
| 解決すべき問 | 委譲元オブジェクトの情報が不要な場合の委譲をどうするか |
| 解決 | 委譲元から委譲先へメッセージをそっくりそのまま送る |
| 分類 | 振舞い |
| 関連 | Delegation、Self-Delegation |
public boolean isEmpty() {
return stack.isEmpty();
}
public int size() {
return stack.size();
}
| 解決すべき問 | 委譲元にメッセージを送り返す必要があるオブジェクトへどのように委譲するか |
| 解決 | 委譲先に送るメッセージにおいてthisをパラメータとして渡す |
| 分類 | 振舞い |
| 関連 | Delegation、Simple Delegation |
Simple Delegationに比べると、オブジェクト間の結合が強くなってしまいます。
public String toString() {
return toExternalForm(); // (1)
}
public String toExternalForm() {
return handler.toExternalForm(this); // (2)
}
protected String toExternalForm(URL u) {
String result = u.getProtocol() + ":";
if ((u.getHost() != null) &&
(u.getHost().length() > 0)) {
:
(1)は、Intention-Revealing Methodパターンです。
(2)は、Self-Delegationです。
Self-Delegationを過度に使うような時は、ロジックを委譲元に戻した方がよいでしょう。
| 解決すべき問 | 異なるメソッドを起動する別な要素は |
| 解決 | 起動するメソッドの名前を保持するインスタンス変数を生成し、この変数名の末尾にMessageを付ける。Javaリフレクション機構を用いて実行する |
| 分類 | 振舞い |
| 関連 | なし |
このパターンを使うよりは、インタフェースを使う方がよいでしょう。リフレクションが有用なのは、メタタイプなアプリケーション(デバッガ、コンパイラ、オブジェクトインスペクタ、テストツール、プロファイラ等)です。
| 解決すべき問 | いくつもの操作が協調した結果であるコレクションをどのように返すか |
| 解決 | まずComposed Methodでそれぞれの操作をメソッドに分割し、各メソッドは部分集合を返し、それらを結合する |
| 分類 | 振舞い |
| 関連 | Composed Method |
public List getScoringPlayers() {
List scoringPlayers = new ArrayList();
// get scoring qbs
Iterator qbIterator = qbs.iterator();
while (qbIterator.hasNext()) {
Quaterback qb = (Quaterback)qbIterator.next();
if (qb.hasTDs() || qb.hasCompletions()) {
scoringPlayers.add(qb);
}
}
// get scoring wrs
Iterator wrIterator = wrs.iterator();
while (wrIterator.hasNext()) {
WideReceiver wr = (WideReceiver)wrIterator.next();
if (wr.hasTDs() || wr.hasReceptions()) {
scoringPlayers.add(wr);
}
}
// ... etc
// get scoring ks
Iterator kickerIterator = kickers.iterator();
while (kickerIterator.hasNext()) {
Kicker kicker = (Kicker)kickerIterator.next();
if (kicker.hasFieldGoals() || kicker.hasExtraPoints()) {
scoringPlayers.add(kicker);
}
}
return scoringPlayers;
}
このコードは一時変数のリストに、個々の独立したタスクの結果であるプレイヤを蓄積しています。まずこのコードをComposing Methodを使い、個々のタスクを独立したメソドに分解します。
public List getScoringPlayers() {
List scoringPlayers = new ArrayList();
scoringPlayers.addAll(scoringQBs());
scoringPlayers.addAll(scoringWRs());
scoringPlayers.addAll(scoringKickers());
return scoringPlayers;
}
private List scoringQBs() {
List list = new ArrayList();
Iterator iterator = qbs.iterator();
whilte (iterator.hasNext()) {
Quaterback each = (Quaterback)iterator.next();
if (each.hasTDs() || each.hasCompletions()) {
list.add(each);
}
}
return list;
}
private List scoringWRs() {
List list = new ArrayList();
Iterator iterator = wrs.iterator();
whilte (iterator.hasNext()) {
WideReceiver each = (WideReceiver)iterator.next();
if (each.hasTDs() || each.hasReceptions()) {
list.add(each);
}
}
return list;
}
private List scoringKickers() {
List list = new ArrayList();
Iterator iterator = kickers.iterator();
whilte (iterator.hasNext()) {
Kicker each = (Kicker)iterator.next();
if (each.hasFieldGoals() || each.hasExtraPoints()) {
list.add(each);
}
}
return list;
}
この例ではうまく行ってますが、オブジェクトの選別操作ではしばしば既にリストに追加されているオブジェクトを知っている必要があります。また、このような部分集合の連結を用いると、性能上問題になることもあります。
そこで、集合を個々のメソッドに渡し、個々のメソッドでは直接その集合に追加していく方法があります。これが、Collecting Parameterパターンです。
public List getScoringPlayers() {
List scoringPlayers = new ArrayList();
addScoringQBsTo(scoringPlayers);
addScoringWRsTo(scoringPlayers);
addScoringKickersTo(scoringPlayers);
return scoringPlayers;
}
private void addScoringQBsTo(List list) {
Iterator iterator = qbs.iterator();
whilte (iterator.hasNext()) {
Quaterback each = (Quaterback)iterator.next();
if (each.hasTDs() || each.hasCompletions()) {
list.add(each);
}
}
}
private void addScoringWRsTo(List list) {
Iterator iterator = wrs.iterator();
whilte (iterator.hasNext()) {
WideReceiver each = (WideReceiver)iterator.next();
if (each.hasTDs() || each.hasReceptions()) {
list.add(each);
}
}
}
private void addScoringKickers(List list) {
Iterator iterator = kickers.iterator();
whilte (iterator.hasNext()) {
Kicker each = (Kicker)iterator.next();
if (each.hasFieldGoals() || each.hasExtraPoints()) {
list.add(each);
}
}
}
オブジェクトの属性(インスタンス変数)によって、オブジェクトの「状態」が決まります。ここで扱うパターンは実装についてなので、設計について扱うステートパターン類は含みません。
インタンス変数の区分
また、一時変数に関するパターンもこの章で紹介します。一時変数は、設計時には扱われません。
| 解決すべき問 | 状態をどのように表現するか |
| 解決 | クラスの定義でインスタンス変数を提供する |
| 分類 | 状態 |
| 関連 | Variable State |
ある時点でのオブジェクトの属性のスナップショットが「状態」です。状態を表す属性が1つも無ければ、そのオブジェクトは単なる関数の集合です。属性の実装として、Javaではインスタンス変数を用います。
また、状態はオブジェクトのテストに関わります。メッセージを送った後、2つの出力をテストします。1つは戻り値でもう1つがオブジェクトの状態です。
| 解決すべき問 | 単一クラスのインスタンスで状態の表現をどのように変えるか |
| 解決 | HashMapを使って属性をキー・値の組合せで保持する。属性へのアクセスをgetPropertyメソッドを介して行い、属性の変更をsetPropertyメソッドで行う。 |
| 分類 | 状態 |
| 関連 | Common State |
インスタンス同士で属性が(属性の値がではなく)違うクラスを表現します。このパターンは滅多に使用しません。
java.util.PropertyクラスがVariable Stateパターンの例です。
| 解決すべき問 | インスタンス変数をどのようにデフォルト値に初期化するか |
| 解決 | インスタンス変数を宣言する時に、初期値を代入する |
| 分類 | 状態 |
| 関連 | Lazy Initialization |
Javaは、インスタンス変数を代入なしに宣言すると、暗黙的に初期値が代入されます。しかし、これに頼ってはいけません。
よって、Lazy Initializationを使っていない全ての変数は、例え初期値がデフォルト値と同じであっても明示的に初期化します。
public class Employee {
int hoursPerWeek = 40;
boolean isPartTime = false;
// ...
}
インスタンス変数がコンストラクタでは初期化されず、インスタンス生成後、別なメソッドで初期化される場合は、インスタンス変数を明示的にnullに初期化します。
public class Employee {
private Beneficiary beneficiary = null;
public Employee() {
}
public void setBeneficiary(String name) {
beneficiary = new Beneficiary(name);
}
}
初期化する値が不明のときは、Default Value ConstantやDefault Value Methodを使います。
| 解決すべき問 | インスタンス変数をどのようにデフォルト値に初期化するか |
| 解決 | インスタンス変数がGetting Methodの中でアクセスされる度に値をテストし、未初期化ならば初期値をセットする |
| 分類 | 状態 |
| 関連 | Explicit Initialization |
性能上、初期化処理をインスタンス変数が最初に使用される時まで遅らせます。そのため"lazy"と名前がつけられています。
Lazy InitializationはSetting Methodの中で行われます。
public String getTitle() {
if (title == null) {
title = "Grunt Level I";
}
return title;
}
この例なら、Default Value Methodでも実現できます。アクセスの度に判定が行われるので、性能を気にするかもしれません。Lazy Initializationの真価は、オブジェクトの初期化を前もって行うコストが大きい時に発揮します。あまり頻繁に属性がアクセスされないときに適しています。
例:全従業員の写真が格納されているシステムにおいて、給与計算時にはこのイメージをメモリに展開する必要がありません。
Lazy Initializationを使うときは、合わせてIndirect Variable Acceessを使わなければなりません。そこで、Lazy Initializationを使うオブジェクトはその全ての属性にIndirect Variable Accessを使うべきです。
| 解決すべき問 | 変数のデフォルト値をどのように表現するか |
| 解決 | デフォルト値を指定するConstantを提供する。このConstantで変数を初期化する |
| 分類 | 状態 |
| 関連 | Default Value Method、Explicit Initialization、Lazy Initialization |
インスタンス生成時にデフォルト値を与えるならば、Explicit Initializationを用います。ただし、途中でリセットする必要がある時はこれが使えません。この場合、本当はDefault Value Methodを適用すべきなのですが、一般にはDefault Value Constantが用いられています。しかし、このパターンは推奨しません。
private static final int defaultFullTimeHoursPerWeek = 40; private int hoursPerWeek = defaultFullTimeHoursPerWeek;
サブクラスでオーバーライドできないのが問題です。これはDefault Value Methodを適用すると解決できます。
| 解決すべき問 | 変数のデフォルト値をどのように表現するか |
| 解決 | デフォルト値を返却するメソッドを提供する |
| 分類 | 状態 |
| 関連 | Default Value Constant、Constant |
public int getDefaultHoursPerWeek() {
return 40;
}
これを、Constantを使ってさらに一歩進めると、
public int getDefaultHoursPerWeek() {
return defaultFullTimeHoursPerWeek;
}
サブクラスでオーバーライドできます。また、Lazy InitializationやExplicit Initializationと一緒に使うことができます。
private int hoursPerWeek = getDefaultHoursPerWeek();
public int getHoursPerWeek() {
if (hoursPerWeek == 0) {
hoursPerWeek = getDefaultHoursPerWeek();
}
return hoursPerWeek;
}
| 解決すべき問 | 定数をどうやって表現するか |
| 解決 | finalクラス変数を宣言し、定数として使用する |
| 分類 | 状態 |
| 関連 | Constant Method |
このパターンはJavaSoftによって定められた標準で、多くのコードが従っていますが、推奨いたしません。
Javaにはconst修飾子がないので、代りにfinalを使用します。Javaのswitch構文は貧弱で、case文においてint型かそれより小さい基本型の定数式しか使えません。そのため、Constantパターンが使われています。
public static final char SPACE = ' '; public static final String OPEN_FILE = "Open File"; public static final Point ENDPOINT = new Point(100, 120); // (1)
(1)は、オブジェクトの内容ではなく、参照がconstantです。したがって、オブジェクトの内容は一定ではありません。
ENDPOINT.y = 222; // OK ENDPOINT = new Point(2,3); // エラー
オブジェクトの内容を変更できないようにするには、そのオブジェクトをStringクラスのようにイミュータブルに設計します。例えば、コンストラクタでイミュータブルフラグを提供する等があります。
| 解決すべき問 | 変数のデフォルト値をどうやって表現するか |
| 解決 | 定数値を返却するアクセッサメソッドを提供する |
| 分類 | 状態 |
| 関連 | Constant |
「定数」は興味深いことにそれが一定であることが滅多にありません(もちろんπやアボガドロ数のような数学的定数は不変です)。例えば、U.S.A.の州の数は、100年後も50でしょうか?会社の資格レベルは何年も固定でしょうか?
定数の変更は、Constantパターンでも十分ですが、いつか固定の数ではなくなるかもしれません。例えば、今日は週の労働時間が40Hでも、明日は雇用種類によって異なった計算結果になるかもしれません。
定数が計算値に変わったとき、Constantパターンを使用していたら、全てのクライアントコードを変更する羽目になります。
public int hoursPerWeek() {
return 40;
}
public String disclaimer() {
return "Systems Systems, Inc. claims no liability " +
"for injuries sustained while using this product.";
}
Constantパターンに対するメリットは以下のとおりです。
Constatn Methodの命名方法は、Getting Methodを適用します。
| 解決すべき問 | 複数のクラスで必要な共通の定数の集りをどのように共有するか |
| 解決 | interfaceを定義する。定数をinterface中に宣言する。定数を必要とするクラスでinterfaceをimplementsする |
| 分類 | 状態 |
| 関連 | Indirect Variable Access、Lazy Initialization |
複数のクラスで使用される定数の一群があります。それがもはやどのクラス単独には属さないような場合は、独立させます。便宜上の理由だけで無関係な定数を一つに集めてはいけません。
public interface PayrollConstants {
int HOURS_PER_WEEK = 40;
int HOURS_PER_YEAR = 2000;
double OVERTIME_RATE = 1.5;
int WEEKS_IN_PAY_PERIOD = 2;
}
public class ConstantPool implements PayrollConstants {
public static void main(String[] args) {
new ConstantPool();
}
public ConstantPool() {
double hourlyRate = 30.0;
System.out.println("Pay for rate of " +
hourlyRate + " = " +
calculatePay(hourlyRate));
}
public double calculatePay(double baseRate) {
return baseRate *
WEEKS_IN_PAY_PERIOD *
HOURS_PER_WEEK;
}
}
Constant Poolパターンをもっと効果的に使うには、Enumerated Constantsと合わせ、型の安全性を保証します。
| 解決すべき問 | インスタンス変数の値をどのように読み書きするか |
| 解決 | インスタンス変数を直接読み書きする |
| 分類 | 状態 |
| 関連 | Indirect Variable Access、Lazy Initialization |
ここでの"variable access"とは、オブジェクト内部におけるアクセスを示します。インスタンス変数を定義したオブジェクト内のメソッドコード中では、直接インスタンス変数にアクセスするか、Getting Methodを介して間接的にアクセスするのか決めなくてはなりません。どちらが正しいというものではありません。
Direct Variable Accessを使う状況は、以下のとおりです。
アプリケーションレベルのUIオブジェクトがその典型例です。
class Employee {
private String lastName;
private String firstName;
private int hoursWorkedPerWeek;
// ...
public void setHoursWorkedPerWeek(int hours) {
hoursWorkedPerWeek = hours; // (1)
}
public int getAnnualHours() {
return hoursWorkedPerWeek * 50; // (2)
}
public String toString() {
return lastName + ", " + firstName; // (3)
}
}
(1)、(2)、(3)がDirect Variable Accessです。(2)はSetting Methodです。Setting Methodでは、直接変数に値を代入する必要があります。
| 解決すべき問 | インスタンス変数の値をどのように読み書きするか |
| 解決 | Getting MethodとSetting Methodを用いてインスタンス変数の値を読み書きする |
| 分類 | 状態 |
| 関連 | Direct Variable Access、Lazy Initialization |
オブジェクトの内部からインスタンス変数をアクセスする際は全て、Getting MethodとSetting Methodを使います。このパターンは、Direct Variable Accessを使った問題点を解決します。
また、一時変数とインスタンス変数とを混同することがなくなります。
class Employee {
private String lastName;
private String firstName;
private int hoursWorkedPerWeek;
// ...
public int getHoursWorkedPerWeek() {
return hoursWorkedPerWeek; // (1)
}
public int getAnnualHours() {
return getHoursWorkedPerWeek() * 50; // (2)
}
public String toString() {
return getLastName() + ", " + getFirstName();
}
}
(1)はGetting Methodなので直接変数にアクセスします。(2)、(3)はIndirect Variable Accessの例です。
| 解決すべき問 | インスタンス変数へのアクセスをどのように提供するか |
| 解決 | インスタンス変数を返却するメソッドを提供する。get+インスタンス変数名()と命名する |
| 分類 | 状態 |
| 関連 | Setting Method |
クライアントオブジェクトからアクセスされるインスタンス変数にはGetting Methodを提供します。クラス設計時には、どの属性を外部に公開(Getting Methodを介して)するか決定します。全てのGetting Methodを盲目的にpublicにしてはいけません。命名は、インスタンス変数名の最初の1文字を大文字にし、'get'を接頭辞に付けます。
| インスタンス変数 | Getting Method名 |
|---|---|
| newEmployees | getNewEmployees() |
| ytdAmount | getYtdAmount() |
| 解決すべき問 | インスタンス変数をどのように変更するか |
| 解決 | インスタンス変数を設定するメソッドを提供する。set+インスタンス変数名()と命名する |
| 分類 | 状態 |
| 関連 | Getting Method |
クライアントオブジェクトがオブジェクトの状態を変更することを可能にします。インスタンス変数はprivateにします。全ての属性についてpublicなSetting Methodを提供してはいけません。頑丈でかつ再利用できるオブジェクトを作るには、オブジェクトの完全性を下げようとする外部からの影響を完全に制御下に置かなくてはなりません。Setting Methodは蟻の一穴となるかもしれません。
Indirect Variable Accessを使っているなら、全ての属性についてSetting Methodを宣言するでしょう。このとき、できるだけprivateかまたはprotectedとします。
命名は、インスタンス変数名の最初の1文字を大文字にし、'get'を接頭辞に付けます。
| インスタンス変数 | Setting Method名 |
|---|---|
| newEmployees | setNewEmployees() |
| ytdAmount | setYtdAmount() |
| 解決すべき問 | コレクションを保持するインスタンス変数へのアクセスをどのように提供するか |
| 解決 | JDK1.1:委譲メッセージを介してのみコレクションへアクセスを許す。JDK1.2:Collectionsクラスのunmodifiableラッパーを使用する |
| 分類 | 状態 |
| 関連 | Getting Method |
コレクションはprivateとします。不幸にもコレクションを直接返却することが一般的に使われていますが、コレクションへの直接アクセスは出来ないようにします。代りにコレクションに対してクライアントが行うであろう操作をメソッドとして提供します。このメソッドはある種のSimple Delegationパターンになります。ただし、それぞれのメソッドには、特定の状況に合わせた名前を付けるようにします。
private Vector withdrawals = new Vector();
// ...
public Enumeration getWithdrawals() {
return withdrawals.elements();
}
public int getNumberOfWithdrawals() {
return withdrawals.size();
}
コレクションに対する枚挙を提供するには、コレクション自身を返却するのではなく、コレクションのEnumerationを返却するようにします。
Enumerationはsize()メソッドを提供していないので、これを返却するメソッドを追加することがあるでしょう。
例えば、Listが返すIteratorオブジェクトにはremove()メソッドがあります。これを外部へ渡すのは避け、代わりに不変なラッパーを使います(Collectionsクラスのstaticメソッドを利用)。このラッパーは、コレクションの変更を禁止されています(UnsupportedOperationMethodをスローします)。
private List withdrawals = new ArrayList();
//...
public List getWithdrawals() {
return Collections.unmodifiableList(withdrawals);
}
Collection Accessor Methodのjavadocコメントには、変更不可能なコレクションを返却する旨を明記して下さい。
| 解決すべき問 | コレクションの要素へ安全で汎用的なアクセスをどのように提供するか |
| 解決 | 引数にクロージャを取るメソッドを提供する。コレクションをイテレートし、コレクションの各要素についてクロージャにメッセージを送る |
| 分類 | 状態 |
| 関連 | Collection Accessor Method |
Collection Accessor Methodを適用すると、クライアントにコレクションを変更されないようにします。ただし、クライアントがコレクションの個々の要素を操作できるようにします。この時、全てのクライアントでは、コレクションの枚挙をループして個々の要素を引き出してから望みの操作を実行するコードを書かねばなりません。
そこで、クロージャを使ってコレクションの枚挙を元のオブジェクト内部にカプセル化します。クロージャとは関数の定義とそれを実行する環境を合わせたもので、関数を動的に生成することが出来ます。Javaではクロージャを匿名インナークラスを使って実装します。
Enumeration Methodはオブジェクトの内部にあるコレクションを枚挙する機能を提供します。個々の要素オブジェクトについて、クロージャへパラメータに要素オブジェクトを入れたメッセージを送り返して操作を実行します。
public class Closure {
public void exec(Object item) {}
}
public void dependentsDo(Closure closure) {
Iterator iterator = dependents.iterator();
while (iterator.hasNext()) {
Dependent dependent = (Dependent)iterator.next();
closure.exec(dependent);
}
}
命名として、コレクション名+"Do"を使います。
employee.dependentsDo(new Closure() {
public void exec(Object item) {
Dependent dependent = (Dependent)item;
System.out.println(dependent.getName() +
" is " + dependent.getAge() + " years old.");
}
});
| 解決すべき問 | 安全なC言語のenumのような能力をどうやって提供するか |
| 解決 | 列挙を表現する特別なクラスを作る |
| 分類 | 状態 |
| 関連 | Constant |
Employeeクラスに、正規雇用かパートかを表す状態を属性として持たせたとします。
public static final int FULL_TIME = 1; public static final int PART_TIME = 0;
このとき、状態を判別するクライアント側のコードは次のようになります。
if (employee.getEmployeeStatus() == Employee.FULL_TIME) {
// ...
}
ここで、別なクライアント側のコードで、たまたま定数がint型であることと、その値が0と1であることを利用して、次のようなボーナス額を計算していました。
double bonus = employee.getEmployeeStatus() *
(employee.getBaseSalary() * .1);
これは確かに「動き」はするプログラムです。しかし、後に状態を表す属性の実装方法をint型ではなくchar型に変更し、正規雇用を'F'、パートを'P'、さらにフレックス制適用を追加し'X'としました。この時、上記のボーナス計算プログラムはエラーにはならず、びっくりするほど高額のボーナスを与えてくれるようになります。実装が外部にさらされ、カプセル化を破壊しています。
これとは別に、状態を設定するメソッドsetEmployeeStatusを考えると、引数で渡される値(int型であったりchar型であったり)が状態として定義している範囲に入っているかをテストしなくてはなりません。このテストには、Guard Clauseを適用することが出来ますが、それぞれのメソッドにコードを追加していくので、コードが増え、テストと保守も増えます。それでも利用者が誤って使うことは防げません。それに、エラーは実行時よりもコンパイル時に捕まえたいところです。残念ながらJavaにはC/C++のenum型が無いです。そこで、Enumerated Constantsを実装するために再利用可能なスーパークラスを作ります。
package enum;
import java.util.List;
import java.util.ArrayList;
public class Enum {
protected static int numberOfEnums = 0;
protected static List list = new ArrayList();
private int ordinal;
private String name;
protected Enum(String _name) {
name = _name;
ordinal = numberOfEnums++;
list.add(this);
}
public static toString() {
return name;
}
public int getOrdinal() {
return ordinal;
}
public static Enum get(int ordinal) {
return (Enum)list.get(ordinal);
}
public static int size() {
return numberOfEnums;
}
}
これを継承していろいろな種類の列挙クラスを定義します。
package member;
public class MaritalStatus extends enum.Enum {
private MaritalStatus(String maritalStatus) {
super(maritalStatus);
}
public static final MaritalStatus SINGLE =
new MaritalStatus("Single");
public static final MaritalStatus MARRIEDE =
new MaritalStatus("Married");
public static final MaritalStatus DIVORCED =
new MaritalStatus("Divorced");
public static final MaritalStatus SEPARATED =
new MaritalStatus("Separated");
public static final MaritalStatus WIDOWED =
new MaritalStatus("Widowed");
}
MaritalStatusを利用するクライアントは、MaritalStatus.MARRIEDという形で定数を利用します。ただし、new MaritalStatus("HOPELESS")のように新たなインスタンスは生成できません(コンパイルエラー)。
| 解決すべき問 | booleanのプロパティをどのように設定するか |
| 解決 | 2つのメソッドを作る。1つはプロパティを真に設定し、もう1つは偽に設定する。どちらも引数は取らない |
| 分類 | 状態 |
| 関連 | Setting Method |
boolean型の属性に対するSetting Methodを、他の型の属性と同様に扱って、setBoolValue(boolean)のように定義してしまいがちです。しかし、boolean属性は、trueとfalseの2つしか取りませんし、型が変更になることが多発します。Boolean Property-Setting Methodパターンは、boolean属性の変更をカプセル化します。
public void setFullTime() {
fullTime = true;
}
public void setPartTime() {
fullTime = false;
}
このパターンのさらによい点は、コードを分かりやすくします。employee.setFullTime(false)に比べ、コードが何をするものか明瞭になっています。
public void setLocked() {
locked = true;
}
public void setUnlocked() {
locked = false;
}
| 解決すべき問 | インスタンス変数をどのように命名するか |
| 解決 | どのように実装したかではなく、オブジェクトの属性として果たす役割に従って命名する |
| 分類 | 状態 |
| 関連 | Role-Suggesting Temporary Variable Name |
よい命名は、保守可能なソフトウェアを開発する上でとても重要です。Composed Methodでさえ、無意味なメソッド名に対しては無力です。保守上、最初にすることはコードが何をしているか理解することです。ロジックに手を付ける前に、インスタンス変数名をより意味ある名前に付け変えることをよく行います。
インスタンス変数は、オブジェクトモデリングとつながっているので実装とは離れて命名することが重要です。Javaで、集合を保持するインスタンス変数があるとき、java.util.Listを使って実装し、それにproductsListと命名する傾向があります。これでは、実装をHashMapに変更したら、インスタンス変数名も変更しなければなりません。そうでないと、コードが誤解を生むものになってしまいます。
この場合の解決策は、productsと命名することです。同様に、idNumberではなくid、filenameStringではなくfilenameとします。
| 解決すべき問 | メソッド内で、後で使う値をどのように保持するか |
| 解決 | ローカルスコープの変数を宣言し、それに値を代入する |
| 分類 | 状態 |
| 関連 | Collecting Temporary Variable、Caching Temporary Variable、Reusing Temporary Variable、Explaining Temporary Variable |
存在するスコープがメソッドまたはブロックである変数で、スタック変数とも言われます。オブジェクト指向設計ではモデリング要素として登場していません。変数を、使用する直前で宣言する規約はJavaでも有効ですが、Composed Methodを適用すれば1つのメソッドは10行程度に収まるので、さほど問題ではありません。むしろ重要なことは、メソッドで最初に宣言する時に、初期値を割り当てることです。ただし、不必要なオブジェクトの生成をしないように注意します。オブジェクトの生成はコストがかかるし後に異なる型のオブジェクトを生成するかもしれません。このような時は、nullを割り当てます。
以下、一時変数を使用する4つのパターンを述べます。
| 解決すべき問 | メソッド内で、後で使う複数の値をどのように集めるか |
| 解決 | 集めた複数の値を保持する一時変数(Temporary Variable)を使用する |
| 分類 | 状態 |
| 関連 | Temporary Variable |
このパターンはよくListやVectorといったコレクションとともに使います。1つ以上の式の結果であるオブジェクトをコレクションに追加し、後ろでこの変数をイテレートしたりメソッドの戻り値として使用します。Composed Methodを十分に使いこなしていれば、滅多にこのパターンを必要とすることはないでしょう。
protected void writeRequiredFieldsFunction(PrintWriter toClient) {
toClient.println("function validate(form)");
toClient.println("{");
Vector fields = new Vector(); // (1)
fields.addElement("userId"); // (2)
fields.addElement("password"); // (2)
writeRequiredFieldCode(toClient, "form", fields); // (3)
toClient.println(" return true");
toClient.println("}");
}
(1)はCollecting Temporary Variableとして使うVector型のfieldsを宣言しています。
(2)は後で使うオブジェクトを追加しています。
(3)で呼び出したメソッドで、fieldsを使用しています。
なお、本来この例のメソッドは、リファクタリングすべきです。
| 解決すべき問 | メソッドの性能をどのように改善するか |
| 解決 | コストのかかる式を一時変数に割り当て、キャッシュとする。メソッドの残りではこの変数を使用する |
| 分類 | 状態 |
| 関連 | Temporary Variable |
性能は向上しますが、可読性は悪化しません。高価な演算結果(例えば、SimpleDateFormatクラスのformatメソッド)を繰り返し使うために、Caching Temporary Variableを使用します。
| 解決すべき問 | メソッド内の複雑な式をどのように簡単化するか |
| 解決 | 複雑な式を部分式に分割し、部分式の結果を一時変数に割り当て、Role-Suggesting Temporary Variable Nameに従って命名する |
| 分類 | 状態 |
| 関連 | Temporary Variable |
コードを記述する重要な目標は理解しやすくすることです。Composed Methodは、複雑なメソッドの意図を明確にしますが、1つの式までメソッド化するのは行き過ぎです。その式が1回しか使われないなら、メソッド化するべきではありません。そのような時は、Explaining Temporary Variableを適用します。
複雑な式を、理解しやすい部分式に分割し、その値を一時変数に置きます。
int days = (int)((laterDate.getTime() - earlierDate.getTime()) /
(60 * 60 * 24 * 1000));
long msDifference = laterDate.getTime() - earlierDate.getTime(); long msInADay = 60 * 60 * 24 * 1000; int days = (int)(msDifference / msInADay);
| 解決すべき問 | 式の値が変わるかもしれないとき、メソッド内で式の結果を複数回使用するにはどうするか |
| 解決 | 式の結果を一時変数に代入する。その変数をメソッドの残りを通して再利用する |
| 分類 | 状態 |
| 関連 | Temporary Variable |
複数回実行したときに、同じ値を返さない式があります。例えば、new Date()やiterator.next()やtokenizer.nextToken()などです。この式の値を保持する一時変数が、Reusing Temporary Variableです。
| 解決すべき問 | 一時変数をどのように命名するか |
| 解決 | 計算の中でそれが果たす役割にちなんで名付ける |
| 分類 | 状態 |
| 関連 | Temporary Variable |
Role-Suggesting Instance Variable Nameパターンとほとんど同じです。一時変数はより戦術的で実装特有です。短い省略した名前はよくありません。タイプ量を少し減らしても、今後の保守にコストがかかってしまいます。なぜなら、
JDK1.2からコレクション・フレームワークが導入され、オブジェクトの集合を表現するクラスが提供されています。特によく定義されたインタフェースとその実装で構成されています。また、同期/不変といった付加機能を持つラッパーもあります。検索/ソートといったアルゴリズムも提供されています。しかも、JDK1.1のVector/Hashと互換性を保っています。
| 解決すべき問 | オブジェクトの集合をどう表現するか |
| 解決 | コレクションを使う |
| 分類 | コレクション |
| 関連 | List、Array、Map、Set |
Java 2には2つの継承階層があります。1つは独立したオブジェクトの集合(collection)、もう一つはオブジェクト間のマッピングに基づく集合(map)です。
| [1] | Langr,Jeff. Essential Java style : patterns for implementation. Upper Saddle River,NJ:Prentice Hall Inc., 2000. |
| [2] | Beck,Kent. Smalltalk Best Practice Patterns. Upper Saddle River,NJ:Prentice Hall Inc., 1996. |