コーディングをしていると、こんなシチュエーションありますよね。
- あの人の書いたコード、なんかちょっとイマイチ…
- でも、直接そういうと、なんか失礼…
そんなときは、「アンチパターン」の言葉を使って、さりげなく相手に伝えましょう。
オブジェクト乱交状態
内部へのアクセスを制限なく許してしまうクラス
よくあるのは、すべてのメンバにgetter/setterがあるようなクラス。カプセル化が崩壊しているパターンですね。
クラスは、契約による設計(不変表明)に従い、その内部状態を正しく保たなければなりません。
内部状態を自由に書き換えられるようだと、その不変表明が守られなくなりますね。
Javaの場合は、JavaBeansという仕様のため、getter/setterだらけのクラスができがちではある。望ましくはないが。
getter/setterだらけのクラスがあるなら、Tell, Don’t Ask も意識しましょう。オブジェクトに「命じる」ようなメソッド定義すれば、getter/setterは少なくなるはず。
シーケンスによる結合
メソッドが特定の順序で呼び出される必要のあるクラス
「初期化」「計算」「後片付け」といったメソッドがあり、これを順番に呼ばないと処理が成立しないクラス。
Cなどの手続き型言語に慣れた人が、やりがちなパターン。
メソッドには「メソッド独立性」という考えがあります。
どのメソッドをどの順番に呼んでも、処理が成立しなければならない。
この考えでクラス設計すると、再利用性が高い、頑健なクラスになります。
ポルターガイスト
オブジェクトに情報を渡すためだけに使うクラス
他のメソッドを呼び出すときに、渡す情報を格納するためだけに使うクラス。
XXXParameter みたいな名前がよくつけられます。受け渡しのためのごく短期間だけ存在し、状態も持たないオブジェクトになります。
手続き型のように考えて、プロセスとデータを完全に分離してしまう。そのようなときに現れるクラスです。C言語でいう構造体のようなものですね。
データを扱うときは、データの状態をなんらかのクラスが管理すると考えて、設計しましょう。
ドメインモデル貧血症
ビジネスロジック(妥当性検証、計算、ビジネスルール等)が欠けたドメインモデル
ビジネスロジックを個々のクラスに実装するようなこと。トランザクションスクリプトと呼ばれます。
貧血ドメインモデルの例。
// 書籍
class Book {
}
// 利用者
class User {
}
// 借り入れ操作
class BorrowBook {
// 借りる
void borrowBook(User user, Book book) {
}
// 返す
void returnBook(User user, Book book) {
}
}
このコードは、以下の2種類で構成されています。
- データクラス Bookクラス、Userクラス
- ロジッククラス BorrowBook
これを、「ロジックとデータを明確に分離できていてよい」と感じるなら、それは手続き型プログラミングの考えです。
これには以下の欠点があります。
- ロジックがデータを変更するので、データが本来持っている構造を隠してしまう。
- カプセル化や情報隠蔽の原則を破壊する。
- データクラスは、それを変化させるためのロジックが外に置かれるため、自身の妥当性を保障できない。
- 似たようなトランザクションスクリプトでコードの複製が発生し、再利用性が減少する。
- モデルを理解しやすいように表現することが難しい。
BorrowBook のような、メソッドみたいな名前のクラスがあるときは要注意。貧血症になっているかもしれません。
<改善例>
// 書籍
class Book {
}
// 利用者
class User {
// 借りる
void borrow(Book book) {
}
// 返す
void return(Book book) {
}
}
BaseBean
ユーティリティクラスに処理を委譲せず、継承して使ってしまうクラス
予め定義されているStringクラスを継承して、MyStringクラスを作るようなことです。
継承を使うとき、派生クラスは基底クラスの内部実装に依存します。すると将来の基底クラスの変更により派生クラスの実装が破壊される可能性があります。
このような継承は、実装継承と言われるよくないやり方です。リスコフ置換原則を守りましょう。
サブクラスに必要な機能が含まれるというだけで、親クラスを継承するのは、悪手です。
代わりに、HAS-Aの関係にある委譲(デリゲーション)を使うべきです。
※Beanという名前は、Javaのエンティティオブジェクトの命名JavaBeanが由来
Call Super
サブクラスがスーパークラス(派生元クラス)のメソッドを呼び出さなければならないような設計
例:レンタルショップの目録についてレポートを生成するクラスがあるとします。
今あるレンタル品のリストを取得する方法はそれぞれの店で異なるが、最終的に生成されるレポートは全ての店で同一です。
ここで、以下のような「レポート生成」抽象クラスを提供するかもしれません。
abstract class ReportGenerator {
public virtual Report CreateReport() {
// 標準のレポートオブジェクトを生成
// ...
return new Report(...);
}
}
このクラスは、サブクラスでCreateReport
を実装することを期待しています。
class ConcreteReportGenerator : ReportGenerator {
public override Report CreateReport() {
// その店独特の方法でビデオをリスト化
// ...
// 最後に、親クラスのCreateReport()を呼ぶ
return base.CreateReport();
}
}
そしてこのクラスは、オーバーライドしたメソッドの最後に、親クラスのCreateReport()を呼ぶことを強制します。
しかし実際には、このような記述を忘れたり、またreportオブジェクトを返した後に編集してしまったりすることがあります。これをすると、情報が破壊されることになります。
継承先のクラスに、特定の実装のしかたを強要するクラスは、危険です。こういうクラスは後に重大な問題を引き起しがち。
改善例:
abstract class ReportGenerator {
public Report CreateReport() {
// リスト作成(仮想関数)
Tabulate();
// 標準のレポートオブジェクトを生成
// ...
return new Report(...);
}
protected abstract void Tabulate();
}
class ConcreteReportGenerator : ReportGenerator {
protected override void Tabulate() {
// 店特有のリスト化処理を実装
// ...
}
}
CreateReportを仮想関数にするのではなく、店ごとに違う「リスト化処理」(Tabulate)を仮想関数にします。(テンプレートメソッドパターン)
ヨーヨー問題
たくさんのクラスを行ったり来たりしないと、処理の流れがおえないクラス
多階層すぎる継承関係をもつクラスでよく発生します。
クラスA→B→C→Dと、多段階の継承関係があるとします。
クラスDに対して呼び出すメソッドMは、はたしてA,B,C,Dのどのクラスのメソッドが呼ばれるのでしょうか?
継承は、ただでさえ処理のトレースが難しくなります。継承が多階層に及ぶと手に負えなくなります。
継承関係は、できるだけ1段階だけにする、もしくは継承ではなく委譲を使うべきです。
まとめ
クラスに関するアンチパターンは、集約すると以下のパターンが多いです。
- 手続き型の欠点を持ち込んでしまう
- 継承を誤って使う
誤った使い方をしないように気をつけましょう。
コメント