クラス設計に関するメモ

経験的にこのようにした方がよいと思った点についての記録です。

仕事で大規模(2000クラス超)かつ製品寿命がながいパッケージソフトを作っていた関係で、 ちょっとした設計の間違いが、 あとあとで大変な苦労する羽目になったりすることを経験してきました。 このような規模が大きいアプリケーションを作ることはなかなかないかもしれませんが、 なにかの参考になれば、と思います。


継承する前に委譲を検討する

継承はスーパークラスの仕様をよく理解しておかないと、 バグを作りこみやすいので十分注意する必要があります。

メソッドのオーバーライドをするときも、
    public void foo(){
        // サブクラスの動作
    }
なのか、
    public void foo(){
        super.foo();
        // サブクラスの動作
    }
なのか、
    public void foo(){
        // サブクラスの動作
        super.foo();
    }
なのか。

どれが適切な実装なのか、注意しなければなりません。 どれが適切なのかを判断するには、スーパークラスの仕様を十分理解しておく必要があります。

あるクラスに機能を追加したいときは、 そのクラスを継承して次々に新しいクラスを作るのではなく、 追加したい機能だけを全く別のクラスで実装し、 そのクラスに責任を委譲するのが良い場合が多いです。

例えば、クラス X があったとします。そのクラスに機能 a を追加するため、継承により、 Xa というクラスを作成したとします。

そのとき、クラス X に機能 b を追加したクラス Xb を作成したとします。

その後、両方に機能 c を追加したくなったらどうするか? X を継承したクラス Xc を作成し、Xa と Xb は Xc を継承するように変更する、 というようなことになります。

つまり、クラスを継承してサブクラスに機能追加をする方法では、 機能が追加されるたびに継承関係が複雑化し、破綻してしまいます。 また、この方法では、機能追加することによって Xa と Xb の振る舞いが変わってしまう 危険性も十分考えられます。

これに対して、委譲による機能追加はシンプルです。X に機能 a を追加したいときは、 機能 a を持っているクラス A に対して責務を委譲します。機能 b や機能 c についても 同じように責務を委譲し、クラスに機能を追加することができます。 継承関係の制限を受けることもありません。

これはどういうことでしょうか? オブジェクト指向の入門的なテーマとして、車の話が挙げあられることが多いですね。 車をスーパークラスとし、これを継承して乗用車やバスやトラックなどのサブクラスを作る、 といった例がよくあります。 継承より委譲を使うということは、車と言うクラスで、 乗用車、バス、トラックなどのすべてを表現しようとします。

どのように実装するか? 部品の組み合わせを変えて、部品に機能を委譲して表現します。 シートが5人分なら乗用車、シートが40人分ならバス、大きな荷台ならトラックというように。 これなら、車クラスに部品のコレクションを持たせておき、好きな部品をつければいいのです。

Singleton パターンを使うときの注意

Singleton パターンって大丈夫?

Singleton パターンは、へたに使うとグローバル変数となります。 グローバル変数は、プログラムを作る上で諸悪の根源のようなものだということは、 みなさんご存知のとおりですね。

では、オブジェクト指向プログラミングの中で Singleton パターンを使った場合、 同じような弊害が発生するのかどうかを考えてみましょう。

オブジェクト指向ではカプセル化が可能なので、被害度はやや小さそうです。 Singleton パターンでグローバル変数的なものを作ったとしても、 カプセル化によりアクセスをコントロールできますので。 少なくとも、しらない間に勝手に値が変更されると言うようなことはなさそうです。

と、思ったら大間違い。普通、アクセサメソッドを書くときに、 そこまで注意して書くかどうか考えてみればわかると思います。 気にしても、せいぜいマルチスレッドからのアクセス程度ではないでしょうか。

どこで Singleton が使われている?

では、例えば Java の標準のクラスの中で Singleton パターンはどの程度使われているのでしょうか? Java のコア API のソースを検索してみれば、 ほとんど使われていないのがわかります。 java.awt.Toolkit と java.lang.Runtime 程度しかありません。 (もしかすると、もう少しあったかも)

API ドキュメントで getInstance() というメソッドを探すとたくんさんありますが、 これは単に新しいインスタンスを生成するためのメソッドであることがわかります。 java.lang.System や java.lang.Math は、ソースを見ればわかりますが、 インスタンスを生成していないので Singleton パターンとは言えません。

つまり、どのような場合に Singleton パターンを適用するのが適切かということになると、 本当にシステムに唯一のインスタンスしか存在し得ないときくらいしか、 Singleton パターンを適用するところはないのだと思われます。 Sun Microsystems によって書かれた実装方法が絶対的に正しいとは言いませんが、 Singleton パターンの適用については適切だと思います。 (java.awt.Toolkit については、2台のディスプレイに別々の表示が可能なコンピュータ があったとすると Singleton パターンは不適切だともいえますね。)

インスタンス数は本当にひとつしかありえないの?

インスタンス数がひとつしかないと決め付けて作ってしまっているので、 その前提が崩れてインスタンスが複数存在しうるとなったときは大変です。 そこを変更しようとしたら、どれだけ大変か、容易に想像できます。 Singleton パターンのまずい点のひとつはここにあります。

Singleton はクラスの肥大化が起こりやすい

一旦 Singleton にしてしまうと継承による機能追加ができなくなります。 つまり、その唯一のクラスに次々に新しいメソッドを追加していくしかないのです。 クラスを分割しようにも、Singleton であるがために、 あちこちにインスタンスを取得するための static メソッドが書かれているので、 影響範囲の管理が困難なのです。 その結果、その Singleton クラスはシステムの進化に伴って肥大化しつづけることになります。

Singleton に依存したクラスのテストも要注意

まだまだ困ることがあります。Singleton パターンでクラスを作ると、 Singleton パターンで作ったクラスに依存しているクラスのテストが書きにくいのです。 Singleton パターンによるクラスは JavaVM に対してユニークに生成されるため、 ひとつのテストを実行することで Singleton の状態が変化してしまうことがあります。

そうすると、最初にテストを実行したときはパスするが2度目以降はパスしないとか、 ある別のテストを実行したあとならテストがパスするとか、 テストがパスしない原因をつかみにくい状況に陥ってしまうことがあります。

Template Method パターンを使うときの注意

Template Method パターンは、サブクラスでメソッドを実装することを前提としています。 継承する前に委譲を検討するでも簡単に説明したように、 継承にはそれなりの難しさが伴っています。 Template Method パターンは継承を前提としているので、継承の難しさが必ずついてまわります。 それ以外にも Template Method パターンの難しさがあります。

Template Method パターンはアルゴリズムを複数のステップに分解し、 抽象メソッドとしておく。それぞれのステップをサブクラスで実装します。 しかし、ソフトウェアと言うのは常に変化します。 そしてあるとき、複数のステップに分解していたアルゴリズムの複数のステップの途中に、 新たな抽象メソッドを追加したくなったりするんですね、これがまた。

しかし、抽象メソッドの追加は影響範囲が大きいのです。 それまでに作ったサブクラス全てに、メソッドを追加してやる必要があります。 新しく追加するメソッドだけ抽象メソッドではない、空のメソッドにする方法もあります。 Template Method のもととなるクラスで、Template Method のうちの一部だけが 抽象メソッドではないと言う状況を許せる人はこうすれば良いのですが。

abstract class SomeAlgorithm {
    abstract protected void firstStep();
    abstract protected void lastStep();
    void algorithm(){
        firstStep();
        lastStep();
    }
}
abstract class SomeAlgorithm {
    abstract protected void firstStep();
    abstract protected void lastStep();
    void algorithm(){
        firstStep();
        appendedStep();
        lastStep();
    }
    protected void appendedStep(){
    }
}

許せない人はどうすればいいのでしょうか。

ひとつの案として、全てのサブクラスを修正する方法があります。 そのためには、全てであるという保証が必要です。 もし、Template Method のもととなるクラスが、 修正しようとしている人が感知しないところで利用されている可能性があるならば、 全てのクラスを修正できないのでこの方法は使えません。

とりあえず、こうすれば解決できます。

abstract class SomeAlgorithm {
    abstract protected void firstStep();
    abstract protected void lastStep();
    void algorithm(){
        firstStep();
        lastStep();
    }
}

abstract class ExtendedSomeAlgorithm extends SomeAlgorithm {
    abstract protected void appendedStep();
    void algorithm(){
        firstStep();
        appendedStep();
        lastStep();
    }
}
個人的には後者のほうが良いと感じます。
なぜなら、appendedStep() がいらないなら SomeAlgorithm クラスを継承すればいいし、 appendedStep() が必要なら ExtendedSomeAlgorithm を継承するという、 アルゴリズムの差を明示できるという点がその理由です。

クラス間の依存に関する注意

クラスを設計するときには、MVC モデルにしたがってクラスを抽出していくことと思います。 MVC モデルと言うか、UML で言えば entity・boundary・controller ですね。

ある問題領域の内部のクラスを設計を考えているとします。 クラスの依存関係は、なるべく双方向依存ではなく単方向依存にするのがポイントです。 そうすることにより、自然と疎結合になってきます。

entity・boundary・controller に分割したとき、 entity からは boundary と controller を参照させないように気をつけましょう。 なぜなら boundary は entity の一表現形式でしかありませんので、 普通は entity は boundary を知っておく必要はないのです。 次のようなコードは entity が boundary に依存しているといえます。

public class FooEntity {
    private FooBoundary foo;
}

public class FooBoundary {
}
entity が boundary に依存しない例は以下のとおりです。
public class FooEntity {
}

public class FooBoundary {
    private FooEntity foo;
}

また controller は entity と entity を結びつけたり、 entity と boundary を結びつけるものです。 entity が controller を参照しなければならないことはあります。 このようなときは、controller が持つべきインタフェースを作り、 entity は必ずそのインタフェースを通して controller を参照するようにします。 そうすることにより、controller が他の entity や boundary などに依存していても、 その影響を最低限に留めることができます。

entity と別の entity が相互依存となるのはあまり気にしなくてもよいです。 Composite パターンで構造を作ったときに、 entity 同士が上下方向の相互参照を作るのはごく自然な例のひとつです。

例えば、Java のプログラムを書いたとします。 Java コンパイラは、コンパイル対象のクラスが依存しているクラスを 自動的にコンパイルしますので、どのように依存しているかをチェックするのにも 使えます。 つまり、entity のソースをコンパイルしても boundary や controller のクラスが 勝手にコンパイルされなければ依存していないと言うことです。

具体的な例としてワープロソフトを考えてみましょう。 ワープロソフトは、作成する文書モデルをその内部メモリ上に保持します。 この文書モデルは entity です。 また、文書を作成するために文書を管理する仕組みが必要ですが、それも entity といえます。 それに対してワープロの画面や、 ワープロで作成している文書を表示している部分は boundary です。 ワープロは文書モデルを、印刷イメージで精密に表示することもできますし、 拡大したり縮小したりもできます。 文書モデル内のテキストだけを表示することもできます。

これはどういうことかというと、文書モデルは何も変化していないけれども、 表示方法が違っているということはお分かりいただけると思います。 ワープロの内部に、印刷イメージ boundary や、テキストオンリーの boundary が 用意されていて、それらの boundary が文書モデルをもとに表示しているのです。 作成した文書をファイルに保存することを考えてみましょう。 XML・RTF・PDF・プレインテキストなどいろいろなファイル形式で保存できますよね。 つまりファイルも boundary というわけです。 entity が boundary に依存していないほうが、 設計としてもすっきりするというのは容易に想像できますよね。

それでは、controller はどこにいるのでしょうか? 文書の編集に関する部分が controller にあたります。 ワープロ画面(boundary)からメニューなどを操作すると編集コマンド(controller)が 動作し、文書モデル(entity)を変更します。 文書モデルの変更はイベントとしてワープロ画面に通知され、 変更通知を受けたワープロ画面が、 更新された文書モデルをもとに画面を再描画します。

クラスの粒度

まず、「粒度」ですが「りゅうど」と読みます。でもワタシはわざと「つぶど」と 読んでいます。その方が自分の直感に訴えるのでそう発音しています。

読み方や、印象、名前など直感に訴えるものはオブジェクト指向では (もちろん、オブジェクト指向でなくても重要ですが)非常に重要です。 しかし、ここのテーマとははずれるので、読み方の話は置いておきます。

クラスを設計するとき、クラス数が増えすぎないようにひとつのクラスを大きめに 作るほうでしょうか? それとも、小さなクラスをたくさん作るほうでしょうか? ワタシは後者です。小さいクラスをたくさん作り、たくさんのクラス同士の コラボレーションでプログラムを動かすようにします。

クラス数が増えることを気にする人がいるようですが、クラス数が増えると何か デメリットがあるのでしょうか?意味もなくひとつのクラスでいいものを複数に 分割するのは無意味ですが、クラスの責任範囲を明確にし、その責任の粒度が 妥当ならば分けたほうが適切だと言えるでしょう。

ワタシの経験からすると、クラスの責任範囲を小さくすることは開発効率の向上に 寄与すると感じています。

クラスの責任範囲を小さくすると、当然、そのクラスのソースコードは小さくなります。 ソースコードが小さいことが、開発効率の向上に寄与します。 なんといっても、作業の中断に強くなります。 ソースコードが小さいので、電話などの割り込みがあっても、割り込みを処理した後に、 割り込み前に何をやろうとしていたかを思い出すまでの時間が短くなります。 保守においても、同じ効果が得られます。

もうひとつの効果は、クラス間の依存が減ることです。 つまり、より良い設計であるといえるでしょう。 クラスが大きい場合は、そのクラスが多くの責任を持ってしまうことがあります。 以下のクラス図を見てください。

ServiceOfB は、ServiceOfA のサービスを利用している場合、 ClientOfB と ServiceOfA の間に間接的な依存関係ができてしまいます。 クラスの責任範囲を小さくすると、以下のようになります。

Role というクラスを作ります。Role は ClientOfA を使用しつつ、ServiceOfB のサービスを提供します。つまり、Role は ClientOfA と ServiceOfB に依存 しています。 ClientOfA は ServiceOfA に依存し、ClientOfB は ServiceOfB に依存します。 しかし、最初に紹介した例にあった ClientOfB の ServiceOfA への依存がなくなって いることがわかると思います。(Role クラスは、本当は何かの目的があって、 ClientOfA と ServiceOfB を使っているわけですから、Role ではなくてその目的を あらわすようなクラス名が適当なのでしょう。)

Singleton の問題回避できるか?

Singleton パターンを使うと、いろいろと問題が出ることは、 「Singleton パターンを使うときの注意」でも紹介しました。 しかし、グローバル変数的なものがあった方がプログラムを単純にできる局面が多いのも 事実です。かといって、Singleton パターンを使うと長い目で見ると問題が多い。

ワタシがこれまでに Singleton パターンを使った部分を思い出して見ると、 グローバル変数的にアクセスできるオブジェクトが欲しかったケースが大半です。 グローバル変数的にアクセスしたいオブジェクトの大半は、データテーブルのように、 システム内で一元的にデータを管理するためのオブジェクトでした。

つまり、システム内で唯一のインスタンスである必要があるのかと言うとそうではなく、 全く同一のオブジェクト(データテーブル)であれば問題ないケースでした。 Singleton パターンを使わずに Singleton パターンの問題を回避しつつ、 グローバル変数的アクセスが可能なオブジェクトを作る方法はないでしょうか。

最近、Singleton の Wrapper を利用することで、大部分の弊害を回避できることが分かりました。 SomeClass をシステム内で唯一のインスタンスとして利用する例を紹介します。 SomeClass を Singleton として利用するために、SingletonWrapper クラスを作成します。 SingletonWrapper クラスでは、SomeClass を唯一のインスタンスとするために、SomeClass を private static なクラス変数として宣言します。 そして、全メソッドをそのまま SomeClass に委譲します。

public class SingletonWrapper {
    private static SomeClass singleInstance = new SomeClass();
    public void someMethod() {
        singleInstance.someMethod();
    }
        :
}

Singleton として SomeClass を扱いたい場合、つまり Singleton 化された SomeClass のクライアントは以下のように実装します。

public class SomeClassClient {
            :
    public void clientMethod() {
            :
        SingletonWrapper someClass = new SingletonWrapper();
        someClass.someMethod();
            :
    }
            :
}

このようにした結果として一番大きいと感じたメリットは、テスタビリティーがあがることです。 SomeClass は Singleton ではありませんので、Singleton を意識する必要は全くありません。 通常のクラスに対するテストを書けばよいのです。 SingletonWrapper は全メソッドは委譲しているだけですから、 委譲先メソッドが間違っていないかどうかを確認するだけの 最低限のテストだけ書けば十分でしょう。

「Singleton パターンを使うときの注意」ではクラスの肥大化を 指摘していますが、この場合は委譲メソッドの数が増えるだけです。通常は SomeClass 内に 多数の private メソッドが存在していますが、SingletonWrapper は委譲のメソッドだけ ですので、private メソッドは一切存在しません。 委譲メソッドだけが増加するのを肥大化と言うのなら肥大化していることになりますが。

クラスの拡張についても容易です。 まず、SomeClass に機能拡張したクラスを作ります。これは、SomeClass を継承してもいいし、 別の機能を持つクラスに SomeClass への委譲を追加する方法でもかまいません。 そして、SingletonWrapper 内の singleInstance の変数宣言を SomeClass から別のクラスに 変更します。 最後に、追加された機能に関するメソッドへの委譲を SingletonWrapper クラスに追加すれば 終わりです。

この変更において SomeClass は変更されることがありません。 つまり、万一 SomeClass が違う場所で使われていたとしても、 (このシステムでは使われていないとしても、他のシステムで使われているかもしれません) そこに影響を与えることなく安全に SingletonWrapper の実装を変更可能なのです。

(車輪の再発明かもしれませんが)



    
UML モデリングのエッセンス 第3版


初歩のUMLモデリング


UMLモデリングの本質


アジャイルソフトウェア開発の奥義


リファクタリング


創るJava



HOME お勧め書籍 / Profile / リンク