9. オブジェクト指向

9.1. オブジェクト指向とは

9.1.1. パラダイムとしてのオブジェクト指向

実世界の問題を考える時、オブジェクトという単位があって、オブジェクト同士がお互いに相互作用しているというモデルで表す。

  • オブジェクトによってカプセル化を行うことで、問題を考えやすくする。

プログラミングだけでなく、設計も含めたソフトウェア開発全体に関するパラダイム。

9.1.2. 言語の機能としてのオブジェクト指向

オブジェクト指向言語の定義は、人によっていろいろである。最も広い定義では、

  • オブジェクトという単位があり、計算の実行と、データの記憶の両方を行う。

  • オブジェクト間でメッセージを送ることができる。(関数呼び出しで実現することが多い)

という二つの条件を満たしていればよい。

多くのオブジェクト指向言語に共通する性質としては

  • オブジェクトは動的に生成される。

  • オブジェクトは抽象データ型である。

  • オブジェクトは他のオブジェクトのコードを再利用できる。(継承、委譲など)

などがある。

9.2. 抽象データ型

抽象データ型(abstarct data type)とは、次の性質を持つような型である。

  • データの表現方法(記憶装置にどのような形式で保存されるか)は、外部から参照できない。これを情報隠蔽(information hiding)と言う。

  • 抽象データ型に対する操作は、その抽象データ型が提供するものだけであり、それ以外の操作は許されない。

この二つの性質により、データに関する抽象化が可能になる。つまり、抽象データ型内部の表現方法が変更されても、使用する側には影響がない。

Adaのパッケージ

パッケージは specificatio packagebody package の二つの部分からなる。

  • specification package には他のパッケージから呼び出すためのインターフェース情報を記述する。

  • body package にはプログラム本体を記述する。

分割コンパイルの場合、パッケージを使う側では specification package だけあればよいはずなのだが、本来は body package に書く型の表現情報が必要な場合がある(コンパイラが記憶領域割り当てのために使うため)。そのような時は、 private 宣言をする。

package Stack_Pack is
  -- public part
  type Stack_Type is limited private;
  function Empty(S : in Stack_Type) return Boolean;
  procedure Push(S : in out Stack_Type; Element : in Integer);
  procedure Pop(S : in out Stack_Type);
  function Top(S : in Stack_Type) return Integer;
  -- private part
  private
    Max_Size : constant := 100;
    type Stack_Type is
      record
      List : array ( 1 .. Max_Size ) of Integer;
      Top_Index : Integer range 0 .. Max_Size := 0;
      end record;
  end Stack_Pack

9.3. オブジェクト指向言語のデザイン

9.3.1. どのくらい純粋にオブジェクト指向にするか?

あらゆるものをオブジェクトとメッセージで表現することは可能であるが、整数の足し算などの基本的な計算からすべてオブジェクトとメッセージで実現するのはなかなか面倒。

  • C++, Javaでは、基本的な部分はオブジェクトではない。

  • Rubyは、すべてのデータがオブジェクトであるが、 whileif などの操作はメソッドではない。

  • Smalltalkは、すべてのデータがオブジェクトであり、すべての操作(代入とリターンを除く)がメッセージである。

Rubyの整数にメソッドを追加する

class Integer
  def foo
    self + 100
  end
end

1.foo  # => 101

Smalltalkの条件分岐

"Push an element into the stack"
push: anElement
  self isFull ifTrue: [ self error: 'stack full' ]
              ifFalse: [ top <- top + 1.
                         elements at: top put: anElement ]

メタクラス

RubyやSmalltalkのように、すべてのデータがオブジェクトである言語では、クラスもオブジェクトである。例えば、 new というメッセージをクラスに送ると、新しいインスタンスができる。

すべてのオブジェクトには振る舞いを定義するクラスがあるので、クラスにも振る舞いを定義するクラス(メタクラス)がある。

すべてのデータがオブジェクトであるから、メタクラスもオブジェクトである。

すべてのオブジェクトには振る舞いを定義するクラスがあるので、メタクラスのクラスもある。

すべてのデータがオブジェクトであるから(ry

9.3.2. クラスかプロトタイプか?

クラス(class)

  • オブジェクトの属性や振る舞いを記述するもの。

  • クラスから複数のインスタンスを生成することができ、すべてのインスタンスはクラスに記述された内容を共有する。

  • 複数のクラス間で属性や振る舞いを共有するために継承(inheritance)機構を備えていることが多い。

  • Smalltalk, C++, Java, Rubyなどなど。

プロトタイプ(prototype)

  • 各オブジェクトに対して、プロトタイプというオブジェクトが指定されている。

  • プロトタイプに対して委譲(delegation)を行う。つまり、オブジェクトが持っていない属性や振る舞いについては、プロトタイプに問い合わせ、プロトタイプも持っていなければ、さらにそのプロトタイプに問い合わせ…と繰り返す。

  • Self, JavaScriptなど。

9.3.3. 情報隠蔽はどのようにするか?

オブジェクトは、必要最低限の操作以外は情報隠蔽するのが原則であるが、変数を公開したい場合もある。また、特定のオブジェクトに対してのみ公開したい場合もある。ユーザが細かく指定できれば便利であるが、それだけ言語が複雑になる。

Smalltalkの場合

  • すべてのインスタンス変数は他のオブジェクトからアクセスできない。

  • すべてのメソッドは任意のオブジェクトからアクセスできる。

C++の場合

  • public : どのクラスからもアクセス可能。

  • protected : 同じクラス、サブクラス、フレンド関数からアクセス可能。

  • private : 同じクラス、フレンド関数からのみアクセス可能。

Javaの場合

クラスだけでなく、パッケージという情報隠蔽の単位がある。

  • public : どのクラスからもアクセス可能。

  • 修飾子なし : 同じパッケージの中のクラスからアクセス可能。

  • protected : 同じクラス、サブクラスからアクセス可能。

  • private : 同じクラスからのみアクセス可能。

9.3.4. 単一継承か、多重継承か?

単一継承(single inheritance)

すべてのクラスについて、スーパークラスが一つしかない

多重継承(multiple inheritance)

スーパークラスが複数存在してもよい

明らかに多重継承の方が強力であるが、言語が複雑になり、プログラマがうま く使いこなせないという意見もある。

9.3.5. 実装を継承するか、仕様を継承するか?

仕様の継承

スーパークラスの公開部分(外から見た振る舞いを規定する部分)だけがサブクラスからアクセス可能

実装の継承

スーパークラスの非公開部分(内部的な処理の部分)も含めてサブクラスからアクセス可能

実装の継承はスーパークラスのコードが再利用できて便利だが、スーパークラスを変更するとサブクラスにも影響が出る可能性があるので、カプセル化の観点からはよくない。

Javaの継承

  • 通常のクラスは extends で継承する。単一継承で実装の継承。

  • インターフェースは implements で継承する。多重継承で仕様の継承。

9.3.6. サブクラスの型

クラスを型として扱う言語では、継承関係にあるクラスの型をどのように検査するかが問題になる。普通、サブクラスはスーパークラスの部分型(元の型の部分集合で、型検査では元の型とみなすこともできる)になる。

Javaのサブクラスの型検査

public class A {
    public static void main(String args[]) {
        A x = new B();    //OK
        B y = new A();    //コンパイル時にエラー
        B y = (B)new A(); //実行時にエラー
    }
}

class B extends A {
}

課題17

上の例で A x = new B(); が許されるのに B y = new A(); が許されないのはどういう理由だと思われるか?

【解答例】

9.3.7. メソッドの束縛は静的か動的か?

スーパークラスに存在するメソッドと同じ名前のメソッドをサブクラスで定義することができる場合、どちらのメソッドを呼び出すか(メソッドの束縛)を決めなければならない。

  • 静的に束縛する方式(early binding)は、呼び出しを行う式の型情報によってコンパイル時に決める。効率が良い。

  • 動的に束縛する方式(late binding)は、実際に呼び出されるオブジェクトによって実行時に決める。柔軟性が高い。

C++の場合

  • デフォルトでは静的に束縛する。

  • virtual と宣言すると、動的に束縛する。

class Bird {
public:
  bool fly() { return true; }
};

class Penguin : public Bird {
public:
  bool fly() { return false; }
};

int main() {
  Bird *x;
  Penguin *y;

  y = new Penguin();
  y->fly() // false
  x = y;
  x->fly() // true
}

Javaの場合

  • デフォルトでは動的に束縛される。

  • final と宣言すると、サブクラスでオーバーライドされないので、静的に束縛する。