Help us understand the problem. What is going on with this article?

オブジェクト指向が0.05%も理解できない記事

More than 1 year has passed since last update.

尽く書を信ずれば即ち書無きに如かず
《孟子『尽心下』より》

イントロダクション

「最も理想的なオブジェクト指向を実現しているプログラミング言語は何か?」と問われたとき、君は何と答えるだろうか?

  • C++、Java、C#。君がそうだと思っているのは表面だけで、たぶん何もわかっていないのだろう。無知であることを知っているのであれば、無知のまま過ごした方が幸せなときもある。
  • Simula、Smalltalk、Ruby。君は本質をいくらか知っているようだから、引き返すなら今のうちだろう。深淵を覗けば、君もまた怪物にならざるを得ない。
  • JavaScript、Python、Go。君が真剣にそう答えるなら、私とは異なる真理に辿り着けたのだろう。君と私のどちらかが正しいのではない、どちらも常に正しく、どちらも常に間違っている。
  • Erlang、Elixir。君は既に答えを知っているようだから、この記事は全く以て無意味だろう。賛同してくれる人が周りにいなくても嘆くことはない、世界が狂っているのか自分が狂っているのかを見分ける手段など存在しないのだから。

この記事を読んでも、正解には辿り着かないだろう。当たり前だ、初めから正解など無いのだから。ただ、時には解のない問いを永遠に求めるのもいいのではないだろうか?そのような問いを掛けることによって、私達はその問題が停止しないことを判断できるのだから、チューリングマシンを超えた存在であることを証明してくれる。

オブジェクト指向の始まり

オブジェクトとは何か?

英単語 object を日本語に訳すとき、いくつかの表現が存在する。「物体」「対象」「目的」等であるが、日本語としてはそれぞれ異なった意味である。しかし、英語ではいずれも object という表現が使われる。英語圏の人にとっては、これら日本語圏の人には複数の意味に取られるような言葉が何らかの共通した一つの意味を内包すると感じられるからこそ、たった一つの言葉で共通して表現できるのだろう。だからこそ、プログラミングにおけるオブジェクト(object)という言葉も、英語圏の人は違和感無く受け入れることができる。だが、私達のような日本語に毒されてしまった人達は、異なる複数の意味に惑わされ、オブジェクト(object)という言葉が持つ本質を見抜けないでいる。そして、それこそがオブジェクトへの無理解と誤謬へ繋がっているのではないかと、私は思うのである。

本題に入ろう。まず、オブジェクト(object)を正しく理解せずに、オブジェクト指向を語ることは難しい。私達には理解できない英単語の object のことは頭の片隅に追いやって、オブジェクトとは何かと言うことをはっきりさせなければならない。

「プログラミングにおけるオブジェクトとは処理の直接的な対象となる具体的なデータである。」

オブジェクト自体はオブジェクト指向から独立した概念である。むしろ、オブジェクトという言葉が先にあって、そこからオブジェクト指向という言葉が生まれたと言っても良い。決して逆ではない。オブジェクトは、処理、つまり、計算や評価が直接行われる対象のモノであり、その実態は、なんらかの具体的な数値、具体的な文字列、具体的なリストや配列、または、それらを複合的にまとめて構造化された具体的なデータの集合体(複合体)である。別の言い方をすれば、プログラミングにおいて、変数を束縛し、また、様々な式の評価によって得られ、演算子や関数などの対象になるモノ(値やデータ)である。つまりそれは、object がもつ複数の意味、「物体」「対象」「目的」等を有する何かであり、だからこそオブジェクトと言われるのである。

ここで勘違いをしてはいけないことがある。一部の言語をのぞけば「数値」や「文字列」はオブジェクトではない。オブジェクトであるのは、1 という具体的な数値、"hoge" という具体的な文字列であり、「数値」や「文字列」と言った抽象的な概念ではない。そして、これら「数値」や「文字列」は型(type)と言われるものである。だが、しばしば私達は「具体的な」という表現を省略して、数値や文字列をオブジェクトと言ってしまっている。この二つを決して混同してはいけない。1

もう少し補足しよう。変数そのものはオブジェクトではない。変数を評価することで得られるなんらかの値(データ)こそがオブジェクトである。リテラルもまたオブジェクトではない。リテラルを評価することで得られる何らかの値(データ)こそがオブジェクトである。何らかの値を返す式が評価されるときに得られるモノは、まさしく、オブジェクトであるが、式自体がオブジェクトでは無いと言うことと同じだ。評価とはオブジェクトを得るための手段であり、そのオブジェクトがまた、式の一部となって評価される。オブジェクトは常に具体的な何らかの値(データ)であり、オブジェクトを処理することがプログラムの本質である。

データ構造を知らなければ、処理はできない

オブジェクトに何らかの処理をすることがプログラムとなるのだが、さて、その処理とはどうやって行うのだろうか?それにはまず、オブジェクト自体がどのようなデータ構造であるのかを知らなければならない。

例として、直交座標系の点を表すデータを考える。単純なx軸とy軸があり、それぞれの軸における数値の精度は倍精度浮動小数点数相当であるとする。Cであれば、次のような単純な構造体になるだろう。

C
struct point {
    double x;
    double y;
};

この構造体のデータに対して、二つの点の距離を求める関数distanceを考える。

C
double distance(const struct point *a, const struct point *b)
{
    return hypot(a->x - b->x, a->y - b->y);
}

はたして、この実装は常に正しく動くのだろうか?直交座標系の点を表す場合、単純にx軸とy軸の数値とすることが唯一の方法ではない。複素平面とみなして一つの複素数とすることもできるし、極座標系として動径と偏角から構成されるとすることもできる。

C
struct point {
    double complex z;
};
double distance(const struct point *a, const struct point *b)
{
    return cabs(a->z - b->z);
}
C
struct point {
    double r;
    double theta;
};
double distance(const struct point *a, const struct point *b)
{
    return sqrt(pow(a->r, 2) + pow(b->r, 2) -
            2 * a->r * b->r * cos(a->theta - b->theta));
}

つまりだ。点がどのようなデータ構造を持っているかによって、その処理は全く変わってくることになる。データ構造を知らなければ、いろいろな処理ができないと言っても良い。そしてそれは、最初に点のオブジェクトを作成する時点から知らなければならない。

C
struct point p = {2.0, 1.0};
struct point p = {2.0 + 1.0 * I};
struct point p = {sqrt(5.0), atan(0.5)};

どれが正しいかを知るには、 point 構造体の定義を見るしかない。これが、オブジェクト指向ではないプログラムの欠点であった。

中身を知らなくても、外見がわかれば良い

データ構造を知る必要があると言うことは、それなりに負担であった。また、データ構造に依存していると言うことは、データ構造を後から簡単に変えられないという問題もあった。そこで、データ構造がどのようなものであるのかを知らなくても外部から操作できる方法を考える必要が出てきた。

その解決策の一つがオブジェクト指向(object-oriented)2。データ構造がどのようなものであるかはオブジェクト自体が知っていればいい。そして、オブジェクト自体に、どのようなモノが見せられるかを任せてしまえば良いと言うことになる。C++ではどのようになるのか見てみよう。

C++
class Point {
    private:
    ... // 内包するデータが定義されている。
    // xy座標かも知れないし、複素数かも知れないし、極座標かも知れない。
    public:
    point(double x, double y); // xy座標で初期化する。
    double getX() const; // x座標を取得する。
    void setX(double x); // x座標を変更する。
    double getY() const; // y座標を取得する。
    void setY(double y); // y座標を変更する。
    complex<double> getZ() const; // 複素数座標を取得する。
    double getR() const; // 動径を取得する。
    double getTheta() const; // 偏角を取得する。
};

このような宣言であれば、データ構造がどのような物であっても関数distanceは次のように書けるだろう。

C++
double distance(const Point &a, const Point &b)
{
    return abs(a.getZ() - b.getZ());
}

さらに言うなら、distance 自体も Point に任せてしまって良いかもしれない。

Point を扱うプログラムにとって知っておかなければならないことは、Point がどのような情報を提供できるのか、また、どのような処理ができるのかという外見だけである。Point 自体がどのようなデータ構造になっているのかは知る必要は無い。もしかしたら後で中身のデータ構造が変わるかも知れないが、外見に変更がなければ、外から扱う側は何も変更する必要がない。このように、オブジェクト指向とは、中身と外見を切り離すこと、つまりは関心の分離だ。

オブジェクト指向の要素

メソッドとメッセージ

先の章で、私達はデータ構造を知らないままオブジェクトを処理できるようになる仕組みを見た。つまり、外見だけを知っていればいいと言うこと、中身は知らなくてもいいと言うことをみてきた。では、この外見は何かと言うことである。

この外見こそがメソッドである。オブジェクトがどのようなメソッドを持つのか、言い換えれば、どのようなメソッドを受け付けて、どのような物を返すのかさえわかれば、それで十分である。そして、オブジェクトがもつメソッドを呼び出すことを、単純にメソッド呼び出しと呼んでいる。

では、メソッド呼び出しとは何であろうか?いくつかの差異はあるが、多くの言語において、メソッド呼び出しは次のような形で書かれる。

レシーバー.メソッド名(引数)

レシーバーの部分がオブジェクトだ。実際は、レシーバー自体も式であり、その評価結果であるところのオブジェクトがレシーバーになる。このレシーバーに対して、引数3にあたるオブジェクトを添えて、メソッド名のメッセージを送ることがメソッド呼び出しの本質である。

そう、重要なのはメッセージだ。通常、関数の呼び出しは、どこに定義された関数であるかと言うことが確定してから呼び出している。対して、メソッド呼び出しは、メソッドがどこに定義されているのかは関心が無く、単にレシーバーたるオブジェクトにメッセージを送っているだけに過ぎない。それをどのように処理するのかは、メッセージを受け取ったオブジェクトに任されており、呼び出し側が指定する事ではない。

だからこそ、私達は、メソッドの中身を知る必要が無い。メソッドを正しく処理する責任はメッセージを受け取ったオブジェクトにあり、メソッドを使おうとしている側にはない。そもそも、オブジェクトの中身がわからないのだから、どのような処理をするのかがわかりようがないとも言える。

ここまでこればもうわかると思う。あるメッセージを受け取る事ができるオブジェクトが複数存在するのであれば、それらのオブジェクトを取り替えながら同じメソッド呼び出しができる。これが多態性と言われるものである。妥当なメッセージであるかをコンパイル時に確認する静的型付けでは、抽象クラスの継承やインターフェースによってどのようなメッセージを受け取れるのかを保証しているだけで、オブジェクトが入れ替わってもメソッド呼び出しの仕方が変わると言うことはない。妥当なメッセージであることを静的に保証する必要が無い動的型付けでは、どんなオブジェクトであっても取りあえずメッセージは送ることはでき(ただし、受け取り拒否の現れとして例外が発生するかも知れないが)、それが、ダックタイピングという手段を生み、柔軟な多態性を実現している。

もうひとつ、先程から何度も言っていることだが、、私達から見えるのは外見だけで十分という話だ。これはカプセル化を意味する。オブジェクトを使う側から見え場、オブジェクトの中身がどのようになっているかについては関心はなく、完全に隠蔽されていても構わない。だからこそ、カプセル化が実現できている。

つまりは、多態性やカプセル化は、メソッド呼び出しだけが存在するというメッセージの受け渡しによって副次的に実現できていると言っても過言ではない。

結局は、オブジェクト指向がオブジェクト指向であるところの所以の物は、このメソッド呼び出し、つまりは、メッセージの受け渡しによって、処理をしていくと言うことである。その他の要素は、メソッド呼び出しの特殊な形か、実現のための副次的な機能か、より高度に抽象化するための追加された機能に過ぎない。

プロパティとゲッター・セッター

通常、オブジェクトというのは何らかの属性、即ち、特性を持っていると考えられる。先ほどのPointではX座標を取得というメソッドを使っていたが、X座標や動径と言ったものは、オブジェクトの特性といっても言い。そう言った特性を取得したり、時には変更することができるべきだろうが、メソッドを使うにはいささか不格好である。

そこでもう一つの外見となるのがプロパティである。通常のメソッド呼び出しの形よりは、単にプロパティ名のみで取得し、=のような代入演算子で変更できる方が好まれる。しかし、プロパティは特殊な呼び出しの形ができるメソッドに過ぎない。C#を例にしてみてみよう。

C#
class Point {
    ...
    public double x { get {...} set {...} } // x座標のプロパティ
    public double y { get {...} set {...} } // y座標のプロパティ
    public Complex z { get {...} } // 複素数座標のプロパティ
    public double r { get {...} } // 動径のプロパティ
    public double theta { get {...} } // 偏角のプロパティ
}

C++の時と同じように、Point がどのようにして座標のデータを所有しているかは関心が無い。ただ、p.x のような形で書けばX座標が得られる、p.r のような形で書けば動径が得られると言うだけで十分である。さらに、set も定義しているプロパティについては、p.x = 1.0; と言う形で属性自体を変更することも可能になる。

もう一度言うが、これらはただの特殊なメソッド呼び出しに過ぎない。同じようにメッセージを送っているだけであり、それをどのように処理するのかはオブジェクトに任される。4

しかし、任意の処理を定義できるプロパティは全ての言語に備わっているわけではない。C++やJavaのように任意の処理が定義できるプロパティが無い言語では、先の章でのC++の例のようにメソッドとして実装する必要がある。慣習として getX()setX() のようなメソッドを用意する事が多く、これらをゲッター・セッターと呼んでいる。ゲッター・セッターは任意の処理を定義できるプロパティが存在しない言語での代替手段に過ぎない。

オブジェクト指向の実現

クラスとプロトタイプ

オブジェクトがどのようなメソッドを持つのかというのが関心事になるが、では、どうやってオブジェクトにメソッドを持たせるのだろうか。オブジェクト一つ一つにメソッドを持たせるのはあまり良い方法とは思えない5。同じ動作になるオブジェクトをひとまとめに扱って、統一した動作を定義した方が良いだろう。それを実現するのがクラスプロトタイプであり、また、同じ動作をするオブジェクトを同じを持つという。

クラスとプロトタイプの違いはオブジェクトがどのようにして自分がもつメソッドを探しに行くかという手段の違いに過ぎない。そして、あるオブジェクトがクラスやプロトタイプに属する場合、そのクラスやプロトタイプのインスタンスと呼ぶ6

ほとんどのオブジェクトはクラスやプロトタイプのコンストラクタを呼び出す形で作成される。自動的にクラスやプロトタイプで定義されているメソッドがそのオブジェクトにも使えるようになる。だからこそ、クラスやプロトタイプの定義が重要となってくるし、たいていは、クラスやプロトタイプの定義によって、ほとんどの動作が決定づけられる。しかし、言語によっては、オブジェクト独自のメソッドを定義できる場合がある。クラスやプロトタイプは共通の動作をさせるためのまとめであって、オブジェクト自体の動作全てではない。あくまでメッセージをどう扱うか決めるのはオブジェクトであって、クラスやプロトタイプではない。

注意して欲しいのは、クラスがオブジェクトであるとは限らないと言うことだ7。クラスはいわば型であり、最初に述べたとおり、「数値」や「文字列」といった類とと同じだ。ただ、同じように「具体的な」という表現を略して、ただクラス名だけを述べて、そのクラスのインスタンスであるところのオブジェクトを指し示している場合がある。この文章も同様であり、そこは混同しないでおいて欲しい。これまでのC++やC#の例もクラスを使っているが、これまでの話はクラス自体の処理を問題にしているわけではなく、クラスのインスタンスの話をしているのである。これは最初のCの構造体の話からそうなっている。

インスタンス変数

あるオブジェクトを処理するとき、私達の関心は外見であるメソッドのみにあったわけだが、そのオブジェクトの中身を作るとき、やはり、そこには何らかのデータ構造があり、何らかの値を所有していなくてはならない。そこで使われるのがインスタンス変数である。幾度も例に挙がった Point はどのようなデータ構造になっているかわからないとしたが、実際は何らかのデータをインスタンス変数として持っていることになる。そのインスタンス変数を利用して、X座標を返したり、動径を求めたりしているのだ。

オブジェクトがインスタンス変数をどのように管理するかは言語によって千差万別である。そのため、統一したことは言えない。ただ、インスタンス変数とプロパティは密接に関わってくる。たとえば、Point がX座標のインスタンス変数として持つのであれば、X座標のプロパティ取得はインスタンス変数をそのまま返せば良いし、プロパティ設定はインスタンス変数にそのまま代入すればいい事になる。もし、別の形であったら、例えば複素数であったら、X座標は複素数の実部を返す、変更は、複素数の実部を変えるという形になるだけである。

言語の中にはインスタンス変数が自動的にプロパティとなってしまうことを抑止することができない言語もある。アクセス権を設定することで他のクラスから(他のオブジェクトからではない)隠蔽できる言語もあるが、やはり、プロパティになってしまうことを止めることはできない。しかし、インスタンス変数とプロパティは概念的には厳密に区別すべきである。インスタンス変数は内部構造のための機能であり、プロパティは外部へ見せるための機能であるからだ。

拡張された構造体

ここまで来たとき、C++、および、その流れを組むJavaやC#等が説明に合っていないのではないかと感じた方も多いと思う。その感覚は間違っていない。なぜなら、C++はこれまでのことを実現するために構造体を拡張したに過ぎず、JavaやC#等はその流れを踏襲しているからだ。

C++はCの言語拡張であり、一部を除いてほぼ互換性を維持している。そのため、構造体を拡張する形でオブジェクト指向を実現しようとした。だからこそ、C++では structclass がデフォルトのアクセス権を除いて全く同じ物として扱えるのである。メソッドとしては関数自体をメンバーとすることにした。C++ではこれをメンバー関数と呼んでいる。メンバー関数は通常のメンバー変数のように変更したりできるわけではないのだが、そのアクセス形式はメンバー変数と同じになっている。そのため、メソッド呼び出しはメッセージの送信と言うよりも、変数の型に結びつけられた構造体へのメンバーである関数の呼び出しに過ぎない8

また、メンバー変数はプロパティであることを止めることはできない。そのため、アクセス修飾子で制限が必要になる場合がある。逆に、C++ではメンバー変数しかプロパティになることはできない。メソッドのような柔軟性を持たせられないため、ゲッター・セッターを使うしかなくなる場合も多い。Javaではメンバー変数ではなくフィールドという表現を使っているが、結局同じことになってしまう。

この構造体の拡張は一見うまくいっていたが、実際は弊害も多く存在している。しかし、C#では任意の処理が定義できるプロパティをサポートするなど、新しい言語ではそのような点を修復しようと試みており、そこそこ成功しているとも言える。だが、すでにメッセージのやりとりというオブジェクト指向から離れており、別の形のオブジェクト指向、つまり、新オブジェクト指向9と言ってもいい。

では、C++が再定義した新オブジェクト指向がオブジェクト指向として不十分であるかというと、そういうわけではない。今までの例はC++とC#であって、SmalltalkやRubyではない。それでも、メッセージのやりとりであるはずのオブジェクト指向を説明できている。メッセージのやり取りとほとんど同じことができてしまっていると言ってもいい。

オブジェクト指向の応用

クラスメソッド

これまで見てきた物はオブジェクトに対する処理であった。オブジェクトは何か具体的なデータであって、クラスはオブジェクトではないといったが、実際の処理では、なんらかの具体的なデータから独立した処理が必要な場合もある。その時使われるのがクラスメソッドである。それに対して、いままで見てきたクラスのインスタンスであるところのオブジェクトのメソッドはインスタンスメソッドと呼ばれる。

言語によっては型、つまりは、クラス自体をオブジェクトとして扱えるものがある。このような言語においてクラスメソッドは単純に、そのクラス自体のメソッドとなるため、今までと何も変わらない。クラスにとってのインスタンスメソッドと言っても良いだろう。

しかし、C++のようなクラス自体をオブジェクトとして扱えない場合もある。この場合でも、あたかもメソッドのように呼び出せるが、実質は、ただ名前空間がわかれた関数である。static キーワードを使う言語が多いため、staticメソッドとか、静的メソッドなどと言われるが、はっきり言うと、オブジェクト指向でも何でもない、ただの関数である。同じように扱えるように見えるだけにすぎない。

DRYのための継承

同じ処理を行うデータであれば同じクラスのインスタンスとすることができる。しかし、ほとんどが同じだが、一部だけ異なると言った場合はどうだろうか?そのばあい、ある型がとある型を拡張発展したものと考えることができる。

ほとんど一緒なのに別々にコードを書くことはあまり有意義ではない。同じコードは一回限りにした方が管理がしやすくなり、保守性が高まる。そこで考えられたのが継承だ。

Point の例で考えるとして、色つきの点を考えてみよう。ColoredPoint と言う物だ。ColoredPoint のX座標や動径については Point と何も変わっていない。ただ、色というプロパティが新たに追加されたに過ぎない。そこで、ColoredPointPoint を継承することで、Point に既に実装された処理を実装し直すことは不要になる、新たに追加された色の処理のみ記述すれば良くなる。いわゆるDRYの原則に従うことができると言うことだ。

継承といっても言語によっていろいろな違いがある。多重継承、単一継承など継承ができる方法の違いもある。また、Mix-inトレイトと言った、通常の継承とは異なる物もある。そして、DRYを実現するのは継承だけではなく、委譲という方法もある。しかし、たいていの場合は、何らかの方法でDRYが容易にできる仕組みを用意していることが多いだろう。

オブジェクト指向とは何か?

オブジェクト指向とは認識の限界である。
《raccy『オブジェクト指向とは何か?』より》

自己引用で済まないが、そういうことである。オブジェクト指向に関わる事柄を色々見てきたが、実際は言語間の違いが多すぎて「メソッド呼び出し」ぐらいしか共通するものが無い。もはや、オブジェクト指向は各言語でいろいろな解釈がされ、統一することなど不可能なのだ。

論理的な話より、より深くオブジェクト指向というものがどういう動作なのかを取り扱おうとすれば、結局の所、特定の言語の説明になってしまう。しかし、それでいいのだ。オブジェクト指向はこうあるべきと論じることこそが無意味であり、各言語に合わせたオブジェクト指向の作法に従った方が有意義なのだ。

では、なぜオブジェクト指向で考えるのが良いのか?となる。オブジェクト指向とはデータ構造から関心を分離すること、と最初の方で述べたが、それは確かに重要かも知れない。だが、私達が本当にしたかったことは、人間の思考に近づけたかっただけだったのではないかと思う。

いずれにせよ、オブジェクト指向とは私達の理解を助けるための手段の一つに過ぎないのだろう。


  1. 型(type)自体をオブジェクトとして扱える言語も存在する。それでも、具体的な数値と型としての「数値」は区別して考えなくてはならない。 

  2. オブジェクト指向を使わなくても解決する手段はある。実装をモジュールやライブラリなどに閉じ込め、APIとして必要な関数を提供し、関数を通してのみ操作するようにするという方法だ。Cの標準ライブラリで言えばFILEが良い例になる。FILEの中身がどのようになっているのかは使う側から全く見えないが、FILEを扱う各関数が用意されているため、私達がFILEをどのように処理したら良いのか困ることはない。 

  3. 引数は0個または複数ある場合もあり、また、それらもそれぞれが式である。そして、実際に渡されるのは評価結果であるところのオブジェクトになる。 

  4. Rubyでは本当にただのメソッドとして動作する。これは()を省略可能であるということ、 x.y = zx.y=(z) という y=() というメソッド呼び出しに過ぎないと解釈される構文による物である。 

  5. 「良い方法とは思えない」と言っているが、後述するプロトタイプはある一つのオブジェクトメソッドを持たせ、それをプロトタイプとする方法である。 

  6. JavaScriptではコンストラクタにあたる関数に対して prototype という特殊なプロパティのオブジェクトがプロトタイプとなるため、プロトタイプ自体ではなく、そのコンストラクタのインスタンスという表現をする。例えば、Array のインスタンスプロトタイプは Array.prototype であり、Array はコンストラクタに過ぎないのだが、new Array で作成されるオブジェクト(配列になる)は Array のインスタンスであるという表現をする。 

  7. プロトタイプはオブジェクトである。また、クラスがオブジェクトである言語もある。 

  8. もし、virtual 修飾子が付いているメンバー関数であれば、型ではなくオブジェクトそのもののクラスを見に行く事になるのだが。 

  9. 「新」と言ってもC++のリリースは1983年であり、オブジェクト指向の黎明期には誕生していたことになる。ただ、メッセージを重視し、1980年に公開されたSmalltalkに比べれば新しい。 

raccy
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away