オブジェクト指向パラダイムについての考察

上で述べたように、オブジェクト指向パラダイムは、あくまでも「オブジェクト 指向が現実世界をそのままソフトウェアに表現する技術である」 わけではない(!)ということを理解しないと、ちゃんとした理解は できないと考えます。もちろん似ている点はあるのですが、あくまでも別物と 考えたほうがすっきりするでしょう。

一般的入門書のアンチテーゼのようになってしまいますが、その違いについて 考察します。

現実との比較による説明

クラスは種類、インスタンスは具体的なもの

オブジェクト指向の一番ベースになる仕組みは「クラス」で、これにたいして 「インスタンス」という言葉もあり、これらはペアで概念化されてます。「ク ラス」はもの=オブジェクトを分類したときの同じものの集まり(種類) 、といった意味で、「インスタンス」はその具体例(具体的なもの) 、という意味です。集合とその要素、という捉え方で間違いはないでしょ う。 これは現実世界でも普通にあることで、「人 (human being)」の具体例として の個人、「国」の具体例としての日本、アメリカ等です。

しかし、プログラミングにおけるクラス・インスタンスと現実世界との決定的 な違いは、プログラミングではクラス・インスタンスはプログラマが自分で設 計して作り上げるものにたいして、現実世界ではもともと「もの」があ り、それを分類・整理したものがクラスであり、その中に分類されるもの がインスタンスです。つまり見方が逆なわけです。

さらに、プログラミングではクラスからインスタンスが 作られるので、インスタンスはずっと同じクラスに 分類されますが、現実には少年がおじいさんになったり するわけで分類は固定化されていません。

これは実は継承についても同じことで、時間・空間的変化により、引き継げて いた性質が引き継げなくなるというのもよくあることです。

多態性はメッセージの送り方の共通化

多態性は「類似したクラスに対するメッセージの送り方(メソッドの呼び出し 方)を共通にする仕組み」なわけで、よく「泣く(鳴く)」にたいして子供も 動物も鳥もなくのでとりあえず「なく」としておいて、呼び出し側は「なく」 とだけしておけば、後でそのメソッドをいかようにでも実装できる、のような 説明がされます。

しかし、現実世界では子供も動物もは自分で(自律的に?)「なく」=自 由意志で行動するのにたいして、プログラムの中のオブジェクトはメソッ ド呼び出し=外部からの指示がないかぎり自分から動くことはなく、 また事前にコーディングされた=決められたことをその通りにしか実行で きない、という決定的違いがあります。

現実世界をそのままプログラムとして表現しない!

ということで、結局「オブジェクト指向が現実世界をそのままソフトウェアに 表現する」というのは実際は全くなりたっていないわけで、現実世界との対比 は、あくまでも例え話として割り切る、というのが大切だと思っています。

プログラミング手法としてのオブジェクト指向パラダイム

ということで、結局オブジェクト指向をプログラミング 手法としてとらえると そなわってる、というところに戻って考えるほうが、結局 うまく理解できると思ってます。ここではもう少し 細かくみていきます。

クラスの仕組み

端的にいうとクラスの仕組みは、「カプセル化」という言葉でいわれることが 多いように、以下のように整理できると思います。 それぞれの項目について、少し詳しくみていきます。

まとめる

たとえばファイル処理を考えてみます。VBAでのファイル入出力は、たとえ ば以下のようになります。 Dim n As Long, buf As String n = FreeFile ' おまじない Open "C:\tmp\test.txt" For Output As #n ' ファイルを開ける Print #n, Now ' 今の時間を書き込む Close #n ' ファイルを閉じる Open "C:\tmp\test.txt" For Input As #n ' 再度ファイルを開ける Line Input #n, buf ' 一行読み込む Close #n

上のコードを含むエクセルブック です(ただし データファイルはI:\dummyfile.txt に変更してあります)

ここでは、nがアクセスするファイルを番号として覚えておく変数、Open がファ イルを開けるサブルーチン(VB用語ではプロシージャ)、Print で1行書き 出し、"Line Input" (2語)で1行読み込み、Close でファイルを閉じる、と いう処理を行います。これらは基本的に一連の作業なわけですが、処理の記述 という意味ではばらばらです。そこで、これを (Java を例にして)「まとめる」 とどうなるかを考えます。

「まとめる」とは、その処理で使う変数と、サブルーチンをまとめるというこ とです。ここでの変数とは構造化言語ではグローバル変数に対応する、プログ ラム内ですっと保持しておきたいものを意味します。クラスにするには、まず 最初にクラス名を宣言し、利用するサブルーチンと変数をまとめて記述します。 たとえば以下のようになるでしょう。ちなみに、オブジェクト指向では、クラ スの中につくる変数を「インスタンス変数」、クラスの中につくるサ ブルーチンを「メソッド」と呼びます。

class FileIO { int n; void open(...) { // 引数は略 // ロジックは略 } String read() { // Line Input に対応 // ロジックは略、引数ではなく返り値で文字列を返す } void write(...) { // Print に対応、引数は略 // ロジックは略 } void close() { // // ロジックは略 } } とりあえずただまとめただけとも言えますが、まとめて整理したということ自 体に意味があります。これくらいのサンプルだとありがたみは正直感じにくい かもしれませんが、業務アプリケーションの開発で数十万ステップとかになる と、適宜まとめて扱うべき部品(この場合はクラス)の数をへらすというのは 見通しをよくする意味でも非常に重要です。

まとめることで、部品数を減らすだけではなく、名前もシンプルにつけかえる ことも出来、もっと言えば名前をつけること自体が楽になります。クラスが違 えば同じメソッド名でも混乱することはないので、操作をそのままあらわす名 前をつければいいわけです。さらに、適切な名前をつけることであとで探しや すい=再利用しやすいということにもつながります。いくら良いサブルーチン (メソッド)があってもあとで見つけ辛いのでは再利用されるチャンスも少な いわけです。

つまり、「まとめる」ことで

ということが言えるとおもいます。

隠す

上の例の場合、n は open/read/write/close メソッドが使うのであって、この クラスの外から見える必要はありませんし、もっともっというと見えない=操 作できないほうが(知らない間にに値がかわる等の)バグがはいりにくいわけ です。さらに、もしメソッドにバグがあってもその影響範囲を限定することが できますし、n を long とか double に変えたくなったときも影響範囲はこの クラス内に限定されます。 このようにオブジェクト指向ではインスタンス変数にアクセスできる範囲をそ のクラス内だけに限定する機能があります。この場合は以下のようになりま す。 class FileIO { private int n; void open(...) { // 引数は略 // ロジックは略 } String read() { // Line Input に対応 // ロジックは略、引数ではなく返り値で文字列を返す } void write(...) { // Print に対応、引数は略 // ロジックは略 } void close() { // // ロジックは略 } } 変数の宣言に private とつけることで、この変数はこのクラスの外からは アクセスできないことになります。

逆に、変数やメソッドを隠すだけでなく公開することを宣言することもできま す。上のメソッドはこのクラスを利用するコードで使えないといけないわけな ので、当然公開しないといけません。この場合は以下のようになります。

public class FileIO { private int n; public void open(...) { // 引数は略 // ロジックは略 } public String read() { // Line Input に対応 // ロジックは略、引数ではなく返り値で文字列を返す } public void write(...) { // Print に対応、引数は略 // ロジックは略 } public void close() { // // ロジックは略 } } この機能により、プログラムのバグの元になるグローバル変数を使うことなく プログラミングすることができるようになるわけです。

複製を複数つくる

上のクラスを使うことで、ファイルを開いて、読み書きして、最後にファイル を閉じるための部品となります。ここで、対象となるファイルが 1つだけの場合はいいのですが、「ファイルを1つ開いたままで読みだし、 別のファイルを開いて処理結果を書き込む」のようなプログラムを 作る場合は同時に2つのファイルを開けておく必要があります。

構造化言語ではそれら用に2つの変数を確保して、それぞれにファイルの番号 を保存しておいて処理をすすめるのですが、オブジェクト指向の場合はもっと 考えやすい仕組みがあります。どうするかというと、そのクラスからインスタ ンスを実行時に2つつくれば、それぞれのインスタンス変数は別のものになり ますので、問題なく別のものとしてアクセスできるわけです。 コーディングとしてどうするかというと、

インスタンスを保持している変数名.メソッド名(適宜、引数)
という書き方で区別することが出来るわけです。たとえば上のクラス宣言がす でにあるとして、以下のようなコードをメインルーチンで書くことになります。 FileIO f1 = new FileIO(); FileIO f2 = new FileIO(); f1.open("source.txt"); f2.open("result.txt"); String s; while (! f1.eof()) { s = f1.read(); // 適当な処理 f2.write(s); } f1.close(); f2.close(); のようになります。この「インスタンスを指定して、メソッドを呼び出す」や り方は、「お店」に「注文する」のようなたとえ話に近いものですが、繰り返 しですがこれはあくまでもたとえだと考えたほうがいいでしょう。

このように、インスタンスの複製を複数つくる仕組みにより、メソッド自体の コードは単純になり、複数の存在の可能性も意識する必要はありません。これ がもし普通の構造化言語だと、変数を複数用意したり、サブルーチンを呼び出 すのに切り分けをしないといけないし、と複雑になってしまうわけです。

特に GUI アプリケーションで顕著ですが、ボタンをいくつかつくって、テキス トボックスもいっぱいわりあてて、などと複数の部品を組みあわせて全体を構 成するというプログラミングを行います。オブジェクト指向の手法を用いれば、 上の仕組みのより問題なくプログラミングをすすめることが出来ます。

以上、一旦クラスを定義すると、そのインスタンスをいくつもつくることがで き、ファイル、文字列など同種のデータを同時並行的に扱う処理も、クラス内 のロジックを単純化することができるわけです。以下の図も参考にしてください。

Object Oriented Programming Structure

変数の考え方を整理する

ここで、構造化言語でのローカル変数、グローバル変数と、 オブジェクト指向におけるインスタンス変数について 整理することで、オブジェクト指向の有用性を再確認してみます。
ローカルグローバルインスタンス
複数ルーチンからのアクセス ×
有効範囲の限定 ×
存在期間 ×
変数の複製 ××
以上のことより、「インスタンス変数」は「すぐには消えないローカル変数」 とも言えますし、「同じクラスのあいだだけのグローバル変数」とも言えると 思います。この柔軟性、言い方をかえると「いいとこどり」なことによりオブ ジェクト指向がプログラミングにとって非常に有用なものとなっています。

多態性の仕組み

言葉の意味としては(多くの)いろいろな形態に変化する、といった意味で、 ポリモーフィズムとカタカナ書きされることも多いものです。正直な ところ、オブジェクト指向の文脈以外で見聞きすることは少ない=とっつきに くいものだと思います。 言葉がとっつきにくいだけでなく、この仕組み自体が分かり辛く、例え話にし てもいまいちな感じをもってます。メソッドを呼び出す側は相手を意識しない ということなのですが、「なけ」と言えば犬なら「ワン」、猫なら「ニャー」、 人(赤ん坊)なら「オンギャー」となく、という例え話がありますが、上で述 べたように実際の世界とはかなり違いがあります。なので、なんとなく分かっ たような、でもいまいちしっくりこないという印象をもつ人が多いようです。

多態性を仕組み=プログラミング技術論として説明すると、 「共通」メインルーチンを作る仕組みといえるでしょう。 再利用可能なサブルーチンをまとめて「共通」サブルーチン=ライブラリとし て呼び出される側のロジックをまとめたわけですが、多態性は逆 に呼び出す側のロジックを一本化する、と捉えられます。

端的にいうと共通のメインルーチンすなわちサブルーチンの呼び 出し方を共通化するという(だけの?)ことですが、これはプログラミン グの方法論として非常に大切だと思います。逆に考えて、構造化言語の時代ま ではこのようなものはなかったわけで、「サブルーチン」というものを考えた ことに並び称されるだけの価値があります。

コード例をみてみます。上で FileIO をつかって「クラス」の説明をしていま すが、それにさらに付け加えます。今度はファイルではなくネットワーク越し にデータを読み書きするクラス NetIO を考えると、次のようになると思います。

public class NetIO { // コンストラクタ public NetIO(...) { // 引数は略 (IP addr, port, ...?) // ロジックは略 } public void open() { // // ロジックは略 } public String read() { // Line Input に対応 // ロジックは略、引数ではなく返り値で文字列を返す } public void write(...) { // Print に対応、引数は略 // ロジックは略 } public void close() { // // ロジックは略 } } ここでコンストラクタとは、インスタンスの作成時に呼び出されるメソッドで クラス名と同じになります。初期化等をするためのものです。さて、このクラ スとメソッドを統一するために、前述の FileIO もすこしだけ変えておきます。 public class FileIO { private int n; // コンストラクタ public FileIO(...) { // 引数は略 (file name, mode, ...?) // ロジックは略 } public void open(...) { // 引数は略 // ロジックは略 } public String read() { // Line Input に対応 // ロジックは略、引数ではなく返り値で文字列を返す } public void write(...) { // Print に対応、引数は略 // ロジックは略 } public void close() { // // ロジックは略 } } ここで、これらのクラスを使いたい=メインルーチン側では ファイルだとかネットワークだとかを意識しないで 使いたいわけですので、とりあえずそれを DataIO ということにしてみます。 public class DataIO { // コンストラクタ public DataIO(...) { // 引数、ロジックは略 } public void open(...) { // 引数、ロジックは略 } public String read() { // ロジックは略 } public void write(...) { // 引数、ロジックは略 } public void close() { // ロジックは略 } } そして、FileIO と NetIO のクラスはこの DataIO で決められたメソッドと同 じように呼び出せるものにします(インターフェースを一致させる、のような 言い方をすることがあります)。この場合は以下のように、 extends というキーワードを使って、後述する「継承」 することを宣言しており、メソッドのインターフェースを一致させます。 public class FileIO extends DataIO { // 中身は上と一緒 } public class NetIO extends DataIO { // 中身は上と一緒 } この場合 DataIO というクラスは直接は使わないわけで、DataIO 自体にメソッ ドを実装する必要は特にありません。ただし、実装を一切しないとき(このよ うなクラスを抽象クラスといいます)は宣言の仕方(キーワード)が微妙にかわ ります。以下を参照してください。 interface DataIO { public void open(...); public String read(); { // Line Input に対応 public void write(...); public void close(); } public class FileIO implements DataIO { // 中身は上と一緒 } public class NetIO implements DataIO { // 中身は上と一緒 } 抽象クラスを宣言するときには class ではなく interface を用い、 それを継承して実装するクラスは extends ではなく implements を用います。

もとに戻ると、ネットにせよファイルにせよ、どこかからデータを読み込んで 処理するプログラムは以下のように書けます。

int getLineCount (DataIO r) { int count = 0; String s; s = r.read(); return s.length(); } この例では1行読んでその文字数を返しています。この場合、r はネットから かファイルからかを意識する必要はないですしどちらでも引数としてわたせま す。さらに、他の入力方法(キーボードから?)を追加する場合も、KeyIO を implements DataIO とすれば、上の getLineCount は全く変更する必要がない わけです。

継承の仕組み

前の説明では、似たクラスの共通点と相違点を整理する仕組みで、全体集合と 部分集合に相当する、ということだってわけですが、オブジェクト指向のクラ スがそもそも分類というよりインスタンスのひな型という枠組みなわけで、プ ログラミングとして継承がどういうふうに捉えられるかという観点で説明を試 みます。

継承を単純に説明すると「変数とメソッドをまとめた共通クラスを作って、そ の定義全体を別のクラスが再利用できる仕組み」と言えます。サブルーチンで プログラムを構成する構造化言語では共通サブルーチン=ライブラリをつくっ てルーチンをまとめました。これと同様にクラスでプログラムを構成するオブ ジェクト指向の環境では変数とメソッドをまとめた共通クラスを作れるという ことです。多態性でメインルーチンを一本化するだけでなく、似たクラスの共 通部分もまとめてしまおうという合理的な機能です。

継承を使うと共通に使いたいメソッドとインスタンス変数を共通クラスとして 定義し、利用したいクラスはその共通クラスを「継承」することを宣言します。 これにより共通クラスの定義内容がそのまま使えます。ここで、共通クラスの ことを「スーパークラス」あるいは親クラス、利用するクラスのことを「サブ クラス」あるいは子クラスと呼びます。

このように親子関係になぞらえて、さらに継承というのは英語では inheritance = 相続という意味もあり、親子関係が例え話としてよく使われる わけですが、実際は前述のとおりそのままというわけではなく、親子では親の 一部の形質、資産などが継承されるのに対してオブジェクト指向では原則10 0%引き継がれますし、親の遺産は親がいなくなって(なくなって)発生する わけですが親クラスの性質はなくなるわけではありません。
以上、繰り返しですが、親子とかの言葉には惑わされず、「継承はクラスの共 通部分を別クラスにまとめることでコードの重複を排除する仕組み」というこ とを再確認してください。 ちなみに、継承を宣言することは前述の多態性の利用の宣言ということにもな ります。この場合、継承を宣言したサブクラスはスーパークラスのインスタン ス変数とメソッドの定義情報をそのまま使えることの表裏で、メソッドの呼び 出し方もスーパークラスに合わせなくてはいけない、ということは覚えておか なくてはいけません。

以下に、継承と多態性を利用した例を示します。ファイル操作は例外(後述の 予定)が発生するのでコードがちょっと複雑になってますが、雰囲気を掴んで ください。

import java.io.*; interface DataIO { public void open(String s); public String read(); public void close(); } class FileIO implements DataIO { BufferedReader b; public FileIO (String s) { open(s); } public void open(String s) { try { b = new BufferedReader(new FileReader(s)); } catch(IOException e) { System.err.println("Error"); System.exit(1); } } public String read() { try { return b.readLine(); } catch(IOException e) { System.err.println("Error"); System.exit(1); } return null; } public void close() { try { b.close(); } catch(IOException e) { System.err.println("Error"); System.exit(1); } } } public class Sample01 { public static void main(String args[]) { FileIO a = new FileIO(args.length>0 ? args[0] : "Sample01.java"); String s; int i=1; while((s = a.read()) != null) { s = i + ":" + s; System.out.println(s); i++; } } }

型チェック、構文についての補足

プログラミングする立場で考えると、クラスも、構造化言語以前からあるデー タ型の発展形、とみることができます。前に説明したように、データ型にはメ モリの有効活用とバグのコンパイル時でのチェック、という2つのメリットが あったわけですが、オブジェクト指向ではこの型チェックの仕組みが推し進め られ、プログラマが定義したクラスを型として指定できるようになったという ことです。上の例だと FileIO f = 1; f.open(); f.close(1); などは全てエラーになります。機械語(アセンブリ言語)の頃はこのような チェックの仕組みはほとんどなかったわけですが、高級言語・構造化言語では 言語処理系が用意した型について使い方をチェックする仕組みがあり、引数の 型および個数についてもチェックができるようになりました。さらに、プログ ラミング言語C等で構造体というユーザ定義のデータ型を導入することでチェッ クの範囲がひろがり、オブジェクトにつながるわけです。このようにコンパイ ラが、言うなれば「型にあてはめて」プログラムのロジックチェックをするこ とができるようになり、プログラマの負担を軽減してくれるようになったわけ です。

それと並行して、プログラマの間違いを防ぐという意味で、言語の仕様にも変 化があります。たとえば C,C++ にはあり、Java では省かれた機能としては、 昔ながらの GOTO による制御、共用体、グローバル変数などがあります。これ らは機能の縮小という意味では「言語の退化」と言えるかもしれませんが、プ ログラムを難読化したり間違えやすくする機能は最初から提供しないほうがプ ログラム品質の向上に寄与できるということだと思います。つまり、プログラ ミング言語は進化するにつれて機能を増やすだけでなく不要な機能を使えなく する方向にも進化するということです。

まとめ

3大要素である、クラス、多態性、継承について表として整理すると、 以下のようになると思います。
クラス多態性継承
説明 サブルーチンと変数をまとめてプログラムの部品を作る メソッドを呼び出す方法を共通化する 重複するクラス定義を共通化する
目的 部品の整理 無駄を省く 無駄を省く
覚え方 まとめて隠してたくさん作る仕組み 共通メインルーチンを作る仕組み クラスの共通部分を別クラスにまとめる仕組み

講義用スタイル
印刷用スタイル