BOOK Preview

Code Complete 第2版 上・下
― 完全なプログラミングを目指して

第6章 クラスの作成

マイクロソフトプレスの書籍紹介ページ
書籍情報のページ
2005/04/26


■ 広範な型チェックよりもポリモーフィズムを選ぶ
 case文の数が増えてきたら、継承を使って設計した方がよいという兆候かもしれない。ただし、常にそれが正しいとは限らない。次に、オブジェクト指向のアプローチが切望されるコードの典型的な例を見てみよう。

switch ( shape.type ) {
  case Shape_Circle:
    shape.DrawCircle();
    break;
  case Shape_Square:
    shape.DrawSquare();
    break;
  ...
}
リスト6-9 ポリモーフィズムで書き換えるのが妥当と思われるcase文の例(C++)

 このshape.DrawCircle()ルーチンとshape.DrawSquare()ルーチンの呼び出しは、図形が円であるか四角形であるかに関係なく呼び出せるshape.Draw()という1つのルーチンに置き換えるべきである。

 これに対し、まったく異なる種類のオブジェクトや振る舞いを分別するためにcase文が使われることがある。オブジェクト指向プログラミングにふさわしいcase文は、次のようになる。

switch ( ui.Command() ) {
  case Command_OpenFile:
    OpenFile();
    break;
  case Command_Print:
    Print();
    break;
  case Command_Save:
    Save();
    break;
  case Command_Exit:
    ShutDown();
    break;
  ...
}
リスト6-10 ポリモーフィズムで書き換えるのは妥当でないcase文の例(C++)

 この場合は、基底クラスと派生クラスを作成して、(Commandパターンの要領で)コマンドごとにポリモーフィックなDoCommand()ルーチンを作成するという方法もある。しかし、このような単純なケースでは、DoCommand()ルーチンが存在する意味がほとんどないので、case文の方が理解しやすい解決策である。

■ すべてのデータをprotectedではなくprivateにする
 Joshua Blochが言うように、「継承はカプセル化を無効にする」(Bloch2001)。クラスを継承すると、そのクラスのprotectedルーチンとデータへのアクセスが特権付きで許可される。派生クラスが基底クラスの属性にどうしてもアクセスする必要があるなら、そうするのではなくprotectedのアクセサ関数を用意する方がよいだろう。

●多重継承

 継承は強力なツールである。のこぎりで木を切る代わりにチェーンソーで木を切り倒すようなものだ。注意して使用すればその効果は絶大だが、きちんと用心しない人の手に渡ると、危険なツールとなる。

 継承がチェーンソーであるとすれば、多重継承は1950年代のチェーンソーである。ブレードガードも自動停止装置も付いておらず、エンジン音もうるさい。そのようなツールが役立つときもあるが、怪我をしないようにガレージにしまっておく方が賢明だ。

C++の多重継承が、単一継承のもとでは存在し得ない複雑さのパンドラの箱を開けることは、紛れもない事実である。
─ Scott Meyers

 多重継承の活用を勧める専門家もいるが(Meyer 1997)、私の経験では、多重継承の主な用途は「ミックスイン(mixin)」を定義することである。ミックスインとは、オブジェクトに一連の特性を追加するために使用される単純なクラスのことだ。ミックスインは、特性を派生クラスに「混ぜ合わせる(mixed in)」ことができるので、そう呼ばれている。ミックスインとしては、Displayable、Persistant、Serializable、Sortableといったクラスが考えられる。ミックスインは必ずと言ってよいほど抽象的なもので、他のオブジェクトから切り離した状態でインスタンス化するためのものではない。

 ミックスインを定義するには多重継承を使用しなければならないが、すべてのミックスインが完全に独立している限り、多重継承の永遠のテーマであるダイアモンド継承(継承のルートがひし形になる多重継承)の問題とは無縁である。また、属性を1つの「かたまり」にすることで、設計が理解しやすくなる。プログラマにとっては、DisplayableとPersistentのミックスインを使用するオブジェクトと、それらを使用せずに11個のより具体的なルーチンを使用するオブジェクトでは、ミックスインを使用するオブジェクトの方が理解しやすい。

 ミックスインの価値に気付いているJavaとVisual Basicは、インターフェイスの多重継承を許可し、クラスの多重継承を許可していない。C++は、インターフェイスと実装の両方で多重継承をサポートしている。プログラマは、他の選択肢をよく検討し、システムの複雑さと理解しやすさへの影響を秤にかけたうえで、多重継承を使用すべきだろう。

●なぜ継承のルールはこんなにたくさんあるのか

 ここまでは、継承のトラブルに巻き込まれないようにするためのルールをいろいろ紹介してきた。これらのルールの裏には、継承は、プログラマの第一の責務である複雑さへの対処にマイナスに働く傾向があるというメッセージが隠されている。複雑さを抑えるには、継承に対して大きな偏見を抱くべきである。次に、継承を使用する状況と包含を使用する状況とをまとめてみよう。

  • 複数のクラスが共通のデータを持つが振る舞いを共有しないという場合は、これらのクラスに包含できるクラスを作成する。

  • 複数のクラスが共通の振る舞いを持つがデータを共有しないという場合は、共通のルーチンを定義する共通の基底クラスを継承する。

  • 複数のクラスがデータと振る舞いを共有するという場合は、共通のデータとルーチンを定義する共通の基底クラスを継承する。

  • 基底クラスでインターフェイスを制御したい場合は、インターフェイスを継承する。インターフェイスを直接制御したい場合は、インターフェイスを包含する。

参照
複雑さについては、第5章の「5.2.1 ソフトウェアの鉄則:複雑さへの対応」を参照。

6.3.3 メンバルーチンとメンバデータ

 ここでは、メンバルーチン(メンバ関数)とメンバデータを効果的に実装するためのガイドラインを紹介する。

参照
ルーチン全般については、「第7章 高品質なルーチン」を参照。

■ クラスのルーチンの数をできるだけ少なく保つ
 C++プログラムの研究によって、クラスあたりのルーチンの数が増えれば増えるほど、エラーの発生率が上昇することが判明している(Basili, Briandand Melo 1996)。ただし、深い継承ツリー、クラス内部で呼び出されるルーチンの数、クラス間の密結合といった競合要因の方が重大であることもわかっている。ルーチンの数を最小限に抑えることと、これらの要因との妥協点を見極めよう。

■ 必要のないメンバルーチンや演算子を暗黙に生成しない
 このルーチンは拒否したい、という場合がある。おそらく、代入を許可したくない場合や、オブジェクトの生成を許可したくない場合があるだろう。コンパイラが演算子を自動的に生成するので、アクセスが許可されても仕方がないとあきらめているかもしれない。だが、そのような場合でも、コンストラクタ、代入演算子、他のルーチンや演算子をprivateで宣言し、クライアントからのアクセスを拒否すれば、それらを使用できないようにすることは可能だ(コンストラクタをprivateで宣言することは、シングルトンクラスを定義する標準的な方法である。これについては後述する)。

■ クラスが呼び出すルーチンの種類をできるだけ少なくする
 ある調査によれば、クラスのエラーの数は、クラスから呼び出されたルーチンの総数と相関関係にあるという統計結果が出ている(Basili, Briand andMelo 1996)。この調査では、クラスが使用するクラスの数が増えれば増えるほど、エラーの発生率が上昇することもわかっている。これらの概念は「ファンアウト」とも呼ばれている。

■ 他のクラスへの間接的なルーチン呼び出しをできるだけ少なくする
 直接的な結び付きも十分に危険であるが、account.ContactPerson().DaytimeContactInfo().PhoneNumber()といった間接的な結び付きはさらに危険である。ある研究者らが「デメテルの法則」と呼ばれる法則を打ち立てた(Lieberherr and Holland 1989)。基本的にObjectAは自身のルーチンのみを呼び出すというものだ。ObjectAがObjectBをインスタンス化した場合、ObjectAはObjectBのルーチンをどれでも呼び出せる。しかし、ObjectBが提供するオブジェクトのルーチンを呼び出すことは避けるべきである。accountの例で言うと、account.ContactPerson()を呼び出すのは良いが、account.ContactPerson().DaytimeContactInfo()を呼び出すのは良くない。

 ここでは簡単な説明にとどめるので、詳細については、章末の「6.7 参考資料」を参照すること。

参照
「デメテルの法則」については、『The Pragmatic Programmer』(Hunt and Thomas 2000)、『Applying UML and Patterns』(Larman 2001)、および『Fundamentals of Object-Oriented Design in UML』(Page Jones 2000)が参考になるだろう。

■ 一般に、他のクラスとのコラボレーションの範囲をできるだけ狭める
 
次のすべてを最小限に抑えるようにする。

  • インスタンス化するオブジェクトの数
  • インスタンス化したオブジェクトでの直接ルーチン呼び出しの数
  • インスタンス化したオブジェクトから返されたオブジェクトでのルーチン呼び出しの数

6.3.4 コンストラクタ

 次に、コンストラクタを対象としたガイドラインを紹介する。コンストラクタのガイドラインは、言語間(C++、Java、Visual Basic)での差がほとんどない。デストラクタのガイドラインは大きく異なるので、デストラクタの情報については、章末の「6.7 参考資料」を参照すること。

■ 可能であれば、すべてのコンストラクタですべてのメンバデータを初期化する
 すべてのコンストラクタですべてのメンバデータを初期化することは、安価な防御的プログラミングである。

■ プライベートコンストラクタを使用することで、シングルトンを強調する
 インスタンスを1つだけ許可するクラスを定義したい場合は、クラスのすべてのコンストラクタを隠ぺいして、クラスのたった1つのインスタンスにアクセスするための「static GetInstance()」ルーチンを提供すればよい。次に、このしくみを実際に見てみよう。

参照
これをC++で行う場合のコードも同じようなものである。詳細については、『More Effective C++』の26項を参照(Meyers 1996)。
 
public class MaxId {
 
プライベートコンストラクタ
// コンストラクタとデストラクタ
 
private MaxId() {
    ...
  }
  ...
 
単一のインスタンスにアクセスするためのパブリックルーチン
// パブリックルーチン
 
public static MaxId GetInstance() {
    return m_instance;
  }
  ...
 
単一のインスタンス
// プライベートメンバ
 
private static final MaxId m_instance = new MaxId();
  ... }
リスト6-11 プライベートコンストラクタを使ってシングルトンを強調する例(Java)

 プライベートコンストラクタは、m_instanceという静的オブジェクトの初期化のときだけ呼び出される。MaxIdシングルトンを参照したい場合は、MaxId.GetInstance()を参照するだけでよい。

■ シャローコピーを使用する理由が特になければ、ディープコピーを優先する
 複合オブジェクトで最も悩むのは、オブジェクトのディープコピー(深いコピー)を実装するのか、それともシャローコピー(浅いコピー)を実装するのかである。「深い」と「浅い」の意味は状況によりけりだが、オブジェクトのディープコピーとは、オブジェクトのメンバデータをメンバごとにコピーするものだ。これに対し、シャローコピーは1つのオブジェクトをポイントするか、または参照するだけの参照コピーである。

 シャローコピーを作成する目的は、一般に、パフォーマンスを向上させることである。確かに、大きなオブジェクトのコピーをいくつも作成するのは、見た目が良くないかもしれないが、パフォーマンスに無視できないほどの影響を及ぼすことはめったにない。一方、パフォーマンスを低下させているのが少数のオブジェクトの場合、プログラマは周知のとおり、原因となっているコードを突き止めるのがへたである。パフォーマンスが改善するという確証がないのに複雑さを増大させることは、良い打開策であるとは言えない。ディープコピーとシャローコピーを選択する良い方法は、シャローコピーを使用すべき根拠が明確になるまでは、ディープコピーを使用することだ。

参照
パフォーマンス問題の原因を特定する方法については、下巻の「第25章 コードチューニング戦略」を参照。

 ディープコピーはシャローコピーよりもコーディングや保守が容易である。シャローコピーは、どちらの方法でコピーされたオブジェクトにも含まれているコードの他に、参照をカウントするコード、オブジェクトを確実にコピー、比較、削除するためのコードなどを追加する。このようなコードはエラーの原因になりやすいので、シャローコピーを作成する理由が特になければ、避けた方がよいだろう。

 シャローコピーを使用する必要があることに気付いたら、Scott Meyers著『More Effective C++』の29項を参照しよう(Meyers 1996)。C++での問題点が見事に解説されている。Martin Fowler著『Refactoring』では、シャローコピーをディープコピーに、ディープコピーをシャローコピーに変換する手順を紹介している(Fowler 1999、Fowlerはそれらを「参照オブジェクト」、「値オブジェクト」と呼んでいる)。


 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」


Insider.NET フォーラム 新着記事
  • 第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用のアドイン。プレゼンテーション時の字幕の付加や、多言語での質疑応答、スライドの翻訳を行える
@ITメールマガジン 新着情報やスタッフのコラムがメールで届きます(無料)

注目のテーマ

Insider.NET 記事ランキング

本日 月間