BOOK Preview
|
|
|
6.2 良いクラスインターフェイス
高品質なクラスを作成する最初の、そしておそらく最も重要な手順は、良いインターフェイスを作成することである。そのためには、インターフェイスを表す抽象概念をうまく作成し、実装の詳細を抽象化で覆い隠す必要がある。
6.2.1 良い抽象化
第5章の「5.3.2 一貫した抽象化」で説明したように、抽象化とは、複雑な処理を単純に見せる能力である。クラスのインターフェイスは、実装をインターフェイスで覆い隠すことによって抽象化する。こうしたクラスのインターフェイスは、クラスとして一貫性のあるルーチン群を提供すべきである。
たとえば、社員を実装するクラスがあるとしよう。このクラスには、社員の名前、住所、電話番号などを説明するデータが含まれている。また、このクラスは社員を初期化したり使用したりするためのサービスも提供する。このクラスのコードは次のようなものだ。
|
|
リスト6-1 うまく抽象化されたクラスインターフェイスの例(C++) |
|
このクラスは、これらのサービスをサポートするためのルーチンやデータを他にも持っているかもしれないが、クラスのユーザーはそれらについて一切知る必要がない。クラスのインターフェイスに含まれているルーチンはすべて一貫するように配慮されているので、インターフェイスの抽象化は申し分がない。
それに対して、抽象化が十分でないクラスには、さまざまな関数が混在している。例を見てみよう。
|
|
リスト6-2 抽象化がうまくいっていないクラスインターフェイスの例(C++) |
1つのクラスに、コマンドスタックを操作するためのルーチン、レポートの書式設定するためのルーチン、レポートを印刷するためのルーチン、グローバルデータを初期化するためのルーチンが含まれている。コマンドスタック、レポートルーチン、グローバルデータに何らかの関係があるとは考えにくい。クラスのインターフェイスは一貫性のある抽象化を実現していないので、クラスの凝集度は弱い。これらのルーチンは、それぞれがクラスのインターフェイスでうまく抽象化されるような、的を絞ったクラスにまとめるべきだろう。
仮に、これらのルーチンがProgramクラスの一部であるとすれば、次に示すように、一貫性のある抽象化を実現するように修正できる。
|
|
リスト6-3 うまく抽象化されたクラスインターフェイスの例(C++) |
インターフェイスがこのようにすっきりしたのは、元のルーチンのいくつかがより適切なクラスへ移動され、いくつかがInitializeUserInterface()や他のルーチンによって使用されるプライベートルーチンに変換されたためだ。
クラスの抽象化の評価は、クラスのパブリックルーチン群、つまりクラスのインターフェイスに基づいて下される。クラス全体がうまく抽象化されているからといって、クラスに含まれているルーチンもそれぞれうまく抽象化されているとは限らないが、そうなるように設計する必要がある。そのためのガイドラインについては、第7章の「7.2 ルーチンレベルでの設計」で紹介する。
うまく抽象化されたインターフェイスを追及していくと、クラスのインターフェイスを作成するためのガイドラインがいくつかあることがわかる。
■ クラスのインターフェイスで一貫性のある抽象化を実現する
クラスについては、6.1で説明したADTを実装するためのメカニズムとして考えればよいだろう。どのクラスでも、ADTは1つだけ実装すべきである。クラスが複数のADTを実装している、あるいはクラスがどのようなADTを実装しているのか判断がつかない場合は、クラスを明確に定義された1つ以上のADTにまとめ直してみよう。
次に、抽象化のレベルが均一でないために、矛盾したインターフェイスを提供しているクラスの例を紹介する。
|
|||
リスト6-4 抽象化のレベルが均一でないクラスインターフェイスの例(C++) |
このクラスは、EmployeeとListContainerの2つのADTを示している。このような抽象化の混在は、プログラマが実装にコンテナクラスなどのクラスライブラリを使用し、クラスライブラリが使用されていることを隠ぺいしなかった場合によく起こる。コンテナクラスが使用されていることを抽象化の一部として含めるかどうかについて検討しよう。通常は、次に示すように、実装上の詳細はプログラムの他の部分から隠ぺいすべきである。
|
|||
リスト6-5 抽象化のレベルに一貫性のあるクラスインターフェイスの例(C++) |
プログラマは、ListContainerクラスを継承する方が便利ではないかと考えるかもしれない。なぜなら、そうすればListContainerクラスがポリモーフィズムをサポートするし、引数としてListContainerオブジェクトを渡す外部の検索関数やソート関数が使えるからだ。もしもそう考えているのなら、「継承は『is a』関係にのみ使用されているか」という継承の本質を問う試験で不合格となる。ListContainerクラスを継承することは、EmployeeCensusオブジェクトがListContainerオブジェクトであること(is a)を意味し、それは明らかに誤りである。EmployeeCensusオブジェクトの抽象化が検索やソートを可能にすることなら、それをクラスのインターフェイスに明示的な要素として組み込むべきである。
クラスのパブリックルーチンを潜水艦の浸水を防ぐエアロックと考えるなら、一貫性のないパブリックルーチンはクラスにおいて水漏れするパネルである。水漏れするパネルは、エアロックを開いたときのように勢いよく水を流入させることはないにしても、しばらく放置しておけば潜水艦を沈没させるだろう。抽象化のレベルが一貫していないと、現実にこのようなことが起きる。抽象化のレベルが混在していると、プログラムを変更すればするほど理解しにくいものになっていき、ついには保守できなくなるだろう。
■ クラスがどのような抽象化を実装しているのか理解する
クラスどうしがあまりにもよく似ているために、クラスのインターフェイスが実現しているはずの抽象化を理解するのに細心の注意を払わなければならないことがある。かつて私が担当していたプログラムでは、情報を表形式で編集できるようにする必要があった。シンプルなグリッドコントロールを使いたかったのだが、そのグリッドコントロールにはデータ入力セルに色を付ける機能がなかった。そこで、その機能を持つスプレッドシートコントロールを使用することにした。
スプレッドシートコントロールはグリッドコントロールよりもはるかに複雑で、グリッドコントロールのルーチンが15個しかなかったのに対し、150個ものルーチンを備えていた。当初は、スプレッドシートコントロールではなくグリッドコントロールを使うはずだったので、スプレッドシートコントロールをグリッドコントロールとして使っていることがわからないように、あるプログラマがそのためのラッパークラスを作成することになった。そのプログラマは、無駄なオーバーヘッドや面倒な手続きにさんざん文句を言った後、立ち去った。そして数日後、スプレッドシートコントロールの150個のルーチンをそのまま公開するラッパークラスをたずさえて戻ってきた。
それは私たちが必要としていたものでなかった。私たちが望んでいたのは、水面下ではるかに複雑なスプレッドシートコントロールを使用している事実をカプセル化する、グリッドコントロールインターフェイスだったのだ。このプログラマは、グリッドコントロールと同じ15個のルーチンと、セルに色を付けるための16個目のルーチンだけを公開すればよかったのに、150個のルーチンすべてを公開した。そのため、基本実装を変更しようとすると150個のパブリックルーチンをサポートしなければならない可能性を作っていたのだ。私たちが期待していたカプセル化を実現しなかったばかりか、自分で自分の仕事を勝手に増やしていたのである。
状況によっては、スプレッドシートコントロールが正しい抽象化かもしれないし、グリッドコントロールがそうかもしれない。2つの同じような抽象化のどちらかを選択しなければならない場合は、必ず正しい方を選択しよう。
■ サービスは逆のサービスと2つ1組にして提供する
ほとんどの操作には、対応する操作、同等の操作、反対の操作がある。明かりをつける操作があるとしたら、明かりを消す操作も必要だろう。リストに項目を追加する操作があるとしたら、リストから項目を削除する操作も必要だろう。メニュー項目を有効にする操作があるとしたら、メニュー項目を無効にする操作も必要だろう。クラスを設計する際には、パブリックルーチンをそれぞれ調べて、それを補うルーチンが必要かどうかを判断する。逆の操作が不要であれば作成してはならないが、その操作が必要かどうかを必ず確認しよう。
■ 関係のない情報は別のクラスへ移動する
クラスのルーチンの半分がクラスのデータの半分を処理し、残り半分のルーチンが残り半分のデータを処理することに気付いたとしよう。このような場合は、実際には2つのクラスを1つのクラスに見せかけているだけなので、それらを2つに分解しよう。
■ インターフェイスはできるだけ意味的なものではなくプログラム的なものにする
インターフェイスにはそれぞれプログラム的な部分と意味的な部分がある。プログラム的な部分は、コンパイラで処理できるデータ型やインターフェイスのその他の属性で構成される。意味的な部分は、インターフェイスの使用法に関する条件で構成されるので、コンパイラで処理することはできない。意味的なインターフェイスには、「RoutineAはRoutineBよりも先に呼び出されなければならない」とか、「dataMember1がRoutineAに渡される前に初期化されていないと、RoutineAはクラッシュする」といった条件が含まれる。意味的なインターフェイスはコメントとして説明すべきだが、説明文がなくてもわかるようなインターフェイスにするよう心がける。コンパイラで処理できない部分のインターフェイスは誤用される可能性がある。アサーションなどのテクニックを使って、意味的なインターフェイス要素をプログラム的なインターフェイス要素に置き換える方法を探そう。
■ インターフェイスの変更中に抽象化が損なわれないように注意する
クラスを変更したり拡張したりしているときに、追加機能が必要であることに気付いたとしよう。その機能は最初のクラスインターフェイスとうまくかみ合わないが、別の方法で実装するのはかなり難しそうである。たとえば、Employeeクラスが次のように拡張されたとしよう。
|
|
|
リスト6-6 保守によって徐々に崩壊していくクラスインターフェイスの例(C++) |
以前のサンプルコードでは明瞭だった抽象化が、関連性の弱い関数の寄せ集めに変わっている。郵便番号や電話番号や職種を確認するルーチンと社員との間には、論理的なつながりはない。またSQLクエリを公開するルーチンは、Employeeクラスよりも抽象化のレベルがはるかに低く、Employeeクラスの抽象化を崩壊させている。
■ インターフェイスの抽象化と矛盾するパブリックメンバを追加しない
クラスインターフェイスにルーチンを追加する際、必ず「このルーチンは既存のインターフェイスが実現する抽象化と矛盾しないか」どうかについて検討する。矛盾する場合は、別の方法で変更することを検討して、抽象化の整合性を維持すること。
■ 抽象化とモジュール凝集度を同時に検討する
抽象化とモジュール凝集度(または強度)の概念は深く結び付いている。うまく抽象化されたクラスインターフェイスは、一般に凝集度が強い。凝集度の強いクラスは抽象化をうまく実現する傾向にある。
クラスの凝集度を重視するよりも、クラスインターフェイスが実現する抽象化を重視する方が、クラスの設計に対する理解が深まるようだ。クラスの凝集度が弱く、それを修正する方法がわからない場合は、代わりにクラスが一貫性のある抽象化を実現しているかどうかについて考えてみよう。
INDEX | ||
Code Complete 第2版 上・下 | ||
第6章 クラスの作成 | ||
1.6.1 クラスの基礎:抽象データ型(ADT)(1) | ||
2.6.1 クラスの基礎:抽象データ型(ADT)(2) | ||
3.6.2 良いクラスインターフェイス(1) | ||
4.6.2 良いクラスインターフェイス(2) | ||
5.6.3 設計と実装の問題(1) | ||
6.6.3 設計と実装の問題(2) | ||
7.6.4 クラスを作成する理由 | ||
8.6.5 言語固有の問題/6.6 クラスを超えて:パッケージ/6.7 参考資料/6.8 まとめ | ||
「BOOK Preview」 |
- 第2回 簡潔なコーディングのために (2017/7/26)
ラムダ式で記述できるメンバの増加、throw式、out変数、タプルなど、C# 7には以前よりもコードを簡潔に記述できるような機能が導入されている - 第1回 Visual Studio Codeデバッグの基礎知識 (2017/7/21)
Node.jsプログラムをデバッグしながら、Visual Studio Codeに統合されているデバッグ機能の基本の「キ」をマスターしよう - 第1回 明瞭なコーディングのために (2017/7/19)
C# 7で追加された新機能の中から、「数値リテラル構文の改善」と「ローカル関数」を紹介する。これらは分かりやすいコードを記述するのに使える - Presentation Translator (2017/7/18)
Presentation TranslatorはPowerPoint用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
|
|