オブジェクトモデル(Objective-Cでのオブジェクト指向のプログラミング)

Last-modified: 2021-11-06 (土) 08:49:53

オブジェクトモデル

オブジェクト指向のプログラミングを深く理解するには、状態と振る舞い、データとデータに加える操作をハイレベルな単位の中、つまりオブジェクト(object)中で結合させ、このことを言語レベルでサポート、これらについて知る必要があります。1つのオブジェクトは、関連する関数のグループと、それら関数を提供するデータ構造で成り立っています。この関数群はオブジェクトのメソッド(method)と呼ばれており、データ構造の個々のフィールドはインスタンス変数(instance variable)とされています。メソッドはインスタンス変数の周りをかこみ、プログラムのそのほかの部分から見えなくしています。この様子をFigure 3-1に図示します。

 

Figure 3-1  An object. (referred from Apple Developer Library)
Figure 3-1 1つのオブジェクト

 

なにか解きづらいプログラミング上の問題に取り組んだことがあるなら、そこにあるデザインは、ある特定な種類のデータへ働きかける関数のかたまりを持っていたでしょう。これは言語サポートのない暗示的な「オブジェクト」です。オブジェクト指向のプログラミングはそれら関数のかたまりを明示的なものにし、個々の要素のことではなく、かたまりのことを考えるのを可能とします。オブジェクトのデータへとどく唯一の道、唯一のインターフェイスがメソッドだと考えられます。

状態と振る舞いの両方を結合して一つの単位にすることで、オブジェクトはそれぞれが別個のものであったときより、より良いものになります。全体は、個々の部品の集合より実質的にも優れています。オブジェクトは自己完結した「サブプログラム」のようなもので、特定な機能を発揮する面を支配する権限を持っています。これは、大きなプログラムデザインの中で、1個の独立したモジュラしての役割を果たせます。

用語:オブジェクト指向での用語は、言語によって違いがあります。たとえば、C++でメソッドはメンバ関数と呼ばれ、インスタンス変数はデータメンバとされています。この文章ではObjective-C、その起源であるSmalltalk、での用語を使います。

たとえば、住宅での水の使用をモデル化したプログラムを書こうとしたら、水を行き渡らせるシステムのさまざまな要素を表すオブジェクトを作る必要があります。その1つに蛇口オブジェクトがあり、これは水の流れを開始したり止めたり、流れで出る量をセットしたり、決まった時間内に使用された水の量を返したり、そのほかもろもろのメソッドを持つことになるでしょう。この作業をするために、蛇口オブジェクトは、栓が開いているのかしまっているのか、どれぐらいの水が消費されているのか、水がどこから来ているのかといったインスタンス変数を持つでしょう。

あきらかに、プログラム上の蛇口オブジェクトは実際のものより簡略することができます(機械的が蛇口はたくさんの計量計や計測器といった多くのものを備えているのと似ています)。現実の蛇口といえど、ほかの要素と同じく、状態と振る舞いの両方を外へ現しています。システムを効率的にモデル化するには、状態と振る舞いをを結合するプログラミングユニット、オブジェクトのようにするのが必要です。

プログラムは、パズルのある部分を解くためにお互いを呼び出す内部で結合しあったオブジェクトのネットワークで構成されています(Figure 3-2に例示します)。どのオブジェクトも、プログラムの全体的なデザインにおいて、行うべき特定の役目があり、ほかのオブジェクトと通信ができます。オブジェクトはメッセージ(messages)を通して通信し、このメッセージはメソッドを実行するためのリクエストです。

Figure 3-2  Objects in a network. (referred from Apple Developer Library)
Figure 3-2 ネットワークの中のオブジェクト

 

ネットワークの中のオブジェクトはすべて同じとは限りません。たとえば蛇口オブジェクトを加えると、水の利用をモデル化したプログラムは、その蛇口オブジェクトへ水を運べるパイプオブジェクトと、パイプ中の水流を調節するバルブオブジェクトも持つことなるでしょう。機能するように揃えているパイプバルブ蛇口、そして機器オブジェクト、これは食器洗浄機、トイレ、洗濯機があてはまりバルブを開け閉めできます。そしておそらくはこの機器オブジェクトと蛇口オブジェクトを利用するユーザオブジェクト、これらが適切に配置する建物オブジェクトがありえます。建物オブジェクトにどれくらいの水が使われているかと尋ねれば、すべての蛇口バルブへ現在の状態を報告しなさいと呼びかけるでしょう。ユーザが機器を使い始めたら、機器バルブを開けて、必要な水をえる必要があるでしょう。

メッセージメタファ

すべてのプログラミングパラダイム*には、独自の用語とメタファ**が備わっています。オブジェクト指向のプログラミングにちなむ専門用語もちいて、ある特定の視野にたってプログラムの中で何が起きている考えるようになります。(*paradigm, 方法論)(**metaphor, 隠喩)

たとえば、オブジェクトを役者に見立てて、人間のような目的と能力を授けると比喩的に表現したりします。これは時にして、オブジェクトはある状況で何をすべきか決定し、情報を求めてほかのオブジェクトにたずね、要求された情報を理解するのに自問自答し、責任をほかのオブジェクトへ委譲し、またプロセスを管理するものとして話を進めがちです。

関数やメソッドが行う仕事について考える、手続き型プログラミングでしていたように考えるよりも、このメタファではオブジェクトをメソッドを実行するものとして考えるよう勧めます。オブジェクトは状態と振る舞いに対して受け身な入れ物ではなく、プログラムの活動におけるエージェントと言えます。

このメタファは実際にとても便利です。1つのオブジェクトは、役者のように多数の側面をもちます。プログラムの全体にわたるデザインの中で果たすべき特定の役割があり、その役割の中でプログラムのほかの部分とは完全に独立して行動します。自身の役割を果たすために、ほかのオブジェクトと情報を交換しあいしますが、自身を自分で内包し、ある程度は自分自身のために仕事をします。演技中の役者と同じように、台本から逸脱することはできませんが、演じる役割は多面性があり複雑なものでも可能です。

オブジェクトを役者へ見立てるアイディアは、オブジェクト指向プログラミングの重要なメタファとぴったり一致します。オブジェクトはメッセージを通して通信するというアイディアです。関数でそうしたようにメソッドを呼び出すのではなく、オブジェクトがメソッドの中の1つを実行してくれるように、オブジェクトへ要請のメッセージを送るのです。

このメタファという概念を理解するには時間がかかりますが、メタファはメソッドとオブジェクトを俯瞰する便利な方法となります。メソッドが働きかける特定のデータから離す形でメソッドを抽象化し、その代わりに、その振る舞いのみに集中させます。たとえば、あるオブジェクト指向プログラミングのインターフェイスでは、ある始まりのメソッドがある操作を起動し、あるアーカイブメソッドは情報をアーカイブし、、そしてあるドローメソッドは画像を作ることになります。厳密にどのメソッドが起動されたのか、どの情報がアーカイブされたのか、どの画像が描かれたのかはメソッドの名前から分かりません。異なったオブジェクトは、異なった方法でメソッドを実行することになるでしょう。

したがって、メソッド群は、抽象的な振る舞いの語彙集となります。それら振る舞いの1つを呼び出すには、メソッドを1つのオブジェクトへ関連づけることで、振る舞いを具体的なものにする必要があります。これはオブジェクトを、メッセージのレシーバ*に指名することで行われます。レシーバとして選んだオブジェクトは起動するべき操作、アーカイブすべきデータ、または描かれるべきイメージを正確に把握します。(*receiver: 受け手)

メソッドはオブジェクトに付随しているので、特定のレシーバ(メソッドと、メソッドが働きかけるデータ構造のオーナー*)を通してのみ呼び出されます。レシーバが異なれば同じメソッドでも異なった実行の可能性があります。結果として、レシーバが違えば同じメッセージへの対応は別のものになる可能性があります。メッセージから得られる結果は、メッセージもしくはメッセージの名前からだけでは予測できません。メッセージを受けるオブジェクトに依存しているのです。(*owner: 所有者)

レシーバ(要求に応えられるメソッドのオーナー)からメッセージ(要求された動作)を切り離すことで、メッセージメタファは、振る舞いが特定の実装から離れて抽象化できるというアイディアを、完璧なものにしています。

クラス

プログラムは、同じ種類のオブジェクトを1つ以上を持てます。たとえば、水の使用をモデル化するプログラムは、蛇口パイプ、そしておろらくは何百もの機器と利用者を保持してしるものでしょう。同じ種類のオブジェクトは同じクラスのメンバといいます。1つのクラスの全メンバは、同じメソッドを実行でき、同じひとそろいのインスタンス変数を持つことができます。それらは1つの共通する定義を、どの種類もたった一度だけ定義されて、それを共有しています。

これについては、オブジェクトはCの構造体と似ています。構造体の宣言は型を定義します。たとえば次の定義では、key型の構造体を定義します。

 
struct key {
   char *word;
   int count;
};
 

構造体が一度定義されると、その構造体の名前を使い、その型を持つインスタンスをいくらでも生成できます。

 
struct key  a, b, c, d;
struct key *p = malloc(sizeof(struct key) * MAXITEMS);
 

宣言とは1つの構造体に対しての雛形*ですが、宣言だけではプログラムが使える構造体を生成できません。その型の実際の構造体のためにメモリーを割り当てるもう1つのステップを取ります。この1つのステップは何回も繰り返すことができます。(*template, テンプレート)

これと同じように、オブジェクトを定義すると、オブジェクトの雛形を生成できます。これはオブジェクトのクラスを定義します。雛形は、同じようなオブジェクト ークラスのインスタンス(instance)ー をたくさん作るのに使われます。たとえば、蛇口クラスの定義は1つだけです。この定義を使って、プログラムは必要なだけ蛇口インスタンスを割り当てられるでしょう。

クラス定義は、インスタンスの一部となるデータ要素(インスタンス変数)を配置する構造体の定義に似ています。どのインスタンスも自前のインスタンス変数のために割り振られたメモリを持っており、インスタンスに特有な値を格納します。

しかし、クラス定義は、クラスのメンバの振る舞いを指定するメソッドも定義しているという点で、構造体の定義とは異なります。すべてのインスタンスは、そのクラスのために定義されたメソッドへのアクセスするという点に特徴があります。2つのオブジェクトのデータ構造が同じでも、メソッドが違っていれば、同じクラスには属していないでしょう。

モジュール性

Cプログラマにとって、モジュールとはソースコードが入っているファイルにすぎません。大きな(そんなに大きくなくても)プログラムをいくつかのファイルへ分割すれば、個々に管理しやすい部品とできる便利な方法です。どの部品も独立して動き、個別にコンパイルでき、そのあとで、ほかの部品と合わしてプログラムがリンクされて1つにまとめることができます。

静的保存クラス指定子を用いて、名前空間のスコープをそれが定義されたファイルのみと制限すれば、ソースモジュールの独立性は高まります。この種のモジュールは、ファイルシステムで定義された1つのユニットです。これはソースコードの入れ物であって、言語上の論理的なユニットではありません。何がコンテナに入っているかはプログラマ次第です。コードの論理的に関係している部分をまとめようと、それらモジュールを使えますが、そうする必要もありません。ファイルはタンスの引きだしのようなものです。靴下を1つの引出に入れ、下着のそのほかの、などなど。もしくは他のやり方で収納したり、単純にすべてを混ぜて入れるという選択もできます。

メソッドへのアクセス:インスタンス変数がそうであるように、メソッドをオブジェクトの一部と考える都合がよいです。Figure 3-1のように、メソッドはオブジェクトのインスタンス変数を取り囲むものと図示できます。しかしメソッドはメモリの中でインスタンス変数と一緒にまとめられていません。メモリは新しいどのオブジェクトでもインスタンス変数へ割り当てられますが、メソッドにメモリを割り当てる必要はありません。1つのインスタンスがただ1つ必要としているのはメソッドへのアクセスであり、同じクラスのすべてのインスタンスは同じメソッド一式へのアクセスを共有しています。メモリには1つだけメソッドのコピーがあり、1つのクラスからどんなにたくさんのインスタンスが作られようとも関係ありません。

オブジェクト指向のプログラミング言語はソースコードの入れ物としてファイルを使うのをサポートしていますが、論理的なモジュールという概念 ークラス宣言ー を言語に追加しています。期待のとおり、どのクラスも自身のソースファイルの中で定義されています。この論理的なモジュールがモジュールの入れ物と一致しているのです。

たとえば、Objective-Cでは、パイプクラスを定義する1つのファイルの中に、そのパイプオブジェクトと相互作用するバルブクラスを定義できます。ですがこれは、パイプに関するコードをの入れ物モジュールを生成し、バルブを別のファイルへを分ける方が良いでしょう。バルブクラスの定義はプログラム作成において1つのモジュール単位としてそのまま扱われます。ソースコードが入っているファイルがどんなに増えても、1つの論理的モジュールとなります。

言語が提供するクラス定義を論理的ユニットにするメカニズムは、「抽象化のメカニズム」の項でいくらか詳細に説明しています。

再利用性

オブジェクト指向のプログラミングの第一のゴールは、コードをできるかぎり再利用できるもの、異なるたくさんの場面とアプリケーションに対応できるように、することです。したがって、たとえ微細な違いであっても、すでに存在するコードを書きなおすのが避けられます。

再利用性は次のような因子から影響を受けます。

  • どれくらいコードが信頼性があり、バグがないか
  • どれくらいドキュメントが明快か
  • どれくらいプログラムインターフェイスがシンプルで直感的か
  • どれくらいコードがタスクを実行するのに効果的か
  • どれくらい機能が豊富か

これらの因子を適用するのはオブジェクトモデルだけではありません。すべてのコード、標準C関数やクラス定義、これらが再利用性を判断するのに使われます。たとえば効果的で十分に自己文章化してある関数は、文章化しておらず信頼できないものよりはるかに再利用されるでしょう。

それでも、一般的な比較では、クラス定義は関数ができない方法で再利用可能なコードを提供しているといわれています。関数をより再利用性の高いものにする方法はいくらかあります。たとえば、特別な名前を持つグローバル変数を使おうとしないで、データをパラメータとして渡したりします。そうやったとしても、一般化できた関数の小さなサブセットしか、その関数が最初に設計されたアプリケーション以外へと引っ張り出すことができません。これらの再利用性は、本質的に、次の3つの方法と制限されています。

・関数の名前はグローバルです。どの関数も、固有の名前を持たなければなりません(静的に宣言されたものをのぞく)。この命名規則は、複雑なシステムを作る際で、過度にライブラリコードへ依存するのが難しくなります。プログラムインターフェイスはほとんど把握できなくなり、重要な概念を得るのが簡単できなくなるほど広範囲へ散らばってしまいます。

その一方でクラスは、プログラムインターフェイスを共有できます。同じ命名規則が何度も使用されれば、多岐にわたる機能性を、比較的小さくて理解しやすいインターフェイスでパッケージ化できます。

・関数は、任意の時に、ライブラリの1つから選ばれます。必要としている個々の関数を探して選び出すのはプログラマの自由です。

逆に、オブジェクトは機能性のパッケージとしてもたらされ、個々のメソッドやインスタンス関数のかたまりではありません。これらは統合した機能を提供するので、オブジェクト指向ライブラリの利用者は問題を解く鍵をつなぎ合わせるのに難航することはありません。

・関数は、特定のプログラムのために考え出された特定のデータ構造へ一般的につなぎ合わされています。データと関数の相互作用は、インターフェイスの避けられない部分です。1つの関数は、パラメタとして許容できる種類のデータ構造をあつかうときのみ役に立つのです。このやり方はデータを隠すので、オブジェクトにはこの問題がありません。これは、クラスが関数よりも簡単に再利用される重要な理由の1つです。

オブジェクトのデータは保護されており、プログラムのほかの部分からは操作できません。したがって、メソッドはデータの一貫性に信頼をおけます。外からのアクセスによってデータが非論理的か一貫性を欠いたものにできないと確信できます。この結果として、オブジェクトのデータ構造は、関数へ渡されたものより高い信頼性があり、メソッドは一層そのデータへ依存できます。結果として、再利用可能なメソッドが簡単に書けます。

そのうえ、オブジェクトのデータは隠されているので、クラスはそのインターフェイスに影響をあたえずに、異なったデータ構造をつかう実装にできます。クラスを使うすべてのプログラムは、なんらソースコードを変更しないまま新しいバージョンのクラスを使えます。再プログラミングの必要はありません。

抽象化のメカニズム

この点について、オブジェクトは高いレベルの抽象化を実現するユニットで、またアプリケーションの中で一貫した役割を持つモノとして導入されています。しかし、それらをさまざまな言語的なサポートが無しにこ扱うのは無理でしょう。最も重要なメカニズムの中の2つがカプセル化とポリモーフィズムです。(Encapsulation: 隠蔽)(Polymorphism: 多相性)

カプセル化はオブジェクトの実装がインターフェイスより外へ出て行かないようにし、ポリモーフィズムはどのクラスにもそれ自身の名前空間を与えることから生じています。このあとのセクションでは、順々にそれらのメカニズムを解説します。

カプセル化

抽象化のどのレベルにおいても、効果的にデザインを行うには、実装の詳細を陰に隠せるようにし、共通のインターフェイスの元で、それらの詳細な事柄を束ねるユニットがあるのを考える必要があります。プログラミングユニットが真に効果的なものとするには、インターフェイスと実装とを隔てるバリヤーの存在が絶対条件です。インターフェイスは実装をカプセル化しなくてはなりません。つまり、実装をプログラムのほかの部分から隠します。カプセル化は、実装部分を予想していないアクションと不注意なアクセスから守ります。

Cにおいて、関数は明確にカプセル化されています。その実装は、プログラムのほかの部分へアクセスすることはできず、関数本体の外で起こったどんな出来事からも守られます。Objective-Cにおいて、メソッドの実装は同様にカプセル化されていますが、それより重要なのはオブジェクトのインスタンス変数もカプセル化されていることです。これらインスタンス変数は、オブジェクトの中に隠されていて、外からは見えません。インスタンス変数のカプセル化は時にして情報の隠蔽*と呼ばれます。(*information hiding

このことに初めて出会うと、インスタンス変数の情報を隠蔽してしまうのは、プログラマの自由を制限しているように聞こえるかもしれません。実際は、できることの幅が拡がり、このやり方でないなら押しつけられていた制限から解放されます。オブジェクトの実装のどの部分もほかの部分へ漏れ出し、アクセス可能になるか、プログラムのほかの部分と関連を持つかして、オブジェクトを実装する側のプログラマの手とオブジェクトを使う側のプログラマの手が硬く結びつけられてしまします。こうなると、どちらも、もう一方をまず最初にチェックしなくては、変更を加えられません。

たとえば、あなたは水の利用をモデル化したプログラムのために開発された蛇口オブジェクトに興味があり、自分が書きつつある他のプログラムへ組み込みたいとします。オブジェクトのインターフェイスが一度決められてしまえば、いつ他のものがそれに働きかけるのか、バグを修正のか、それを実行するもっと良い方法を見つけるのかに関心を向けなくても済みます。あなたはインターフェイスのみに依存しているので、あなたのコードを破壊するようなことは何もできません。プログラムは、オブジェクトの実装から隔離されています。

それに加えて、それら蛇口オブジェクトを実装する側は、あなたがどうやってそのクラスを使うのかに興味があり、あなたの要求を満たすものかを確かめる必要はありますが、あなたがどのようにしてコードを書いているかの方法については関心を持つ必要がなくなります。オブジェクトの実装へ手を入れることはできず、将来のリリースで実装の変更をする自由も制限できません。実装は、オブジェクトがしなくてはならないどんなことからも隔離されています。

ポリモーフィズム

異なったオブジェクトがそれぞれのやり方で同一のメッセージに応答できる能力を、ポリモーフィズム*と呼びます。(*Polymorphism

ポリモーフィズムは、どのクラスも自分が持つ名前空間に存在しているという事実から生じています。クラス定義において割り振られた名前は、ほかで割り振られた名前と衝突しません。このことは、オブジェクトのデータ構造の中にあるインスタンス変数でも、オブジェクトのメソッドでも、両方に当てはまります。

  • C構造体のフィールドが保護された名前空間の中に存在しているのと同じように、オブジェクトのインスタンス変数もそうなっています。
  • メソッドの名前もまた、保護されています。C関数の名前と異なり、メソッドの名前はグローバルシンボルではありません。1つのクラスにあるメソッドの名前は、ほかのクラスにあるメソッドと衝突しえません。まったく異なる2つのクラスは、全く同じ名前を持つメソッドを実行できます。

メソッドの名前は、オブジェクトインターフェイスの一部です。あるメッセージが、ほかのオブジェクトが何かするのを要求するために送られたら、そのメッセージはオブジェクトが行うべきメソッドの名前が名付けられています。異なったオブジェクトが、同じ名前を持つメソッドをそれぞれで持てるので、メソッドの意味は、メッセージを受け取る特定のオブジェクトに関連するものと理解できなければなりません。2つの異なるオブジェクトへ向けて送られた同じメッセージは、2つの別個なメソッドを呼び出せます。

ポリモーフィズムの主要な利点は、プログラムインターフェイスを簡素にすることです。これは、クラスからクラスへと渡って再利用ができる規則を決められるようにしています。プログラムへ加えるすべての新しい関数へ新しい名前を考え出す代わりに、同じ名前を再利用できます。プログラムインターフェイスは、一式の抽象的な振る舞いとして記述され、クラスでの実装と完全に切り離されます。

オーバーローディング*ポリモーフィズムパラメタオーバーローディングという用語は、基本的に同じものを指しますが、その視点はわずかに違います。ポリモーフィズムは多元的*なものの見方をし、複数のクラスが同じ名前のメソッドを持つのを認めています。パラメータオーバーローディングはメソッド名寄りのものの見方をして、メソッドへ渡されたパラメータによって異なる効果が得られるのを認めています。オペレータオーバーローディングも似ています。言語に備わった操作(たとえばCでの ==, + 演算子)を、特定な種類のオブジェクトにおいて、特定の解釈を割り振ったメソッドへ変換する能力のことです。Objective-Cはメソッドの名前でポリモーフィズムを実装しており、パラメータやオペレータオーバーローディングではありません。(*pluralistic, 根源が多くあること)(**overloading

たとえば、あたえられた時間における機器オブジェクトが使う水の量を報告したいとします。機器クラスへはamountConsumedメソッド、蛇口クラスへはamountDispensedAtFaucetメソッドと、建物クラスへはcumulativeUsageメソッドをそれぞれ定義する代わりに、どのクラスにもcumulativeUsageメソッドを簡単に定義できます。この統合は、概念として同じ操作に使うメソッドの数を減らせます。

ポリモーフィズムは、すべてのケースを列挙する1つの関数の中にコードを集めてしまうのではなく、異なったオブジェクトのメソッドの中で独立したものとしてコードが存在できるようにします。これにより、コードは一層と拡張性と再利用性があるものにできます。新しいケースがあらわれたとき、すでに存在するコードを実装し直す必要はありません。新しいメソッドを持つ新しいクラスを加えるだけで、コードはすでに存在したままにできます。

たとえば、あるオブジェクトへ描画のメッセージを送るコードがあったとします。レシーバに依存して、そのメッセージは可能性がある2つのイメージのうち1つを生成することになるとします。三つ目のケースを加えたいときは、メッセージ自体を変えるのも、すでに存在するコードを変える必要はありません。メッセージのレシーバとして、ほかのオブジェクトを割り振るだけです。

継承

何か新しいことを説明する最も簡単な方法は、すでに理解している何かから話を始めることです。もしスクーナー*がなんなのか説明したいとなら、聞いているひとが帆船が何かをすでに知っているかどうかが助けになります。ハープシーコード**がどうやって動くのか説明したいとしたら、最もいいのは、耳を傾けるひとがすでにピアノの中を見たことがあるか、演奏中のギターを見たことがあるか、もしくは、せめてなにかの楽器について何かを思い浮かべられるかを仮定してみることです。(*schooner: 通例 2 本マスト,時には 3 本マスト以上の縦帆式帆船、研究社新英和中辞典より、以下省略)(**harpsichord: ピアノの前身で 16‐18 世紀に盛んに用いられ,現在も用いられている鍵盤楽器)

これと同じ事が、新しい種類のオブジェクトを定義したいときにも当てはまります。記述は、すでに存在するオブジェクトの定義から始められれば、より簡単になります。

このことを念頭に置くと、オブジェクト指向のプログラミングは、新しいクラスの定義をすでに定義されているクラスの上に築くのを可能にしています。基となったクラスをスーパクラスと、新しいクラスはサブクラスと呼びます。サブクラスの定義では、どんなふうにスーパークラスと違うのかのみを指定します。そのほかは、すべて同じと扱われます。

何もスーパークラスからサブクラスへはコピーされません。その代わりに、すべてのスーパークラスのメソッドとインスタンス変数をサブクラスは継承することで、2つのクラスはつながれます。これは、あなたに耳を傾ける人々が帆船についてすでに知っていることを引き継いで、スクーナーを理解して欲しいのとよく似ています。サブクラスの定義に何もないなら(自分自身へのインスタンス変数もメソッドも何も定義していないなら)、2つのクラスは同じもの(名前を除いて)で同じ定義を共有しています。フィドル*が何なのか説明するのに、それはバイオリンと全く同じものだといえばいいのです。しかし、サブクラスを宣言する理由は同意語を創り出すのではありません。スーパークラスと少しは違う何かを生成することです。たとえば、フィドルがクラッシック音楽に加えてブルーグラス**を奏でられるようにと、その振る舞いを拡張することもできます。(*fiddle: バイオリンのことを、くだけた,あるいは多少おどけた言い方)(**bluegrass: 米国南部の白人民俗音楽から生まれたカントリー音楽)

クラス階層

どのクラスも、スーパークラスとして新しいクラス定義へ使うことができます。あるクラスは、ほかのクラスのサブクラスであるのと同時に、自身のサブクラスのスーパークラスになれます。したがって、任意の数のクラスが継承の階層でつながっています。これを概略図を Figure 3-3 に示します。

 

Figure 3-3  An inheritance hierarchy (referred from Apple Developer Library
Figure 3-3 継承階層

 

何のスーパークラスも持たないルートクラスから、すべての継承階層は始まります。ルートクラスから、継承構造は下方向へ枝分かれします。どのクラスもスーパークラスを継承し、さらにそのスーパークラスを通って、継承階層の上位に位置するクラスを継承しています。すべてのクラスはルートクラスから継承しています。

どのクラスも、継承の連鎖にあるクラス定義が積み重なったもの*です。上で示した例では、クラスDは、これのスーパークラスのCとルートクラスの両方を継承しています。クラスDのメンバは、D, Cそしてルートと3つのクラスで定義したメソッドとインスタンス変数をすべて持ちます。(*accumulation, 累積)

一般的に、すべてのクラスは1つだけのスーパークラスを持ち、無数のサブクラスを持てます。しかし、いくつかのオブジェクト指向のプログラミング言語では(Objective-Cではありません)、1つのクラスが複数のスーパークラスを持つことができます。クラスは複数のソースから継承できるのです。Figure 3-3で示した下方向へ枝分かれする1つの縦割りの階層をもつ代わりに、複数から継承するのは階層の枝分かれを(もしくは異なった継承関係)を混ぜ合わせられます。

サブクラス定義

サブクラスは、スーパークラスから継承した定義を、3つの方法で変えることができます。

  • 新しいメソッドや新しいインスタンス変数を加えることで、継承したクラス定義を拡張できます。これがサブクラスを定義する最も一般的な理由です。いつでもサブクラスは新しいメソッドを加えることができ、いっぽうでメソッドが必要としたときには新しいインスタンス変数も加えることができます。
  • すでに存在するメソッドを新しいバーションで置き換えて、継承した振る舞いを変更できます。これは継承したメソッドと同じ名前を持つメソッドを実装するだけで済みます。新しいバージョンは、継承したバージョンをオーバーライドします。(継承したメソッドが見えなくなるわけではありません。これを定義したクラスやほかの継承クラスにおいて定義したクラスで依然と有効です。)
  • すでに存在するメソッドを、新しいバージョンのもので置き換えて、継承した振る舞いを改良したり拡張したりできますが、新しいメソッドの中に組み込まれて古いバージョンは依然保持されています。あるサブクラスが、その新しいメソッドの中にある古いバージョンのものを動作させるメッセージを送ります。継承の連鎖にあるどのクラスも、メソッドの振る舞いの一部として役目を果たすことができます。たとえばFigure 3-3は、クラスDはクラスCで定義されたメソッドをオーバーライドしてクラスCのバージョンを合わせられます、クラスCのバージョンがルートクラスで定義されたバージョンを組み込んでいたとしてもです。

したがってサブクラスは、スーパークラスの定義を満たしつつ、より特定でより特殊化したものにするのに役立ちます。サブクラス化はコードを減らすのではなく、加えたり、何かに置き換えたりします。一般的に、メソッドを継承しないとはできませんし、インスタンス変数は取り除いたり上書きは出来ないのに注意してください。

継承の使用

継承階層の伝統的なたとえは、動植物の分類学から借りてきています。たとえば、木のマツ科に相当するクラスがあったとします。そのサブクラスはモミトウヒマツツガカラマツベイマツスギを挙げることができ、その科を構成するさまざまな属に相当します。マツクラスにはSoftPineHardPineサブクラスがあり、SoftPineのサブクラスにはWhitePineSugarPineそしてBristleconePineHardPineのサブクラスのはPonderosaPineJackPineMontereyPineそしてRedPineがあります。(訳者注。分類学上、科がスーパクラスで、サブクラスは属、その下が種です。たとえば、カラマツ(赤い木肌のマツ)はマツ科カラマツ属カラマツ種と分類されます。)

分類学をプログラムする理由はほとんどありませんが、分類学と類似させて考えるのはよいことです。サブクラスはスーパクラスを特化するか特定の目的に適合させるのに役立ちます。これはまさに、種は属を特化するようなことです。

継承の一般的な使い方を挙げてみます。

  • コードの再利用。二つ以上のクラスが、何かを共有しているが、何かをする方法が違う場合、共通の要素を一つのクラス定義に納めて、ほかのクラスが継承することができます。共通したコードは共有され、たった一度実行されるときに必要とされます。たとえば、COLOR(#888888){蛇口}、COLOR(#888888){バルブ}、COLOR(#888888){パイプ}オブジェクトが水の使用をモデル化したプログラムの中で定義されていて、それらすべては水の供給元と繋がっていなければならず、水が流れ出る速さを記録しなければならないとします。この行為はそれぞれ共通なもので、蛇口バルブパイプオブジェクトが継承した一つのクラスの中でただ一度だけコードにすれば済みます。蛇口オブジェクトはバルブオブジェクトの一つの種類ということができ、おそらく蛇口クラスはバルブオブジェクトから多くのものを継承し、それ自身のものをわずかに加えるだけです。
  • プロトコルの導入。そのサブクラスが実装するだろうと予測される、いくつかのメソッドをクラスは宣言できます。クラスは、それらメソッドの空のバージョンを持つか、サブクラスのメソッドの中へ組み入れられる実装の部分的なバージョンを持つでしょう。どちらのケースでも、すべてのサブクラスが従わなければならないプロトコルを宣言が設定できます。異なったクラスが同じような名前のメソッドを実装するときは、プログラムがポリモーフィズムをそのデザインで使えるようにすると良いことがあります。サブクラスが実行しなくてはならないプロトコル**を導入することは、関係するクラスが守らなければならない規則*を確実なものにします。(*convention)(**protocol
  • ジェネリック*な機能の提供。実装する側の人間は、問題を解くための多くの基本的で一般的なコードを含むクラスを定義できますが、そのコードは詳細のすべてを満たしていません。そのあとで、他のひとが、その一般化したクラスを特定の要求に合わせたサブクラスを生成することができます。たとえば、水の利用をモデル化するプログラムにおける機器クラスは、一般的な水利用の装置を定義して、そのサブクラスを特定の種類の機器とできます。(*generic; 一般的な,包括的な, specific の反語)
    したがって継承は、他のだれかのプログラムタスクを簡単にするとともに、実装をレベルで分離する方法でもあります。
  • 変更を小さいものにする。継承を、一般的な機能をもたらすため、プロトコルを導入ため、コードを再利用するために使えば、クラスはほかのクラスが継承したい物になります。一方で、スーパークラスになると意図して作られていないクラスに変更を加えるにも、継承を使うこともできます。たとえば、あなたのプログラムで満足する動作をしているオブジェクトがあったとすると、それがしていることの一つか二つだけを変えたいとは思わないでしょう。その変更をサブクラスの中で行うことができます。
  • 可能性を予見する。サブクラスは、テスト目的のために、他の因子を外へ括り出すのにも使えます。たとえば、クラスがある特定のユーザインターフェイスのためにコード化されたとすると、代わりになるかもしれないインターフェイスを、プロジェクトを設計する過程で、サブクラスの中へ分離するのが可能です。そうすると、どのインターフェイスも、潜在的なユーザへどちらが自分に適しているか見せる事ができます。選択が行われたら、選ばれたサブクラスをスーパークラスへ統合させられます。

ダイナミズム

プログラムの歴史のあるときに、プログラムがどれぐらいもメモリを使うのかとの問いは、ソースコードがコンパイルされてリンクされたときに決まると一般的にされていました。プログラムが必要とするすべてのメモリすべてが、それが起動されたときに取り分けられました。このメモリは固定されていて、大きくなったり小さくなったりはできませんでした。

その後に、これには重大な制限であるのが明らかになりました。どうやってプログラムが構成されているのかを制限するだけではなく、何をプロググラムができるかを想像するのも制限しました。デザインを制限するのであって、プログラミングテクニックだけはそうなりませんでした。プログラムを実行している際に、動的にメモリを割り当てる(たとえば malloc)関数を導入することで、それまでなかった可能性が開かれました。

コンパイル時とリンク時での制限は限定的です。とゆうのは、コンパイラとリンカは重要な点を決定するのに、プログラムが実装された際のユーザから得られた情報を基にするのではなく、プログラマのソースコードの中で見つかる情報を基にしているからです。

そのような制限の一つをダイナミックアロケーションは取り除きますが、静的メモリアロケーションが制限しているのと同じように、ほかたくさんある制限は依然残っています。たとえば、プログラムを構成する要素は、コンパイル時にのデータ型と一致しなくてはなりません。またアプリケーションの境界は、一般的にはリンク時に設定します。アプリケーションのどの部分も、一つの実行形式ファイルへ統合されなくてはなりません。新しいモジュールや新しい型は、プログラムの実行時に導入できません。

Objective-Cはそれら制限を克服しようと努めており、プログラムができるだけ動的*で流動性に富むものとしようとしています。コンパイル時とリンク時に行う大多数の決定をランタイムへ移動させています。これのゴールは、言語、コンパイラ、リンカーからの要求で行動を制限するのではなく、プログラムのユーザにつぎに何が起こるだろうかを決めてもらうことです。(*dynamic: ダイナミック)

3つの種類のダイナミズムが、オブジェクト指向のデザインでは特に重要です。

  • ダイナミックタイピング(動的型付け)、オブジェクトのクラス決定をランタイムまで待つ
  • ダイナミックバインディング(動的割り付け)、呼び出すメソッドをランタイムに決める
  • ダイナミックローディング(動的呼び出し)、プログラムの実行時に新しいコンポーネントをプログラムに加える

ダイナミックタイピング

コードが変数を適合しない型へ割り振ろうとすると、一般的にコンパイラは警告を発します。次のような警告を見たことがあるでしょう。

incompatible types in assignment
assignment of integer from pointer lacks a cast
// 互換性のない型への割り当て。
// ポインタから整数への割り当てにキャストが足りません。

型チェックは便利ですが、ときにはポリモーフィズムから恩恵を得るのの障害となり、特にどのオブジェクトの型もコンパイラが知らなければなりません。

たとえば、あるオブジェクトに、メソッドを呼び出すメッセージを送りたいとします。ほかのデータ要素と同じように、オブジェクトは変数で表現されています。もし変数の型(属するクラス)がコンパイル時に分かっていなければならないなら、ランタイム要素は、どんな種類のオブジェクトをその変数へ割り当てればいいかの決定へ影響をあたえるられなくなります。変数のクラスがソースコードの中で固定されていたら、メソッドが起動するのはstartのバリエーションとなります(If the class of the variable is fixed in source code, so is the version of start that the message invokes. 訳者、ここちょっとわかりません。*version, 型?変形?改良版?, variation、倒置法??)。

その一方で、もし変数のクラスを発見するまでランタイムを待たせておけるとしたら、どんな種類のオブジェクトも適切なものへ割り振ることができます。レシーバのクラスに応じて、スタートメッセージは異なった種類のメソッドを呼び出して、まったく違う結果を生成します。

したがって、ダイナミックタイピングは、対象へダイナミックバインディングをあたえます(次の章で解説します)。いえ、それ以上のことをします。オブジェクト同士の関連づけを、静的なデザインのにおいてコードにしなくてはならないのではなく、ランタイムに決められるようにします。たとえば、どんな種類のパラメタか厳密に宣言されていないオブジェクトを、パラメータとしてメッセージは送れます。つまり、クラスを宣言していなくてもいいのです。それからメッセージレシーバは、自身のメッセージをオブジェクトへ、これもまたどんな種類のオブジェクトを構うことなく送ります。レシーバは渡されたオブジェクトを何かの自分の仕事に使うので、ある意味、不定な型のオブジェクトでカスタマイズされています。

ダイナミックバインディング

標準Cにおいて、一連の代替関数、たとえば一般的な文字比較の関数を再定義できます。

int strcmp(const char *, const char *);   /* case sensitive */
int strcasecmp(const char *, const char *); /*case insensitive*/

そして、返り値とパラメータのそれぞれが同じ型となる関数へのポインタを宣言します。

int (* compare)(const char *, const char *);

それから、そのポインタへどちらの関数を割り振るのか決めるのをランタイムまで待ちます。

if ( **argv == 'i' )
   compare = strcasecmp;
else
   compare = strcmp;

そしてポインタを通して関数を呼び出します。

if ( compare(s1, s2) )
   ...

これは、オブジェクト指向のプログラミングでダイナミックバインディング*と呼ばれるものと同種で、プログラムが走るときまで厳密にどのメソッドが実行されるかの決定を遅らせます。(* dynamic binding)

これを、すべてのオブジェクト指向の言語がサポートしてはいませんが、ダイナミックバインディングはメッセージングを通してごく自然に透明性を持って行えます。例に示したように、ポインタを宣言し値に割り付ける間接化を行う必要はありません。代替の手続きへそれぞれに違った名前を付ける必要もありません。

メッセージは間接的にメソッドを呼び出します。どのメッセージ表現も「呼び出す」べきメソッドの実装を必ず見つけ出します。そのメソッドを見つけるために、メッセージング機構はかならずレシーバのクラスをチェックして、メッセージで名前を指定されたメソッドの実装の位置をつきとめます。ランタイムでこれが実行されると、メソッドは動的*にメッセージへ結びつけられます。コンパイラがこれを実行すると、メソッドは静的に結びつけられてしまします。(*dynamically)

遅延バインディング。いくつかのオブジェクト指向のプログラミング言語(有名なのはC++)は、メッセージレシーバはソースコードで静的に型付けされるように求められますが、その型が厳密なものとは求められません。オブジェクトは、自身のクラスか、それを継承したほかのクラスへ型付けされます。したがってコンパイラは、メッセージのレシーバが型宣言で指定された同じクラスのインスタンスなのか、サブクラスのインスタンスなのか、もしくはもっと遠いところに由来を持つクラスのインスタンスなのかどうかを見分けられません。レシーバの正確なクラスを知らないので、メッセージで名指しされたメソッドのどのバージョンを呼び出すべきなのか分かりません。この状況では、レシーバを指定されたクラスのインスタンスとみなし、単にそのクラスで定義されたメソッドをメッセージへ割り付ければいいのか、それとも、その置かれた状況が解決される時間まで待てばいいのかが選択肢になります。C++において、その選択はメソッド(関数のメンバ)が仮のも**のとして宣言されるリンク時まで、先延ばしにされます。これはときどき、ダイナミックバインディングというよりは遅延バインディング*と呼ばれます。ダイナミックの意味はランタイムに起きるということの一方で、遅延バインディングは厳密なコンパイル時の型付けによる束縛をともないます。ここで解説しているのは(そしてObjective-Cで実装されているのは)、束縛のないダイナミックバインディングです。(*late binding)(**virtual)

ダイナミックバインディングは、ダイナミックタイピングがなくても可能ですが、まったく興味を引くものではありません。レシーバのクラスが固定されてコンパイラに知らされるときに、ランタイムがメソッドがメッセージへ照合するまで待つのでは、ほとんど役に立ちません。コンパイラはメソッドを自力で見つけ出しても構いませんが、ランタイムでの結果と大して違いはないでしょう。

しかし、レシーバのクラスがダイナミックタイピングされていれば、どのメソッドを起動すべきかコンパイラが決定する方法はありません。メソッドは、ランタイム時にレシーバがどのクラスなのか解消された後でのみ、見つけ出せます。ですからダイナミックタイピングは、ダイナミックバインディングを必要としているのです。

加えてダイナミックタイピングはダイナミックバインディングを興味深いものにしています。というのは、ダイナミックタイピングはレシーバのクラスによってまったく異なる結果をメッセージが受ける可能性を開いています。ランタイム因子はレシーバの選択と外から来たメッセージへ影響を与えます。

また、ダイナミックタイピングとバインディングは、まだ実装していないオブジェクトに向けてメッセージを送れる可能性も開いています。オブジェクトの型をランタイムまで決定しなくてもいいなら、自由にクラスをデザインして、データ型に名前を付け、さらにコードがそれらオブジェクトへメッセージを送る、これらの自由を開放します。統一するのはメッセージであって、データ型ではありません。

 

注意:ダイナミックバインディングは、Objective-Cに組み込まれています。バインディングのために特に何かを調整する必要はないので、デザインにおいて何か起きていることに気を回す必要はありません。

ダイナミックローディング

歴史的に、多くの一般的な環境では、プログラムを実行できる状態にする前に、そのすべての部品はリンクされて一つのファイルへまとめる必要がありました。プログラムが起動したときは、即座にプログラムの全体がメモリへ読み込まれました。

いくつかのオブジェクト指向のプログラミング環境ではこの制約を解消し、一つの実行プログラムの色々な部品を、異なったファイルで保持できるようにしました。プログラムは、必要としている部分だけで起動できます。どの部品も動的に読み込ないべきかを決定づけます。(*dynamically loaded)

起動時には、大きなプログラムのコアだけが、ロードされればいいのです。そのほかのモジュールは、ユーザがサービスを求めるたびに付け加えられます。ユーザが求めていないモジュールは、システムに何のメモリにも要求しません。

ダイナミックローディングは興味深い可能性を引き上げます。たとえば、プログラムの全部を一度に開発しなくても良いのです。ソフトウエアを部品として作っていくことができ、その部品一つずつをアップデートしていけます。1つのプログラムを1つのインターフェイスの基に集められた、いくつかのツールのかたまりと見なすことができ、ユーザが欲しいツールを呼び出すだけです。プログラムは、同じ仕事をする1式の代替ツールで提供することさえできます。その後、組の中から一つのツールを選び、読み込みます。

おそらくダイナミックローディングから得られる現状で最も重要な恩恵は、アプリケーションの拡張性でしょう。何かを加えることができ、設計しておいたプログラムをカスタマイズできるようにします。すべてのプログラムに必要なのは、誰かが書き入れられるフレームワークを提供し、ランタイムに、彼らが実装した部品を探し、動的に読む込むことです。

ダイナミックローディングが向き合う主要な挑戦は、すでに走っている部分と一緒に仕事をする、新しくロードされた部分を得ることです。特にその新しい部分が、他の人が書いた別な部品である場合にです。しかし、この問題のほとんどは、オブジェクト指向の環境ではなくなりました。というのはコードが、実装部分とインターフェイスに明確に分かれて論理的なモジュールの中で管理されているからです。クラスが動的に読み込まれると、新しく読み込まれたコードのどの部分も、すでに存在するコードと衝突することはありません。どのクラスも実装をカプセル化しており、独立した名前空間を持ちます。

加えて、ダイナミックタイピングとダイナミックバインディングにより、他の人が設計したクラスを、あなたが設計したプログラムへ苦労せずに適合できます。一度クラスが動的に読み込まれれば、ほかのクラスと何ら変わりなく扱われます。あなたのコードは、他の人が作ったオブジェクトへメッセージを送れますし、同じようにして彼らのもあなたのへ送れます。あなたと他の人との両方は、相手が実装したクラスのことを知る必要はありません。必要なのは、通信プロトコルを申し合わせるだけです。

ローディングとリンキング。日常的に使われる用語ですが、ダイナミックローディングは、ダイナミックリンキングと呼ばれるものとよく似ています。プログラムのさまざまな部分が集められてプログラムはリンクされ、結果としてそれらは一緒に仕事ができるようになります。起動時にそれらは揮発性メモリに読み込まれてロードされます。リンキングは普通ローディングより先に起こります。ダイナミックローディングは、新しいものを読み込むのか、プログラムの追加的な部品を読み込むのかを別のプロセスと見なし、既に走っている部品へ動的にリンクします。(*Loading and linking)

Referred from

Object-Oriented Programming with Objective-C, The_Object_Model

 

Copyright

Copyright © 201 Apple Inc. All Rights Reserved. Terms of Use | Privacy Policy | Updated: 2010-11-15