BOOK Preview

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

第6章 クラスの作成

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


6.2.2 良いカプセル化

 第5章の「5.3.3 実装の詳細のカプセル化」で説明したように、カプセル化は抽象化よりも強力な概念である。抽象化は、実装の詳細を無視できるようなモデルを提供することで複雑さに対処するが、カプセル化は、たとえ詳細が見たくてもそれを見せない、用心棒的な存在である。

参照
カプセル化の詳細については、第5章の「5.3.3 実装の詳細のカプセル化」を参照。

 この2つの概念は結び付いている。なぜなら、カプセル化を行わなければ、抽象化は失敗することが多いからだ。私の経験では、抽象化とカプセル化は、両方とも行うか、両方とも行わないかのどちらかである。どちらか一方だけを行うことはない。

■ クラスとメンバへのアクセスをできるだけ制限する
 アクセスをできるだけ制限することは、カプセル化を促進する法則の1つである。あるルーチンをpublicにするか、privateにするか、protectedにするか迷っている場合は、適用可能なプライバシーレベルのうち、最も厳しいものを選ぶという考え方がある(Meyers 1998; Bloch 2001)。それも良いガイドラインであるが、私の考えでは、「インターフェイスの抽象化の整合性を最も維持するものはどれか」の方が重要である。ルーチンを公開することが抽象化と矛盾しないのであれば、おそらく公開しても問題はないだろう。判断がつかない場合は、一般に、少し隠すよりも多めに隠す方がよい。

綿密に設計されたモジュールとそうでないモジュールとの分かれ目となる最も重要な要因は、モジュールが内部データやその他の実装上の詳細を他のモジュールからどれくらい隠ぺいしているかである。
─ Joshua Bloch

■ メンバデータを外部に公開しない
 メンバデータを公開することは、カプセル化に違反し、抽象化をうまく制御できなくなる。Arthur Rielが指摘しているように、次のデータを公開するPointクラスは、カプセル化に違反する。

float x;
float y;
float z;

 なぜなら、クライアントコードからPointクラスのデータを自由に変更でき、その値が変更されていることをPointクラスが知っているとは限らないためである(Riel 1996)。ただし、次のデータを公開するPointクラスのカプセル化は申し分がない。

float GetX();
float GetY();
float GetZ();
void SetX( float x );
void SetY( float y );
void SetZ( float z );

 これなら、実装のベースになっているのがfloat型のx、y、zであっても、Pointクラスがそれらをdouble型で保持してfloat型に変換していたとしても、あるいはPointクラスがそれらを月世界で保管して大気圏外の人工衛星から取得していたとしても、まったく見当がつかない。

■ 実装上のプライベートな部分をクラスのインターフェイスに含めない
 本当のカプセル化では、プログラマが実装の詳細を見ることは一切不可能だろう。それらは文字どおり、そして比喩的にも、隠ぺいされている。ただし、C++をはじめとする一般的な言語では、言語の構造上の理由で、実装の詳細をクラスのインターフェイスで開示しなければならない。例を見てみよう。

class Employee {
public:
  ...
  Employee(
      FullName name,
      String address,
      String workPhone,
      String homePhone,
      TaxId taxIdNumber,
      JobClassification jobClass
  );
  ...
  FullName GetName() const;
  String GetAddress() const;
  ...
private:
ここで実装の詳細を開示
  String m_Name;
  String m_Address;
  int m_jobClass;
  ...
};
リスト6-7 クラスの実装の詳細を開示する例(C++)

 クラスのヘッダーファイルにprivate宣言が含まれているのは小さな違反に思えるかもしれない。しかし、これは他のプログラマが実装の詳細を調べるための材料になる。この場合、クライアントコードは住所にAddress型を使用する予定だが、住所はString型で保持されるという情報がヘッダーファイルで公開されている。

 Scott Meyersは、この問題を解決する一般的な方法を、『Effective C++』第2版の34項で説明している(Meyers 1998)。それは、クラスのインターフェイスをクラスの実装から切り離すという方法である。クラスの宣言には、クラスの実装へのポインタを含めるが、他の実装上の詳細は一切盛り込んではならない。

class Employee {
public:
  ...
  Employee( ... );
  ...
  FullName GetName() const;
  String GetAddress() const;
  ...
private:
実装の詳細がポインタで隠ぺいされている
  EmployeeImplementation *m_implementation;
};
リスト6-8 クラスの実装の詳細を隠ぺいする例(C++)

 そして、実装の詳細はEmployeeImplementationクラスにまとめればよい。このクラスをEmployeeクラスにのみ公開し、Employeeクラスを使用するコードには公開しない。

 こういう方法で書かれていないコードがプロジェクトに既に山のようにある場合、この方法を使用するために既存の大量のコードを変更する価値はないと考えるかもしれない。しかし、実装の詳細を公開するコードを読めば、実装の手がかりを求めてクラスインターフェイスのprivateセクションをくまなく調べたいという衝動は消えるだろう。

■ クラスのユーザーについてあれこれ推測しない
 クラスは、クラスのインターフェイスが示唆する規約に従って設計し、実装すべきである。インターフェイスについて明記されていること以外は、インターフェイスがどのように使用されるか(または、されないか)について憶測すべきでない。次のようなコメントは、クラスがユーザーについて憶測していることを意味する。

-- x、y、zを1.0に初期化する
-- 0.0に初期化するとDerivedClassが失敗する

■ フレンドクラスを使用しない
 Stateパターンのような限られた状況下では、複雑さへの対処に効果のある正しい方法で、フレンド(friend)クラス※1を使用することができる(Gammaet al. 1995)。しかし、一般的には、フレンドクラスはカプセル化に違反する。フレンドクラスは一度に検討しなければならないコードの量を増やし、結果として複雑さを増大させる。

※1 訳注 フレンドクラス:privateの部分にアクセスできるクラス。

■ パブリックルーチンしか使用しないからといって、ルーチンをパブリックインターフェイスに追加してはならない
 ルーチンがパブリックルーチンしか使用しないことは重要な問題ではない。それよりも、そのルーチンにインターフェイスの抽象化との一貫性があるかどうかを検討する。

■ 書き手の便宜よりも読み手の便宜を優先する
 最初の開発時でさえ、コードを書くことよりも読むことの方が多い。読み手の便宜を犠牲にしてまで書き手の便宜を優先しても、表面的な節約にしかならない。これは特に、クラスインターフェイスの作成に言えることである。ルーチンがインターフェイスの抽象化とうまくかみ合わなくても、そのとき作業していたクラスの特定のクライアントに都合がよいという理由で、ルーチンをインターフェイスに追加したくなることがある。だが、そのルーチンを追加することは、危険な坂道を下る第一歩である。最初の一歩を踏みとどまろう。

■ カプセル化の意味的な違反には非常に細心の注意を払う
 私はかつて、構文エラーをなくす方法さえわかれば、どのような作業も楽勝だと思っていた時期がある。それからすぐに構文エラーをなくす方法がわかったが、それはコーディングエラーという新しい劇場に入るチケットを買うだけのことで、そのほとんどは構文エラーよりも診断や修正が難しいことを思い知らされた。

何が起きているのかを理解するために基本実装を調べなければならないとしたら、それは抽象化ではない。
─ P. J. Plauger

 意味的なカプセル化は、構文的なカプセル化と同様に難しい。構文的には、クラスの内部ルーチンとデータをprivateで宣言するだけで、他のクラスの内部のしくみに首を突っ込むことを比較的簡単に避けられる。意味的なカプセル化は、これとはまったく別の問題である。次に、クラスのユーザーが意味的に違反する状況をいくつか挙げてみよう。

  • ClassAのPerformFirstOperation()メソッドがClassAのInitializeOperations()メソッドを自動的に呼び出すことがわかっているので、InitializeOperations()メソッドを呼び出さない。

  • データベースへの接続が確立されていなければemployee.Retrieve()メソッドがデータベースに接続することがわかっているので、employee.Retrieve(database)メソッドを呼び出す前にdatabase.Connect()メソッドを呼び出さない。

  • ClassAのPerformFinalOperation()メソッドが既にClassAのTerminate()メソッドを呼び出していることがわかっているので、Terminate()メソッドを呼び出さない。

  • ObjectAがObjectBをスタックに保存していて、ObjectBがまだ有効であることを知っているので、ObjectAがスコープを外れた後も、ObjectAが生成したObjectBへのポインタまたは参照を使用する。

  • ClassAのMAXIMUM_ELEMENTS定数とClassBのMAXIMUM_ELEMENTS定数が同じ値であることを知っているので、ClassAではなくClassBのMAXIMUM_ELEMENTS定数を使用する。

 これらの例の問題点は、クライアントコードをクラスのパブリックインターフェイスではなく、そのプライベートな実装に依存させていることだ。クラスの使用法を理解するためにクラスの実装を調べていることに気付いたら、それはインターフェイスに対するプログラミングではない。インターフェイスを通じて実装に対するプログラミングをしているのである。インターフェイスを通じてプログラミングすれば、カプセル化は崩壊する。カプセル化が崩壊し始めれば、抽象化もじきに崩壊する。

 インターフェイスで公開されているドキュメントだけではクラスの使用法がわからないという場合は、ソースコードを引っ張り出して実装を調べたりしないことが正しい反応である。調べるという姿勢は評価できるが、判断を誤っている。正しい反応とは、クラスの作者に連絡をとり、「このクラスの使用法がわからない」とはっきり言うことである。そして、そのときのクラスの作者の正しい反応は、あなたの質問に面と向かって答えないことだ。そして、クラスインターフェイスのファイルを調べ、クラスインターフェイスの仕様書を書き直し、新しいファイルをチェックインして、「これでわかるかどうか見てくれないかな」と知らせることである。このやり取りをインターフェイスコード自体で行えば、将来のプログラマのために残しておける。あなたの頭の中だけでやり取りすると、そのクラスを使用するクライアントコードが意味的な部分に
微妙に依存するようになる。また、個人どうしのやり取りは、あなたのコードの助けになるが、他人のコードの助けにはならない。

■ 強すぎる結合度に注意する
 「結合度」とは、2つのクラスの結び付きの強さを意味する。一般に、結合度は弱ければ弱いほど良い。この考え方から、次のようなガイドラインが得られる。

  • クラスやメンバへのアクセスをできるだけ制限する。

  • フレンドクラスは結合度が強いので使用しない。

  • スーパークラスでは、データをprotectedではなくprivateで宣言し、そ
    こから派生したサブクラスとスーパークラスとの結合度を弱める。

  • クラスのパブリックインターフェイスでメンバデータを公開しない。

  • カプセル化の意味的な違反に注意する。

  • 「デメテルの法則」に従う。

参照
デメテルの法則については、「6.3.3 メンバルーチンとメンバデータ」を参照。

 結合度は、抽象化やカプセル化と連携している。強い結合度が生じたとしたら、抽象化に漏れ穴があるか、カプセル化が壊れている。クラスのサービスが不完全になると、他のルーチンから内部データを直接読み書きする必要が出てくるだろう。そうなったら、クラスを開けてブラックボックスをガラスの箱と交換するようなもので、クラスのカプセル化は失われたも同然である。


 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 記事ランキング

本日 月間