BOOK Preview
|
|
|
6.4 クラスを作成する理由
ここまでの内容を信じるとすれば、クラスを作成する理由は現実世界のオブジェクトをモデリングすることだけである、と考えるかもしれない。実際には、クラスを作成する理由は他にもたくさんある。次に、クラスを作成する理由としてふさわしいものを挙げてみよう。
|
■ 現実世界のオブジェクトをモデリングする
現実世界のオブジェクトをモデリングすることは、クラスを作成する唯一の理由ではないとしても、そうするにふさわしい理由であることは間違いない。プログラムがモデリングする現実世界のオブジェクトの種類ごとに、新しいクラスを作成しよう。オブジェクトに必要なデータをクラスに追加して、オブジェクトの振る舞いをモデリングするサービスルーチンを構築する。例については、「6.1 クラスの基礎:抽象データ型(ADT)」を参照すること。
|
■ 抽象オブジェクトをモデリングする
クラスを作成するもう1つの理由は、「抽象オブジェクト」をモデリングすることである。抽象オブジェクトとは、具体的な現実世界のオブジェクトではなく、他の具体的なオブジェクトを抽象化するオブジェクトのことである。良い例は、おなじみのShapeオブジェクトだ。Circle(円)やSquare(四角形)は現実に存在するが、Shape(図形)は他の具体的な図形の抽象概念である。
プログラミングでは、抽象化がShapeのように用意されているわけではないので、つじつまの合った抽象化を何とか考え出さなければならない。現実世界の物体から抽象的な概念を引き出すプロセスは、これこれと決まっているわけではない。設計者が異なれば、抽象化される一般概念も異なる。たとえば、円、四角形、三角形といった図形のことを知らなければ、かぼちゃ形、かぶ形、ポンティアック・アズテック形など、変わった図形を思い付くかもしれない。適切な抽象オブジェクトを考え出すことは、オブジェクト指向設計における主な課題の1つである。
■ 複雑さを緩和する
クラスを作成する最も重要な理由は、プログラムの複雑さを低減することである。情報を隠ぺいするためのクラスを作成して、その情報について考えずに済むようにしよう。もちろん、クラスを作成するときは、その情報について考える必要があるだろう。しかし、一度クラスを作成してしまえば、後は詳細を忘れてしまってもかまわないし、内部のしくみを一切知らなくてもクラスを使用できる。クラスを作成する理由としては、他にも、コードのサイズを最小限に抑える、保守性を向上させる、正確さを向上させるといった良い理由があるが、クラスの抽象化の威力を利用しなければ、複雑なプログラムを頭で整理することは不可能である。
■ 複雑さを分離する
複雑さはどのような形(複雑なアルゴリズム、大きなデータセット、複雑な通信プロトコルなど)であってもエラーの原因になりやすい。エラーが発生した場合でも、コード全体に広がっておらず、1つのクラスに限定されていると見つけやすい。これなら、1つのクラスだけを修正すればよく、他のコードに触れる必要がないので、エラーの修正による変更が他のコードに影響することはない。アルゴリズムがクラスに分離されていれば、それよりも優れていて、単純で、信頼性の高いアルゴリズムを思い付いたときに、古いアルゴリズムと簡単に交換できる。開発段階であれば、何種類かの設計を試してみて、最適なものを選択することも容易になる。
■ 実装の詳細を隠ぺいする
実装の詳細を隠ぺいしたいという要求は、クラスを作成するすばらしい理由である。それがデータベースアクセスを伴う複雑なものであっても、特定のメンバデータが数字で保持されているのか文字列で保持されているのかといったありきたりなものであってもかまわない。
■ 変更による影響を限定する
変更されそうな領域を囲い込み、変更の影響を1つのクラスまたは2、3のクラスに限定する。変更される可能性がある領域としては、ハードウェアに依存する部分、入出力、複雑なデータ型、業務ルールなどが挙げられる。
|
■ グローバルデータを隠ぺいする
グローバルデータを使用する必要がある場合は、その実装の詳細をクラスインターフェイスで隠ぺいすることができる。アクセスルーチンを通じてグローバルデータを操作すれば、グローバルデータを直接操作する場合にはない利点が得られる。たとえば、プログラムを変更せずにデータの構造を変更したり、データへのアクセスを監視したりできる。アクセスルーチンを使用するという規則は、データが本当にグローバルなのかどうかについて考えるきっかけにもなる。「グローバルデータ」が実際には単なるオブジェクトデータだったということもよくある。
|
■ 引数の受け渡しを合理化する
複数のルーチン間で引数をやり取りしている場合は、それらのルーチンを、引数をオブジェクトデータとして共有するクラスに分解する必要があるという兆候かもしれない。引数の受け渡しを合理化すること自体は目標ではないが、多くのデータがやり取りされていることは、クラスの編成を変えた方がうまくいくかもしれないことをにおわせる。
■ 制御を一元化する
タスクをそれぞれ1か所で制御するのは良い考えである。制御の形態はさまざまだ。テーブルのエントリの数を知ることも1つの形態だし、ファイル、データベース接続、プリンタといったデバイスを制御することも1つの形態だ。1つのクラスを使ってデータベースのデータを読み書きすることも、制御の一元化の一形態である。データベースをフラットファイルやメモリ上のデータに変換する必要が生じた場合でも、変更はたった1つのクラスにしか影響を与えない。
|
制御の一元化という考え方は情報隠ぺいに似ているが、ヒューリスティクスに独特の威力を発揮するため、プログラミングの道具箱に入れておきたいツールである。
■ コードの再利用を促進する
正しく分解されたクラスのコードは、1つの大きなクラスに詰め込まれた同じコードよりも、他のプログラムにとって再利用しやすい。あるコードが呼び出されるのはプログラムの1つの場所からだけなので、大きなクラスの一部であってもいいような場合でも、そのコードが他のプログラムで使用される可能性があれば、クラスとして独立させるのが妥当である。
NASAのSoftware Engineering Laboratoryでは、再利用を柱とした10個のプロジェクトの追跡調査を行った(McGarry, Waligora and McDermott1989)。オブジェクト指向と機能指向の2つのアプローチとも、最初のプロジェクトでは以前のプロジェクトのコードをそれほど再利用できなかった。これは、以前のプロジェクトが十分なコードベースを確立していなかったことが原因だった。その後、機能指向のアプローチをとっていたプロジェクトは、以前のコードの約35%を再利用することができ、オブジェクト指向のアプローチをとっていたプロジェクトは、以前のコードの70%以上を再利用することができた。前もって計画しておけばコードの70%を書かずに済むというのなら、ぜひそうすべきだろう。
特に目を引くのは、再利用可能なクラスを作成するというNASAの基本方針に、「再利用のための設計」が含まれていないことである。NASAは、プロジェクトの最後に、再利用できそうなものを特定する作業を行っている。そして、クラスを再利用するために必要な作業を、メインプロジェクトの最後の特別プロジェクトとして、あるいは新しいプロジェクトの最初の作業として実施している。この方法は「金メッキ」、つまり、必要のない、そして必要以上に複雑さを増大させる機能の作成を防ぐのに役立つ。
|
■ プログラムのファミリを計画する
プログラムの変更が予想される場合は、変更が予想される部分をクラスとして独立させ、その部分を分離するとよいだろう。そうすれば、クラスの変更がプログラムの他の部分に影響を及ぼすことはなくなるし、まったく新しいクラスに置き換えることもできる。1つのプログラムがどのようなものになるかを考えるだけでなく、プログラムのファミリ全体がどのようなものになるかをよく考えることは、変更の種類をすべて予測するための強力なヒューリスティクスである(Parnas 1976)。
数年前、私は保険を販売するためのプログラム群を開発するチームを率いていた。私たちは、プログラムをそれぞれ特定のクライアントの保険料率や見積もり報告書のフォーマットなどに合わせて調整しなければならなかった。しかし、プログラムの大半は似たようなものだった。つまり、想定される顧客の情報を入力するクラス、顧客データベースに情報を保存するクラス、料率を調べるクラス、グループ全体の料率を計算するクラスなどである。そこで、私たちはプログラムを分解し、顧客によって異なる部分を独自のクラスにまとめてみた。最初のプログラミングに3か月ほどかかったが、新しい顧客を確保したら、その顧客を対象とした新しいクラスをいくつか書いて、それらを残りのコードに放り込むだけでよかった。数日間の作業で、はい、カスタムソフトウェアの一丁上がり。
■ 関連する操作をパッケージにまとめる
情報を隠ぺいしたり、データを共有したり、柔軟性に配慮したりすることが不可能な状況でも、一連の操作を三角関数、統計関数、文字列操作ルーチン、ビット演算ルーチン、グラフィックスルーチンといった実用的なグループにまとめることは可能である。クラスは関連する操作を組み合わせる方法の1つである。使用する言語によっては、パッケージ、名前空間、ヘッダーファイルなども利用できる。
■ 特定のリファクタリングを実行する
下巻の「第24章 リファクタリング」で説明するリファクタリングの多くは、1つのクラスを2つに変換する、委譲を隠ぺいする、中間オブジェクトを削除する、拡張クラスを導入するなど、結果として新しいクラスを作成するものだ。ここで説明した目標を効果的に達成したいという願望が動機となって、こうした新しいクラスが作成されることもあるだろう。
6.4.1 望ましくないクラス
クラスは一般に良いものだが、いくつかの問題にぶつかることもある。次のようなクラスは避けるようにしよう。
■ 「ゴッド」クラスを作成しない
全知全能のクラスを作成してはならない。クラスがGet()ルーチンやSet()ルーチンを使って他のクラスからデータを取得することに明け暮れているなら(つまり、他人の領分に首を突っ込み、あれこれ口出ししているようなら)、それをゴッドクラスにまとめるよりも、他のクラスに整理する方がよいかどうか検討しよう(Riel 1996)。
■ 不適切なクラスを排除する
クラスがデータだけで構成され、振る舞いが定義されていない場合は、それが本当にクラスなのかどうか考えてみよう。そして、クラスを分解して、メンバデータを他の1つ以上のクラスの属性にすることを検討すること。
|
■ クラスの名前を動詞にしない
振る舞いだけで構成され、データを持たないクラスは、実際にはクラスでないことが多い。DatabaseInitializationやStringBuilderといったクラスは、他のクラスのメンバルーチンにすることを検討する。
6.4.2 クラスを作成する理由のまとめ
次に、クラスを作成する理由としてふさわしいものをまとめておこう。
- 現実世界のオブジェクトをモデリングする。
- 抽象オブジェクトをモデリングする。
- 複雑さを緩和する。
- 複雑さを分離する。
- 実装の詳細を隠ぺいする。
- 変更による影響を限定する。
- グローバルデータを隠ぺいする。
- 引数の受け渡しを合理化する。
- 制御を一元化する。
- コードの再利用を促進する。
- プログラムのファミリを計画する。
- 関連する操作をパッケージにまとめる。
- 特定のリファクタリングを実行する。
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用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
|
|