【アンチパターン】そのプログラミングはNGです!【クラス編】

anti-pattern-class

コーディングをしていると、こんなシチュエーションありますよね。

つぶやき
  • あの人の書いたコード、なんかちょっとイマイチ…
  • でも、直接そういうと、なんか失礼…

そんなときは、「アンチパターン」の言葉を使って、さりげなく相手に伝えましょう。

目次

オブジェクト乱交状態

オブジェクト乱交状態

内部へのアクセスを制限なく許してしまうクラス

よくあるのは、すべてのメンバに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

BaseBean

ユーティリティクラスに処理を委譲せず、継承して使ってしまうクラス

予め定義されているStringクラスを継承して、MyStringクラスを作るようなことです。

継承を使うとき、派生クラスは基底クラスの内部実装に依存します。すると将来の基底クラスの変更により派生クラスの実装が破壊される可能性があります。

このような継承は、実装継承と言われるよくないやり方ですリスコフ置換原則を守りましょう。

サブクラスに必要な機能が含まれるというだけで、親クラスを継承するのは、悪手です。代わりにHAS-Aの関係にある委譲(デリゲーション)を使うべきです。

※Beanという名前は、Javaのエンティティオブジェクトの命名JavaBeanが由来

Call Super

Call Suer

サブクラスがスーパークラス(派生元クラス)のメソッドを呼び出さなければならないような設計

例:ビデオレンタルショップの目録についてレポートを生成するクラスがあるとします。

今あるビデオのリストを取得する方法はそれぞれの店で異なるが、最終的に生成されるレポートは全ての店で同一です。ここで、CallSuperアンチパターンを使うフレームワークは、以下の抽象クラスを提供するかもしれません。

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段階だけにする、もしくは継承ではなく委譲を使うべきです。

まとめ

クラスに関するアンチパターンは、集約すると以下のパターンが多いです。

  • 手続き型の欠点を持ち込んでしまう
  • 継承を誤って使う

誤った使い方をしないように気をつけましょう。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次