不当だと思います。
オブジェクト指向批判の文脈では優れた回答がありますので別の視点から。
だいたい、どの言語もそうですけど、歴史の長い言語は間違ってつけちゃった機能があります。だいたい、いろんな人のいうことを聞いて、海鮮全部丼、トッピング全部のせみたいに出来上がってます。
私の偏見ですが、企画する人、規格を作る人と実装する人が別々だと、奇々怪界な仕様になります。とりあえずつけておけって機能も盛り込まれます。
JavaScriptのwithとか。PHPのトレイトとか。
仕事人として正しい態度は、本当にそれは必要かを良く考えて、自分のプロジェクト用にサブセットを作ることを心がけています。PHPで言えばインプリメントも自作のものでは使いません。
時々、高齢者にワードを使わせると、文字修飾を全部使い切った文書をつくりますがあれみたいです。
自分に与えられた問題を効果的に解決するツールセットがあれば、それで良いという考え方って結構大事です。
なんですけど、その方式を開陳する時には格段の注意が必要です。
例えば、私は仮想サーバーとfreebsdとpkg、bashを愛用していて、これで構成管理を全てやってます。ベンダーロックを避けるこのやり方だと、リソースの仕入れを2–30%安くすることができます。それはビジネスの成功にはとても大事な要素です。bashとpkgを手動で導入したらあとは、全てbashのスクリプトでサ
不当だと思います。
オブジェクト指向批判の文脈では優れた回答がありますので別の視点から。
だいたい、どの言語もそうですけど、歴史の長い言語は間違ってつけちゃった機能があります。だいたい、いろんな人のいうことを聞いて、海鮮全部丼、トッピング全部のせみたいに出来上がってます。
私の偏見ですが、企画する人、規格を作る人と実装する人が別々だと、奇々怪界な仕様になります。とりあえずつけておけって機能も盛り込まれます。
JavaScriptのwithとか。PHPのトレイトとか。
仕事人として正しい態度は、本当にそれは必要かを良く考えて、自分のプロジェクト用にサブセットを作ることを心がけています。PHPで言えばインプリメントも自作のものでは使いません。
時々、高齢者にワードを使わせると、文字修飾を全部使い切った文書をつくりますがあれみたいです。
自分に与えられた問題を効果的に解決するツールセットがあれば、それで良いという考え方って結構大事です。
なんですけど、その方式を開陳する時には格段の注意が必要です。
例えば、私は仮想サーバーとfreebsdとpkg、bashを愛用していて、これで構成管理を全てやってます。ベンダーロックを避けるこのやり方だと、リソースの仕入れを2–30%安くすることができます。それはビジネスの成功にはとても大事な要素です。bashとpkgを手動で導入したらあとは、全てbashのスクリプトでサーバーを立ち上げます。サーバーをゼロから作るのは半年に1回ですから、使い方が5年ぐらい変わらないのがいいんです。Bashもpkgも枯れ切っていて、安定し切ってます。もちろんメンテナンスも出過ぎた真似をしないから、変なトラブルに巻き込ませないし、慎重な配慮が常に行き届いています。仕入れが安いのと、常にOSのレベルで問題を自己解決できるというのは、私にとってはとても重要です。
これを一般的に採用されてるDockerやRubyベースの構成管理ツールをこき下ろし、DevOpsの文脈で語り出すと、きっとBashおじさんとか言われるんでしょう。
話を言語に戻すと、機能を限定して使うのはそんなに奇天烈なテクニックではないと思います。Old Plain Cとかいう人もいます。
部分継承やインプリメントを使わないようにしたり、静的関数で済ませられないかとかは考えます。
自分独自のやり方を人に紹介する時には、もっと下から丁寧に説明する方が良いと思っています。
もちろん正当な反論もたくさんあるとは思いますが、今見れば、明らかに、不当な反論もたくさんあります。
まあ、そんな時代もあったな、ということでしょうね。
端的に言うと、staticおじさん批判が全盛だったころには、インターフェイスに対するプログラムやカプセル化といった、実現したいこと(目的)と、そのための手段が、区別されずに話されていた傾向があると思います。
例えば、
- 「ポリモーフィズム」つまり、具象型ではなくて、インターフェイスに対してプログラ厶する、という目的と、その実現手段の一つにすぎない「継承」がゴッチャになって語られている。とくに、「継承」という手段は、つまり実装の再利用(≒コピペ)なので、デメリットも大きいですし。
- 「カプセル化」、つまり、状態変数の影響範囲を局在化するという目的と、その実現手段の一つにすぎない、システム全体の状態をオブジェクト(インスタンス)単位に分割して管理するという手法、がゴッチャになって語られている。状態変数をオブジェクト単位に分割する手法は、考えられる分割方法(オブジェクト構成)が一つしかないのであればうまくいきますが、行いたい複数の処理ごとに、最適な分割方法(オブジェクト構成)が異なる場合(処理Aのためには、こういうオブジェクト構成が望ましいけど、処理Bのためには、別のオブジェクト構成が望ましい、など)には、あまり、うまくいきません。下手すると、あっとい
もちろん正当な反論もたくさんあるとは思いますが、今見れば、明らかに、不当な反論もたくさんあります。
まあ、そんな時代もあったな、ということでしょうね。
端的に言うと、staticおじさん批判が全盛だったころには、インターフェイスに対するプログラムやカプセル化といった、実現したいこと(目的)と、そのための手段が、区別されずに話されていた傾向があると思います。
例えば、
- 「ポリモーフィズム」つまり、具象型ではなくて、インターフェイスに対してプログラ厶する、という目的と、その実現手段の一つにすぎない「継承」がゴッチャになって語られている。とくに、「継承」という手段は、つまり実装の再利用(≒コピペ)なので、デメリットも大きいですし。
- 「カプセル化」、つまり、状態変数の影響範囲を局在化するという目的と、その実現手段の一つにすぎない、システム全体の状態をオブジェクト(インスタンス)単位に分割して管理するという手法、がゴッチャになって語られている。状態変数をオブジェクト単位に分割する手法は、考えられる分割方法(オブジェクト構成)が一つしかないのであればうまくいきますが、行いたい複数の処理ごとに、最適な分割方法(オブジェクト構成)が異なる場合(処理Aのためには、こういうオブジェクト構成が望ましいけど、処理Bのためには、別のオブジェクト構成が望ましい、など)には、あまり、うまくいきません。下手すると、あっという間に、誰も理解できない「スパゲッティ・オブジェクト・プログラム」ができあがります。
今は、当時に比べて、インターフェイスに対するプログラムや、カプセル化(状態変数の影響範囲の局在化)などの目的を達成する手段は一つではなくて、いろいろな手段があること、さらには、それぞれの手段のメリット・デメリットの比較、なんかについて、世間の理解が進んできた、ということだと思っています。
staticおじさんについて悪いのはラベリングです。マスコミは名前をつけて、ステレオタイプな偏見イメージを作り出します。そういうアオリが喝采を受けた時代でした。
自分の経験で言えば、staticメソッドはthisを参照できず、機能が少ないのだから、staticメソッドでもインスタンスメソッドでもできるthisに依存しない処理なら、機能の少ないstaticメソッドを選択すべきだ
と言ったら「えー!static」みたいな反応を受けたこともあり、苦い思いも正直ありますね。価値観は文化圏の中で決まります。そして変わります。正直、自分もOOPおにいさんでした。バードランドメイヤー先生のオブジェクト指向入門に感銘を受け、無謀にもあの一冊だけでEiffel言語のサブセット実装に挑戦したものでした。多重継承時の仮想関数テーブルの配置方法を編みだせずに挫折しましたが。
思うこととして、関数型言語はOOPのある面を参考にしてもいるし、C++のテンプレートライブラリを参考にしたり、あるいは反面教師にもしているわけです。Haskellの型クラスはOOPの仮想テーブルを参考に発案されたとかですね。
なので私にとって両者は基本的に排他の関係にはありませんし、敵同士でもありません。良し悪しもなく特定の状況で特定の目的達成に役立つ度合いの違いだけです。
インスタンスメソッドは、オーバーライド定義しなければ第一引数にth
脚注
staticおじさんについて悪いのはラベリングです。マスコミは名前をつけて、ステレオタイプな偏見イメージを作り出します。そういうアオリが喝采を受けた時代でした。
自分の経験で言えば、staticメソッドはthisを参照できず、機能が少ないのだから、staticメソッドでもインスタンスメソッドでもできるthisに依存しない処理なら、機能の少ないstaticメソッドを選択すべきだ
と言ったら「えー!static」みたいな反応を受けたこともあり、苦い思いも正直ありますね。価値観は文化圏の中で決まります。そして変わります。正直、自分もOOPおにいさんでした。バードランドメイヤー先生のオブジェクト指向入門に感銘を受け、無謀にもあの一冊だけでEiffel言語のサブセット実装に挑戦したものでした。多重継承時の仮想関数テーブルの配置方法を編みだせずに挫折しましたが。
思うこととして、関数型言語はOOPのある面を参考にしてもいるし、C++のテンプレートライブラリを参考にしたり、あるいは反面教師にもしているわけです。Haskellの型クラスはOOPの仮想テーブルを参考に発案されたとかですね。
なので私にとって両者は基本的に排他の関係にはありませんし、敵同士でもありません。良し悪しもなく特定の状況で特定の目的達成に役立つ度合いの違いだけです。
インスタンスメソッドは、オーバーライド定義しなければ第一引数にthisを取るstaticメソッドのシンタックスシュガーだし、クラス定義に縛られないダブルディスパッチ可能な仮想テーブルが欲しければ型クラスを作ってもよい
のです。問題は単面的であることで、重要なのはラベリングではなく、大ざっぱで輪郭も定まらない「oop」なるカテゴリを追うのではなく、道具箱であるプログラミング言語
の言語機能個別にバラしてそれぞれ(継承、状態、仮想関数、モジュール化,バインディング、不変性と定数性..)の本質と効用を理解し、現実世界の制約含めメリデメで評価し組み合わせることだと思っています。たとえば莫大な画面数のERPシステムを作るとしたらJava/OOP以外の選択肢はまだありません。脚注
んー、なんか文脈が間違って理解されてるような。
「staticおじさん」に反対する人のことを「オブジェクト指向擁護者」って言われると違和感しかありません。
そもそも「staticおじさん」の認識が間違ってるように思います。もともとは、Java言語を理解できないのに、Javaを否定する人のことを指してました。むしろプログラマと言うより、仕様書を書くだけのSEを指しているはずです。
プログラマ崩れで、SEと称して分厚い仕様書を書いている「だけ」人が、勉強もしないのに無知でJavaのことを悪く言っている、と言うようにとらえています。だからあんなに叩かれたのだろうと。
よって、「staticおじさん」は実質上プログラマではなく、勉強もしない、頭が固い、無能SEを指しているのであって、反対している人は、いわば「無能SE反対者」です。Java等の「オブジェクト指向擁護者」と言われるはおそらく心外です。
「staticおじさん」のいうJavaの評価が間違ってると言ってるのであって、Java「全体」の擁護をしているのではありません。
その文脈を間違ってはいけないと思います。
その文脈を間違えると、個人攻撃をしているように感じたり、あたかもJava(orオブジェクト指向)全体の擁護をしているように感じるのだと思います。
いわゆるITドカタと言われる、SE->下請けプログラマ(n次請け)の構造を前提に、SEが知らない
んー、なんか文脈が間違って理解されてるような。
「staticおじさん」に反対する人のことを「オブジェクト指向擁護者」って言われると違和感しかありません。
そもそも「staticおじさん」の認識が間違ってるように思います。もともとは、Java言語を理解できないのに、Javaを否定する人のことを指してました。むしろプログラマと言うより、仕様書を書くだけのSEを指しているはずです。
プログラマ崩れで、SEと称して分厚い仕様書を書いている「だけ」人が、勉強もしないのに無知でJavaのことを悪く言っている、と言うようにとらえています。だからあんなに叩かれたのだろうと。
よって、「staticおじさん」は実質上プログラマではなく、勉強もしない、頭が固い、無能SEを指しているのであって、反対している人は、いわば「無能SE反対者」です。Java等の「オブジェクト指向擁護者」と言われるはおそらく心外です。
「staticおじさん」のいうJavaの評価が間違ってると言ってるのであって、Java「全体」の擁護をしているのではありません。
その文脈を間違ってはいけないと思います。
その文脈を間違えると、個人攻撃をしているように感じたり、あたかもJava(orオブジェクト指向)全体の擁護をしているように感じるのだと思います。
いわゆるITドカタと言われる、SE->下請けプログラマ(n次請け)の構造を前提に、SEが知らないのに新しい技術(Java)を悪く言ってる、と言う文脈なので、このITドカタ構造を悪く言ってることになります。
ということで少なくとも、私は「staticおじさん」を評価する気があまりありません。勉強せずにJavaを勝手に評価し、しかも内容は間違ってます。
でも、オブジェクト指向が絶対だとも思ってません。そもそも技術は進歩するものです。
「staticおじさん」は自分の理屈が絶対だと信じていました。そしてその「自分の理屈が絶対」であることが主に批判されてます。そしてそれが主に馬鹿にされていたところです。技術の内容の話ではありません。
それに反対する人が、自分の理屈が絶対だと仮定するのはおかしいし、する必要もありません。よって「オブジェクト指向擁護者」って言われると、何かが違うと思います。そもそもオブジェクト指向が絶対だとか思ってませんので。
ということで、私から見ると不思議な文脈で理解されているようなので書いておきました。
バカにするのは不当でしょう。
自分の中で確立した理論や考察があり、その上で技術者として相手の主張に対して技術面での理由をつけて全否定することがあるのは仕方ありません。その主張自体が正しいかどうかは第三者、あるいはその後の議論で決まることですから、あとで「ごめんなさい、間違っていました」と言うなら立派です。しかし、主張に対して否定できるのはその主張自体までであって、「老害」とか人格まで否定出来るものではないし、ましてや自分なりの理解もないのに「流行りの理論に従って強者につく」のは技術者のやることではありません。
オブジェクト指向プログラミング -- 1兆ドル規模の大失敗 という記事があります。そうなんでしょうか?に対するDaisuke Sawadaさんの回答
私も昔はOOP信者でした。this(self)って素晴らしい、継承考えた人天才、パターンしっかり覚えなくっちゃ、でした。GUIプログラミングと相性が良かったからというのもあるでしょう。でもその当時の私はクロージャも高階関数も理解していませんでした。staticでないと結局困る場面も体感します。OOPって実際は生産性や品質向上に対して強力無比な手法だったのだろうか?と再考し始めます。そして新しいプログラミング言語、たとえばSwiftやRustあたりを知ると、「OOPもパラダイムとして取り込んでるよ」と言いながらも過去の言語とは違っている
バカにするのは不当でしょう。
自分の中で確立した理論や考察があり、その上で技術者として相手の主張に対して技術面での理由をつけて全否定することがあるのは仕方ありません。その主張自体が正しいかどうかは第三者、あるいはその後の議論で決まることですから、あとで「ごめんなさい、間違っていました」と言うなら立派です。しかし、主張に対して否定できるのはその主張自体までであって、「老害」とか人格まで否定出来るものではないし、ましてや自分なりの理解もないのに「流行りの理論に従って強者につく」のは技術者のやることではありません。
オブジェクト指向プログラミング -- 1兆ドル規模の大失敗 という記事があります。そうなんでしょうか?に対するDaisuke Sawadaさんの回答
私も昔はOOP信者でした。this(self)って素晴らしい、継承考えた人天才、パターンしっかり覚えなくっちゃ、でした。GUIプログラミングと相性が良かったからというのもあるでしょう。でもその当時の私はクロージャも高階関数も理解していませんでした。staticでないと結局困る場面も体感します。OOPって実際は生産性や品質向上に対して強力無比な手法だったのだろうか?と再考し始めます。そして新しいプログラミング言語、たとえばSwiftやRustあたりを知ると、「OOPもパラダイムとして取り込んでるよ」と言いながらも過去の言語とは違っていることに気付きます。
あとになって「今の俺はこう考えています」と堂々と言うためには、常に自分が納得していることを主張しておくことです。
そちらの事案についてはあまり歴史的な経緯をちゃんと追っていない素人なので、不当だったかどうかを語れる立場ではないですが、まあ炎上案件というものは概ね過剰で不当な傾向があるかと思います。
一方で、今拝見したところ、件の記事では冒頭に以下のような記載があります。
> staticを理解していない人のコードを見ると、いちいちインスタンス宣言しているので笑ってしまう。
内容の是非はともかく、ご本人が他人をバカにしているように見える書き方をされているので、そんなことを言われたらカチンとくる人がいるのは容易に想像できます。
もちろんだからといって周りがバカにしていいという訳ではないですが、上の一文がなくもう少し謙虚な書き方ができれば、また歴史は違うものになったのではないでしょうか。
プログラムに「スズメ」や「カラス」「ハト」が登場します。
「鳥」クラスが「飛ぶ」メソッドを持つとします、この仮定は一見自然で人間にもわかりやすい「リーダブル」な表明です。この前提を用いて継承を用いプログラムを構成することにしました。
さて3カ月後クライアントが「ペンギン」と「ニワトリ」を登場させるように仕様改定を要求してきました。残念なことにペンギンやニワトリは「跳ぶ」ことはあれど「飛ぶ」ことはできません。わずか2つの「反例」が追加されたがために、あなたは次の3つの選択をしなければなりません。
- ペンギン、ニワトリをプログラムに登場させないようにクライアントに交渉する
- ペンギン、ニワトリは鳥であるが、鳥クラスは継承しないことにした
- ペンギン、ニワトリは鳥クラスを継承し、flyを呼んでも「何もしないようにする」
1を選んだ場合、「なんて馬鹿げた話だ」とクライアントは怒り始めました。継承という実装手法を選んだがために仕様の拡張ができないなんて言うもんだから、このプロダクトの賞味期限をいたずらに短くしただけだというわけです。(私がクライアント、あるいはプロダクトマネージャの職責を負い、プロダクトに本気だとすればキレること間違いなしでしょう。)
わざわざ怒られにいったり交渉事も苦手なので1の結末を予測した並行世界のあなたは2を選ぶことにしました。2カ月後中途入社で物おじしないBさんが「なんでペンギンは鳥で
プログラムに「スズメ」や「カラス」「ハト」が登場します。
「鳥」クラスが「飛ぶ」メソッドを持つとします、この仮定は一見自然で人間にもわかりやすい「リーダブル」な表明です。この前提を用いて継承を用いプログラムを構成することにしました。
さて3カ月後クライアントが「ペンギン」と「ニワトリ」を登場させるように仕様改定を要求してきました。残念なことにペンギンやニワトリは「跳ぶ」ことはあれど「飛ぶ」ことはできません。わずか2つの「反例」が追加されたがために、あなたは次の3つの選択をしなければなりません。
- ペンギン、ニワトリをプログラムに登場させないようにクライアントに交渉する
- ペンギン、ニワトリは鳥であるが、鳥クラスは継承しないことにした
- ペンギン、ニワトリは鳥クラスを継承し、flyを呼んでも「何もしないようにする」
1を選んだ場合、「なんて馬鹿げた話だ」とクライアントは怒り始めました。継承という実装手法を選んだがために仕様の拡張ができないなんて言うもんだから、このプロダクトの賞味期限をいたずらに短くしただけだというわけです。(私がクライアント、あるいはプロダクトマネージャの職責を負い、プロダクトに本気だとすればキレること間違いなしでしょう。)
わざわざ怒られにいったり交渉事も苦手なので1の結末を予測した並行世界のあなたは2を選ぶことにしました。2カ月後中途入社で物おじしないBさんが「なんでペンギンは鳥ではないんですか?」と聞いてきます。あなたはかくかくしかじかこういう経緯があってペンギンは鳥ではないことになっていると説明しました。Bさんは経験があるので「あーそういうことね」と追体験するわけですが、同時に(・・・わざわざ鳥クラスという名前を与えた意味って何だったん?)と早くもテンション低下気味です。
「ペンギンは鳥ではない」という非直観的な実装を同僚やメンバーに説明して回るなんてしたくないあなたは、結局3の結論に行き着きます。これならペンギンは鳥のままであるし、一応関係各所に申し開きができます。
こうして現実的には3の選択肢しか取れないゆえにプログラムのインタフェースが開発を進めるごとにどんどん腐っていきます。(これじゃ設計するどころかその真逆だ)
ペンギンが「飛べない」からといって一方的に「飛ぶ」メソッドを拒絶しつつ鳥クラスを継承するだなんて型の「切り抜き加工」はできないわけです。
鳥クラスに「飛ぶ」クラスをつけたのは無知で愚かだった、次はうまくやれるといい始め、次は窓クラスの設計に挑むことになりました。窓は開け閉めするものだからと「開ける」メソッドと「閉める」メソッドをとりつけたら今度はFIX窓なる固定窓が現れて・・・(以下無限ループ)
- を選べば、実装が仕様を必要以上に制約することになっている
- そんな実装手法を選ぶなよとしかいいようがない
- 実装手法がプロダクトの方向性を妨げる意味とは
- を選べば、非自明な実装が決定仕様として表れ始める
- 皆が忌み嫌う「レガシー」とはこうした非自明、非直観な実装のあつまりのことではなかったか
- を選べば、インタフェースが腐る
- ペンギンは飛ばないけど、飛ぶメソッドがなぜかついている
- 押して開ける扉なのに取っ手がついているようなもの
- それはデザインの基本的原則にすら反する
データ構造を継承するということは、その2つのデータ構造は「変性」を持つということになります。
結局のところ、継承というのはクラス構造にこの長期的に作用する厄介な制約を十分にドメイン知識が獲得できていない開発初期段階でわざわざ実装に持ち込むということです。そして仕様が歪んだり、非自明性を持ち込むことになるのは、上記事例で示した通りです。
(数学的に真であるなど、絶対に変更しえない仕様がある場合はこの制約が有益なこともあると一応示唆しておきます。)
ようするに、継承を用いて完全な設計をすることは、「仕様、ドメイン知識について全知であり不確実性が一切ない」という前提下においてワークします。そしてそうした技法をサービス開発で適用するのは「無意味」どころか「有害」です。どちらかといえば仕様やドメイン知識を獲得しそれを反映するという作業こそが開発、あるいはプロダクトデザインという作業の本質だからです。
あなたが真に設計者ならば、まずこの問いを立てなければなりません。
「なぜ物事が実体化する前に先んじて意思決定しなければならないのか?」
このことについて解像度高く明晰に語れないのであれば、実際のところ設計はしていない、必要ないと考えたほうがよいです。
3つの問題はことごとく「開発者が継承に価値を見出し、必要以上の制約をわざわざ実装にもたらしたから発生している」ことで、クライアントが意地悪で先にその仕様拡張を言ってこなかったからとか、その開発者が無知蒙昧だったからとかではありません。(もしそう思ってるなら上で語ったミスを生涯にわたり繰り返すことでしょう。)
継承という技法は何の設計支援もしてくれないどころか、最終的には3のような「押して開ける扉なのに取っ手がついている」状況にわざわざ陥るように力強く支援をしてくれます。
私はかつて「デザイン」と「設計」と名のつく部署にいて、働いていたので設計とは何かを理解して、設計には何が必要で、何が必要でないかを理解しているつもりです。1のような無駄な制約をたかが実装手法がもたらすというのはデザイナーが最も忌み嫌うことですが、語られるようであまり語られない真実でしょう。
もちろんマテリアルの特性や、どのような設計が有益なプロダクトを構成しうるのかについて「無知蒙昧」であるならばよい設計にはなれませんが、継承に対しては「無知」であったほうがよい設計になれるのではないかと思います。設計行為に対する感受性(センス)を腐らせうる、それが継承の最も根源的な問題です。
質問の中で、まず「オブジェクト指向という言葉」に関して言えば、ある研究者がすでに存在していた種々の技術をまとめて子供のようなエンドユーザーでも使えるものにしようとしていたときに、懐疑的な人から「一体何をしようとしているんだ」と聞かれ、勢いに任せて「オブジェクト指向だよ」と言った、というのが発端です(1970年ごろのことだと思います)。
「オブジェクト」という言葉は、OSの分野などではメモリに割り当てられた複合的なデータ、というものを指す言葉として「オブジェクト指向」以前から使われていたのですが、そのものそのものが自律的なものとして扱うのだ、という意図が込められていたわけです。
プログラムをより良いやり方で組織立って書こうという努力は、コンピューターの性能が上がってきてからようやく人々が考えられるようになった、というようなものではなく、もちろんプログラミングというものが始まった時から常に考えられてきたことです。オブジェクト指向に関して言えば、その大きなインスピレーションの一つに、1961年以前から米空軍で発明されて使われていたデータの格納方式があります。
あちこちの基地の間をテープでやり取りしていたデータがどんどん複雑化していくという問題が当時からあり、今では名前を残していない誰かが、「テープの先頭から決まったオフセットのところに、後に続く読み出しや書き込みのためのプロシージャへのポインタ
質問の中で、まず「オブジェクト指向という言葉」に関して言えば、ある研究者がすでに存在していた種々の技術をまとめて子供のようなエンドユーザーでも使えるものにしようとしていたときに、懐疑的な人から「一体何をしようとしているんだ」と聞かれ、勢いに任せて「オブジェクト指向だよ」と言った、というのが発端です(1970年ごろのことだと思います)。
「オブジェクト」という言葉は、OSの分野などではメモリに割り当てられた複合的なデータ、というものを指す言葉として「オブジェクト指向」以前から使われていたのですが、そのものそのものが自律的なものとして扱うのだ、という意図が込められていたわけです。
プログラムをより良いやり方で組織立って書こうという努力は、コンピューターの性能が上がってきてからようやく人々が考えられるようになった、というようなものではなく、もちろんプログラミングというものが始まった時から常に考えられてきたことです。オブジェクト指向に関して言えば、その大きなインスピレーションの一つに、1961年以前から米空軍で発明されて使われていたデータの格納方式があります。
あちこちの基地の間をテープでやり取りしていたデータがどんどん複雑化していくという問題が当時からあり、今では名前を残していない誰かが、「テープの先頭から決まったオフセットのところに、後に続く読み出しや書き込みのためのプロシージャへのポインターを入れ、そして一緒に送っているデータを読み書きするためのプロシージャを同じテープの中に格納して送れば良いじゃないか」というアイディアを実現して広めたわけです。このようなテープは、データとそれを扱うためのプロシージャが文字通りひとまとめにされており、共通したインターフェイスからそのプロシージャにアクセスできたわけですね。つまり今で言うようなオブジェクト指向のオブジェクトの片鱗を持っていたわけエス。
このようなアイディアは、聞かされてみれば「誰でも自然に思いつく」と思われるかもしれませんが、発明としてはよくある話で、最初に思いついて実装するには何年もかかったものです。
オブジェクト指向という言葉が生まれる他のインスピレーションとしては、インターネットがありました。それは遠隔地にあるコンピュータ同士がメッセージを送り合うことにより動作するというものですが、こちらはネットワークというのはどこかが故障したりするものだから、効率重視をして上手くいくときに動けば良い、という仕組みとは正反対のポリシーで作るべきであるという考えに基づいていました。今では当たり前かもしれませんが、当時の一般的な考えとは逆行していたわけです。でも、この設計のおかげで、インターネットは五十年以上前に立ち上がった時から一度も全体をリブートして立ち上げ直す、というようなことをする必要なく、10^10個というようなノードが参加したネットワークに有機的に拡大したわけです。これこそは本当に「当たり前ではないけれども天才たちが見事に発明したもの」の例だと思います。
オブジェクト指向を謳ったプログラミング言語も、元々はこのようなスケーリングの可能性を持たせ、例えば一部の機能が不全を起こしても全体としてはクラッシュしない、というような特徴を持っているべきだとされていたわけです。もしお使いの言語がそのような特性を持っていないのだとすれば、それは十分安全ではないわけですね。そして、そのような特性を持たせるためには、システムを作るときにそれなりのビジョンと努力が必要となります。
Simulaという、オブジェクト指向という言葉が生まれる前から存在した言語もあります。こちらも素晴らしい発明でしたし、オブジェクト指向という言葉を生んだ言語に影響を与えましたが、やはりいろいろと先行する技術要素についても知っており、かつビジョンもある、というところでようやく生まれた概念であるとは思います。
というわけで、名前としてはある研究者がふと思いついていった言葉、というのが理由ですが、ただその裏にはかなり深いビジョンと歴史の積み重ねとひらめきがあり、なかなか当たり前のように思いつくものではないように思います。
オブジェクト指向とは何ですか?で書いた“メッセージングの「オブジェクト指向プログラミング」”について回答します。これはSmalltalkが(不完全ながらも)もっともよくサポートするパラダイムがゆえに、主に「Smalltalkでのプログラミングの好きなところ」になってしまっていますが、その点はどうぞあしからず。^^;
さて。好きなところは、要求されている仕様や思いついた段取りをそのまま素直に書き下せて、それを即座に実行して想定したとおりに動作するのかを確かめる、そんなインタラクティブなコーディングスタイルを許容してくれるところです(もとよりそれには、処理系や環境、ひいてはそれらの設計者や実装者の「メッセージングのオブジェクト指向」に対する深い理解と協力が不可欠です)。
シンプルな例を挙げると、たとえば、
- xが3で割り切れるときは'Fizz'を返し、
- xが5で割り切れるときは'Buzz'を返し、
- xが15で割り切れるときは'FizzBuzz'を返し、
- いずれにも当てはまらないときはxを返す。
という仕様が与えられたときに、
- xにfizzというメッセージを送ると3で割り切れるときは'Fizz'をそうでないときは''を返し、
- xにbuzzというメッセージを送ると5で割り切れるときは'Buzz'をそうでないときは''を返すようにしておけば、
- (1.と2.の返値を結合させることで)xが15で割り切れるときは'Fi
オブジェクト指向とは何ですか?で書いた“メッセージングの「オブジェクト指向プログラミング」”について回答します。これはSmalltalkが(不完全ながらも)もっともよくサポートするパラダイムがゆえに、主に「Smalltalkでのプログラミングの好きなところ」になってしまっていますが、その点はどうぞあしからず。^^;
さて。好きなところは、要求されている仕様や思いついた段取りをそのまま素直に書き下せて、それを即座に実行して想定したとおりに動作するのかを確かめる、そんなインタラクティブなコーディングスタイルを許容してくれるところです(もとよりそれには、処理系や環境、ひいてはそれらの設計者や実装者の「メッセージングのオブジェクト指向」に対する深い理解と協力が不可欠です)。
シンプルな例を挙げると、たとえば、
- xが3で割り切れるときは'Fizz'を返し、
- xが5で割り切れるときは'Buzz'を返し、
- xが15で割り切れるときは'FizzBuzz'を返し、
- いずれにも当てはまらないときはxを返す。
という仕様が与えられたときに、
- xにfizzというメッセージを送ると3で割り切れるときは'Fizz'をそうでないときは''を返し、
- xにbuzzというメッセージを送ると5で割り切れるときは'Buzz'をそうでないときは''を返すようにしておけば、
- (1.と2.の返値を結合させることで)xが15で割り切れるときは'FizzzBuzz'を返させることができ、
- いずれにも当てはまらないときは''になるのでその時はxを返せばよい。
という段取りを思いついたとして、即座に、Smalltalk環境でなら、
- | x |
- x := 1.
- x fizz, x buzz ifEmpty: x
比較のため、同じくメッセージングのオブジェクト指向を限定的にサポートするRuby(のirb)でなら、
- x = 1
- (x.fizz + x.buzz).tap{ |it| break x if it.empty? }
というように、その仕様(この場合は思いついた処理)のとおりに書き下して実行し、検証ができるそんなところです。
残念ながら、オブジェクト指向(具体的には、そのキモである「決定の遅延」)のサポートにSmalltalkほど重きを置いていないRubyでは、あらかじめInteger#fizz、同#buzzを次のように(これもやはり仕様のとおり書き下すことで)定義しておく必要がありますが…
- class Integer
- def fizz; self % 3 == 0 ? "Fizz" : "" end
- def buzz; self % 5 == 0 ? "Buzz" : "" end
- end
ともあれ、1..15のいずれについても期待通りの結果が得られるはずです。
- (1..15).collect{ |x| (x.fizz + x.buzz).tap{ |it| break x if it.empty? }
- #=> [1, 2, "Fizz", 4, "Buzz", "Fizz", 7, 8, "Fizz", "Buzz", 11, "Fizz", 13, 14, "FizzBuzz"]
ちなみに、Smalltalkの場合はどうかと言うと、とりあえず最初のコードを実行(print it)してみて「△△は○○なんてメッセージは知らない…(MessageNotUnderstood: △△>>○○)」というノーティファイアが現れたら(つまり、実行が中断してしまったら)、おもむろにそのノーティファイアのCreateボタンを押して
fizzについてなら
- Integer >> fizz
- ^ (self isDivisibleBy: 3) ifTrue: 'Fizz' ifFalse: ''
buzzについてなら
- Integer >> buzz
- ^ (self isDivisibleBy: 5) ifTrue: 'Buzz' ifFalse: ''
と指摘された都度に必要なメソッドを定義して処理を続行(Proceed)してやることで、
期待される結果の1を得ることが可能です。(Createによるメソッド自動生成機構のないSmalltalk処理系もありますが、その場合はノーティファイアはそのままにして別途クラスブラウザ等でInteger>>fizzあるいはbuzzを定義してからノーティファイアに戻ってProceedすれば同じことができます。念のため)
もちろん、1から15についてもRubyと同様で問題なく動きます。
- (1 to: 15) collect: [:x | x fizz, x buzz ifEmpty: x].
- "=> #(1 2 'Fizz' 4 'Buzz' 'Fizz' 7 8 'Fizz' 'Buzz' 11 'Fizz' 13 14 'FizzBuzz') "
こうしたメッセージング(あるいはメッセージを記述する行為)を介し「決定の遅延」を意識する、つまるところ「オブジェクト指向」のコーディングの流れは、思考を妨げることが少なく、とても快適で気に入っています。
ついでに、メソッド(非オブジェクト指向であれば関数や手続き)のコールを記述するというよりも、オブジェクトや読み手へのメッセージを記述することを意識したり、それがむしろ推奨されることもとても気に入っていることのひとつです。
たとえば、前述のInteger>>fizzの記述では「 3で割り切れるときは'Fizz'をそうでないときは''を返し…」という仕様を、(self isDivisibleBy: 3) ifTrue: 'Fizz' ifFalse: '' などとストレートに記述できるといった具合です(もし、Number>>isDivisibleBy: が定義されていない処理系なら、fizzやbuzz同様、指摘されたらそのタイミングで期待される振る舞いを記述したメソッドを追加すればよいのです!)
一方で、「嫌い」とまではいきませんが、このメッセージングの考え方でのコーディングを推し進めたとき、処理系にもう一工夫あると便利なのにな…としばしば思うのは、同じメッセージを送って同じ結果が返ると分かっている場合は、前の結果を保持してそれを返して欲しいところです。もっと踏み込むと、我々がメッセージを送るだけで適切なアルゴリズムを選択してよきに計らってくれる、そんなシステムや処理系が理想です。
そこまでいかずとも、関数型でいうところの参照透明性のような利便性があると、メッセージングの記述をもっと読み下しやすく仕様に近づけられるのではないかななどと想像します。
誰がオブジェクト指向を理解するために、動物>哺乳類>犬のようなアナロジーを持ちこんだのでしょうね?
こういったアナロジーはまったく理解の役に立たなかったのではないかと思います。
私はオブジェクト指向に出会う前は、C言語でプログラミングをしていました。C言語でプログラミングをしていると、大抵は問題領域に合わせて構造体を作り、それを関数に渡して内部のデータを操作するというようなことが起ります。そして関数間でそのデータを引き回すのです。
嘗て、「データ構造+アルゴリズム=プログラム」という古典的な名著がありました。そう言われるくらいにデータ構造とアルゴリズムというのはプログラムにおいて切っても切れない関係にあるわけです。
データ構造とアルゴリズムをひとつのパッケージに入れてしまおうというのがオブジェクト指向の発端だと想像しています。
そうすることの利点は、あるコードの集合体を一箇所にまとめて何に関心のあるコードなのか明確することができる。また、それに対して名前をつけることができる。ということです。こうしてコードが表わすある概念に名前をつけて整理するわけですね。これがクラスです。
そうなんです。名前を付けられることはとても重要なことなんです。
名前をつけることで概念を具現化するわけです。そしてもう一歩先に進むと概念同士の関係を構造化することができます。
それが最初に出てきた、動物 < 哺乳類 < 犬のよう
誰がオブジェクト指向を理解するために、動物>哺乳類>犬のようなアナロジーを持ちこんだのでしょうね?
こういったアナロジーはまったく理解の役に立たなかったのではないかと思います。
私はオブジェクト指向に出会う前は、C言語でプログラミングをしていました。C言語でプログラミングをしていると、大抵は問題領域に合わせて構造体を作り、それを関数に渡して内部のデータを操作するというようなことが起ります。そして関数間でそのデータを引き回すのです。
嘗て、「データ構造+アルゴリズム=プログラム」という古典的な名著がありました。そう言われるくらいにデータ構造とアルゴリズムというのはプログラムにおいて切っても切れない関係にあるわけです。
データ構造とアルゴリズムをひとつのパッケージに入れてしまおうというのがオブジェクト指向の発端だと想像しています。
そうすることの利点は、あるコードの集合体を一箇所にまとめて何に関心のあるコードなのか明確することができる。また、それに対して名前をつけることができる。ということです。こうしてコードが表わすある概念に名前をつけて整理するわけですね。これがクラスです。
そうなんです。名前を付けられることはとても重要なことなんです。
名前をつけることで概念を具現化するわけです。そしてもう一歩先に進むと概念同士の関係を構造化することができます。
それが最初に出てきた、動物 < 哺乳類 < 犬のような関係です。動物で例えると分らなくなるので、プログラミングの概念で考えると解りやすいでしょう。
たとえば、プロトコル < TCPとかプロトコル < HTTPとか(え?HTTPとTCPではレイヤが違うんじゃないの?というつっこみが入りそうですが、プロトコルの抽象化のしかたによってはこういうのもありますよ。)
TCPはプロトコルの一種とかHTTPはプロトコルの一種というように言うことができるのです。これを多態性といいます。
TCPであってもHTTPであってもプロトコルとして扱う分には同じように扱えるわけです。こうした考えかたを使えば、例えばある通信ソフトウェアにおいてプロトコルのレイヤを差しかえて使うことができるようになるわけです。
プロトコルの話は一例ですが、このように概念に名前をつけて概念を構造化するのがクラスです。
似たような質問への回答をここに書きました。
これはオブジェクト指向でプログラミングする必要がある!と思う場合は、どんなアルゴリズムを作ろうとした時でしょうか?に対するNiwa Yosukeさんの回答
オブジェクト指向は階層構造による情報整理手法です。
整理するほど情報がないなら別にオブジェクト指向を持ち出さなくてもいいです。しかし、コードを数百行ほど書いたら普通は整理したくなります。
オブジェクト指向が必要なのは、階層構造を作らないとどこに何があるかわかりにくくなるからです。大規模開発でコードが数万行にもなるケースでは、オブジェクト指向がないと管理できず破綻します。
C++ / Java / C# などのオブジェクト指向言語における class は C 言語の struct に端を発しています。struct はプリミティブなデータ型 (int, long, double 等) をいくつか集めてひとまとめにパックしたものです。
C 言語においては、struct を単位としてネストさせ、struct を含む struct を含む struct … と階層構造にすることで複雑なデータセットを構成でき、structA.structB.structC というように "." を連ねることで階層内の目標とする対象へ直感的にたどり着ける便利なアクセス記法を用いる仕様になっています。
パックしたデータはそれぞれ意
似たような質問への回答をここに書きました。
これはオブジェクト指向でプログラミングする必要がある!と思う場合は、どんなアルゴリズムを作ろうとした時でしょうか?に対するNiwa Yosukeさんの回答
オブジェクト指向は階層構造による情報整理手法です。
整理するほど情報がないなら別にオブジェクト指向を持ち出さなくてもいいです。しかし、コードを数百行ほど書いたら普通は整理したくなります。
オブジェクト指向が必要なのは、階層構造を作らないとどこに何があるかわかりにくくなるからです。大規模開発でコードが数万行にもなるケースでは、オブジェクト指向がないと管理できず破綻します。
C++ / Java / C# などのオブジェクト指向言語における class は C 言語の struct に端を発しています。struct はプリミティブなデータ型 (int, long, double 等) をいくつか集めてひとまとめにパックしたものです。
C 言語においては、struct を単位としてネストさせ、struct を含む struct を含む struct … と階層構造にすることで複雑なデータセットを構成でき、structA.structB.structC というように "." を連ねることで階層内の目標とする対象へ直感的にたどり着ける便利なアクセス記法を用いる仕様になっています。
パックしたデータはそれぞれ意味を持っている (= 意味単位にデータをパックする) ため、そのデータセットに適用する関数も抱き合わせで近くに置いておきましょうという発想で、データセット/変数群だけでなく関数をもパック単位に包含するように struct を拡張したのが C++ 以降の class です。
その上で、せっかくデータをわかりやすくパックしたのだから、C 言語のようにフラットに置かれた関数にこまごまとした複数のプリミティブ型データを適用するのではなく、パックしたデータセットの方へそのデータセットに相応しい関数を適用する、というように発想を転換します。データと関数の主客を逆転させ、まとまりとして意味を持つデータセット (= オブジェクト) の方を主軸に据えるため「オブジェクト指向」と呼ばれます。
オブジェクト指向プログラミングには「継承・多態性 (ポリモーフィズム)」「is-a (派生) 関係と has-a (内包) 関係 」「契約と実装の分離」「オーバーライドとオーバーロード」「差分プログラミング」等々の概念やテクニックがありますし、凝り始めるとデザインパターンなどの難しい抽象化にハマっていくことになりますが、そのような難しく抽象的な側面から説明を始めるから、初心者は混乱してオブジェクト指向の目的や意味を見失うのだと私は思います。
オブジェクト指向の本質は単なる「階層構造による整理」であり、加えて「各階層やその中身にアクセス権を割り振る」ということをするだけです。ファイル管理とまったく同じです。
大量のファイルを扱う場合、意味単位別にフォルダを掘って、その中にファイルを格納し、アクセス権を設定する。深い階層には Layer01/Layer02/Layer03 という記法でアクセスする。
それと同様に、大量の変数や関数を扱う場合、意味単位別にクラスを掘って、その中にメンバー(変数、関数)を格納し、アクセス権を設定する。深い階層には Layer01.Layer02.Layer03 という記法でアクセスする。
ただそれだけのことです。
英語では「object-oriented」で「OO」と略され、1960~1980年代のプログラミング手法(OOP)から始まり、その応用としてソフトウエアの設計・分析の手法(OOD/OOA)、近年はユーザーインターフェース・エクスペリエンスのデザイン(OOUI/OOUX)、オブジェクト指向存在論(OOO)なる哲学分野にまで、広く使われる用語です。ここではOOPについて説明を試みます。
オブジェクト指向の「オブジェクト」は、1967年に発表されたSimula 67
というプログラミング言語に組み込まれた当時としては新しい同名の言語機能(あるいはそれに準ずる概念)を指し、それは端的に自らに関連した操作を記述した関数(もしくはプロシージャ。総じて「手続き」)を自分の中に持っている特殊なデータを表します。またSimula 67には、このオブジェクトの仕様の定義と実体化(インスタンス化)のための「クラス」という言語機能があり、これは既存のクラスを継承し差分を書き足すだけで類似の新しいクラスを定義できる機構を備えていました。このSimula 67の「オブジェクト」と「クラス」という2つの言語機能やその振る舞いをヒントにしたり応用することで、次の2つのアイデアが提案されました。
①「メッセージング(メッセージ送信)による決定の遅延の徹底」…アラン・ケイは、オブジェクトを生物の細胞やインターネット(当
脚注
英語では「object-oriented」で「OO」と略され、1960~1980年代のプログラミング手法(OOP)から始まり、その応用としてソフトウエアの設計・分析の手法(OOD/OOA)、近年はユーザーインターフェース・エクスペリエンスのデザイン(OOUI/OOUX)、オブジェクト指向存在論(OOO)なる哲学分野にまで、広く使われる用語です。ここではOOPについて説明を試みます。
オブジェクト指向の「オブジェクト」は、1967年に発表されたSimula 67
というプログラミング言語に組み込まれた当時としては新しい同名の言語機能(あるいはそれに準ずる概念)を指し、それは端的に自らに関連した操作を記述した関数(もしくはプロシージャ。総じて「手続き」)を自分の中に持っている特殊なデータを表します。またSimula 67には、このオブジェクトの仕様の定義と実体化(インスタンス化)のための「クラス」という言語機能があり、これは既存のクラスを継承し差分を書き足すだけで類似の新しいクラスを定義できる機構を備えていました。このSimula 67の「オブジェクト」と「クラス」という2つの言語機能やその振る舞いをヒントにしたり応用することで、次の2つのアイデアが提案されました。
①「メッセージング(メッセージ送信)による決定の遅延の徹底」…アラン・ケイは、オブジェクトを生物の細胞やインターネット(当時はその前身のARPAネット)を構成するコンピューターに見立て、それらが互いにメッセージを送りあうメタファー(あるいは実際に送るのでもよい)でプログラムを書いたり動かしたりすることで、堅牢で変化に柔軟に対応できる長寿命のソフトウエアを構築できないかと考え、Smalltalkという言語処理系
でその有効性を検証しました 。②「クラスによる抽象データ型の実現(カプセル化・継承・多態性)」…ビャーネ・ストラウストラップは、Algolへのクラス(とオブジェクト)の拡張であるSimula 67を踏襲し、Cに対しても同じようにクラスを追加することで、C with Classesを経てC++を設計しました
。この過程で彼は、クラスをユーザー定義のデータ型(広い意味でのバーバラ・リスコフの抽象データ型 )と見做すことが可能であることに気がつき、クラスの継承により、部分型を定義したり、静的型チェックによる安全なプログラミングが可能であると考えました 。なおこのアイデアについては、クラスの発案者であるSimula 67の設計者たち や抽象データ型の発案者であるリスコフ(ただし自ら設計のCLUはパフォーマンス優先でクラスは用いずに抽象データ型を実現している)、Eiffelの設計者であるバートランド・メイヤー ら、複数のグループが時を前後して同様の“気づき”におそらくは自力で到達しています(ここでは知名度とそれに伴う他言語への影響の大きさを鑑み、C++のビャーネ・ストラウストラップを代表格として選びました。あしからず)。①も②も同じように「オブジェクト指向プログラミング」を名乗ったため、OOPというコンセプトは現在に至るまで混乱を極めています。これは私見ですが、もし、①が最初にオブジェクト指向などと名乗らず「メッセージ指向」と、and/or 続く②が「クラス指向」等々と名乗ってくれていたならば、今日のような混乱や「どちらが真のオブジェクト指向か?」などという不毛な論争の多くは避けられていたはずです。
ともあれOOPは今、特に前置きや断りもなく現れて文脈によって①なのか②なのかを聞き手・読み手側が判断しなければならなかったり、話し手・書き手の理解不足により両者の混同があったり、はたまた両者が相反しない範囲(例えば、①は可能な限りの遅延結合、②は比較的早期の結合を是とする点で相容れないので、そこはあまり突き詰めない立ち位置)で互いの要素を取り入れた混成として扱われることが多く、実に学習者泣かせの難解な用語となってしまっています。
基本的に、メジャーなプログラミング言語においては②のアイデアに基づくスタイルを主にサポートするOOPがほとんどですが、一部ではRubyのようにSmalltalkの少なからぬ影響下にある言語においてその仕様やメソッド名に①のコンセプトである「メッセージ」を意識した説明が必須であったり、昨今はアクターやそれに準ずる機能を前面に押し出した言語やフレームワークの台頭で、①に軸足を置いてOOPが語られる機会も多く、①のOOPも完全には排除できません。
以下は関連する補足です。
- [2023–09–20 追記] アラン・ケイが、ビルディングブロックとしての「オブジェクト」の存在とその可能性に気がついたのは 1966年ごろ、まだ「クラス」や「オブジェクト」という言語機能を持たない(同等の機能をそれぞれ「アクティビティ」「プロセス」と呼んでいた)SimulaⅠの処理系のソースコードを読んでいたときのことなので、実際の話の流れはもう少し複雑です^^;
- Smalltalkのごく初期の実装(Smalltalk-72 )では、クラスは継承機構を持たず、メソッドも独立した関数ではなく、非同期ではないながらもコンセプトに従い、実際にオブジェクトにメッセージを送る機構を採用していました 。しかし速度面の問題などから、その後のSmalltalk-76 以降の実装 で はSimula 67スタイルのクラス(つまり、継承機構を有し、メソッドを独立した関数としてクラスに内包する)を採用して、メソッド(つまりメンバー関数)の動的呼び出しをもってメッセージ・パッシングと称する省コスト版(呼び出すメソッドが見つからない時だけメッセージはハンドリングできる)の機構に置き換えられました。以来このため①と②それぞれが目指すところを知らずに、例えばSmalltalkとC++とで比較して単に動的型か静的型かといった程度の違いしか見分けられず、しょせんは同じOOP向け言語だろう…といった誤解が生じやすくなってしまった経緯も混乱に拍車をかけてしまっています。加えて、Simula 67スタイルのクラスを採用したことで、Smalltalkは本来の目的である①に反しない範囲で(あるいはそれを敢えて放棄することで)極めて限定的ながらも②のOOPもサポート可能になったことにも注意を払う必要があるでしょう。
- ①の実践の場であるSmalltalkに対して批判的に派生して考案されたのが「プロトタイプベースのオブジェクト指向」 です。当初はメッセージングを重視していましたが、シンプリシティを追求するうちにメッセージングというメタファーすら無用と排除され、オブジェクトに相当する「フレーム」と、それに値(関数を含む)の格納場所に相当する「スロット」(存在しないスロットに対するアクセスをプロトタイプ等別のフレームに委譲するための特殊なスロットを含む)だけがあればすべては事足りるという、①からはほぼ独立したアイデアやそれを基にしたスタイルとして認知されています。
- メッセージといえばカール・ヒューイットのアクター理論 を思い浮かべる人が多いかと思いますが、こちらは、アラン・ケイらのSmalltalk-72のメッセージングによるコンピューティングをヒントに、並行・並列の非同期処理にかかわる問題の解決に役立てようと考案され定式化されたものです。なおヒューイットは、彼が影響をうけたSmalltalk-72と、彼のPLANNER に影響をうけて一部仕様のみ策定され実装には至らなかった別言語のSmalltalk-71とを混同した記述が多い ため、彼によるアクターとSmalltalkの関係についての言及やそれに基づく記述は注意深く読み解く必要があります。
- 余談ですが、このカール・ヒューイットのアクター理論を読み解くために作られたのがLispの方言の最大派閥ともいえるSchmeです(※読むにはエンコードを Shift-JIS に変更できるブラウザもしくは拡張機能が必要です )。
- Erlang のメッセージングはヒューイットの考えるアクターの要件を満たしておらず 、また、Smalltalkで実践されたケイのメッセージングのアイデアとは無関係に成立したもののようです(少なくとも論文では両者への言及は無いようなので…)。ところが同言語の設計者であるジョー・アームストロングは晩年、①の真の担い手がErlangであるかのように受け取れる発言 をしていて大変興味深いです。
- ②の三要素は、カプセル化=抽象データ型の特徴、継承=クラスの特徴、多態性(ポリモーフィズム)=主に継承(≒クラスを使いこなすこと)により部分型多相が可能になること、つまり②の本質である「クラスによる抽象データ型の実現」を端的に表します。
- ②の継承については、Eiffelなど一部の実装ではクラスをデータ型として、それを継承した派生クラスを部分型として扱うことに問題が生じることが比較的早い時期に指摘されていて 、クラスやその継承でではなく新たに考案された言語機能としてのインターフェース を用いることが定着しています。
- 抽象データ型というのは、組み込み型の「具象」に対する「抽象」という意味で、前述の通り簡単には「ユーザー定義のデータ型」と言い換えられます。実装面では、データと手続きをセットにして、データの内部情報はそのセットにした手続きでしか変更できないようにする機構(カプセル化、あるいは情報隠蔽・アクセスコントロール)を「型」という視点で管理するアイデアです。クラスが提供する機能と区別がつきにくい(②がどんなアイデアだったかを思い起こせば、ある意味当然…)ですが、Simula 67に立ち返りそもそも同言語のクラスは当初、カプセル化のしくみを欠いていた(つまりクラスは元々は抽象データ型の要件を満たすべく考案されたものではない)ことや、また、抽象データ型はデータと手続きをセットにはするが、手続きを型に内包する必要はかならずしもないこと(例:CLUのクラスター。狭義、あるいは言語機能としての「抽象データ型」)などを知ると両者の区別がつきやすくなるかと思います。
- 抽象データ型や部分型多相をクラス(や、その機能である継承)を使って実現するアイデアが②のOOPなので、「抽象データ型やポリモーフィズムはOOP以外でもできる」とか「それらはOOPには必須ではない」という指摘は本末を転倒しています。
脚注
今日、A君はB君の家に遊びに行く予定です。
A「そろそろ行くか。先にB君に連絡を入れておこう」
B「・・・」
「二足歩行で行くね」とは言いません。なぜなら二足歩行は特別でもないし、普段からしていることだからです。
要するに言わんでもわかるやろということです。
—
では、こちらの犬はどのように説明しますか?
「犬という役割に飽いてしまった犬」または「二足歩行している犬」ですね。これを見て、「犬が歩いてる」と説明しても、聞き手は四足歩行を想像するでしょう。
では、「歩く」という言葉の装飾として「人が四足歩行する」は不適切で、「犬が二足歩行する」が適しているのは何故なのでしょうか。
—
我々は無意識に、会話する相手との間にある、共通認識について考えながら会話をしています。
部屋に入って来るなり,妹がぼくに話しかけてきた。①ふり返って、妹を見た。
①は主語が省略されています。さて、①の主語は何でしょうか?
正解は「僕」です。
例文から想像する図としてはこうですね。
これを次のように想像する方は「誰が振り返ったんだ?」と思うかもしれません。
厳密には、部屋の中に兄や友達などが居るかもしれませんが、むしろそれこそ省略せず説明すべき事項だと、人は無意識に考えるわけです。
要するに「なんの説明も無ぇってことは自分が振り返ったんだろうな」ということです。
つまり極端な話、B君の反応にもある通り、普段二足歩行している人が「四足歩行で行く
今日、A君はB君の家に遊びに行く予定です。
A「そろそろ行くか。先にB君に連絡を入れておこう」
B「・・・」
「二足歩行で行くね」とは言いません。なぜなら二足歩行は特別でもないし、普段からしていることだからです。
要するに言わんでもわかるやろということです。
—
では、こちらの犬はどのように説明しますか?
「犬という役割に飽いてしまった犬」または「二足歩行している犬」ですね。これを見て、「犬が歩いてる」と説明しても、聞き手は四足歩行を想像するでしょう。
では、「歩く」という言葉の装飾として「人が四足歩行する」は不適切で、「犬が二足歩行する」が適しているのは何故なのでしょうか。
—
我々は無意識に、会話する相手との間にある、共通認識について考えながら会話をしています。
部屋に入って来るなり,妹がぼくに話しかけてきた。①ふり返って、妹を見た。
①は主語が省略されています。さて、①の主語は何でしょうか?
正解は「僕」です。
例文から想像する図としてはこうですね。
これを次のように想像する方は「誰が振り返ったんだ?」と思うかもしれません。
厳密には、部屋の中に兄や友達などが居るかもしれませんが、むしろそれこそ省略せず説明すべき事項だと、人は無意識に考えるわけです。
要するに「なんの説明も無ぇってことは自分が振り返ったんだろうな」ということです。
つまり極端な話、B君の反応にもある通り、普段二足歩行している人が「四足歩行で行くわ」と言うのは適していると言うことですね。
—
ですので、例えば次のような「オブジェクト指向以外があるかもしれない」場合には「オブジェクト指向」という言葉を用いて会話することになるでしょう。
- 普段手続き形プログラミングをすることが多いベンダーに対して指示をするとき。
- 手続き形プログラムで組まれることが多いシステムを、オブジェクト指向で組んで欲しいとき。
—
ということで、ご質問への回答は、
選択肢としてオブジェクト指向以外の何かがある時に、差別化のために便利な言葉です。
としたいと思います。
—
例文出典:
まず、オブジェクト指向は object-oriented の訳語で、この orient という言葉は「方向付ける」というような意味ですが、元は日が昇る方向、すなわち東を意味する言葉だったようです。今でもオリエンタルといえば東洋的という意味になります。
object-oriented programming をそのまま解釈すると、「オブジェクトによって方向付けられたプログラミング」という意味になります。言い換えるとオブジェクトがプログラミングを方向付けているのです。オブジェクトの方を向いているという意味ではありません。しかし日本語の「オブジェクト指向」という言葉には、何が何を方向づけているのかという関係性が抜け落ちているように感じます。
ここで object は名詞です。ぼくは今までてっきり objective だと形容詞っぽいから object なんだろうと思っていましたが、改めて辞書を見てみると、そんなに簡単な話でもなさそうです。
辞書を見ると object にも objective にも名詞として「目標」とか「目的」という意味があるようです。できれば英語を母語とする人に印象を聞いてみたいところですが、オンラインの辞書ではこの二つの単語は微妙に違った定義がされています。
まず object ですが、これはみんなが知っているモノとしての意味「見たり触ったりできるもので比較的安定した形のもの
まず、オブジェクト指向は object-oriented の訳語で、この orient という言葉は「方向付ける」というような意味ですが、元は日が昇る方向、すなわち東を意味する言葉だったようです。今でもオリエンタルといえば東洋的という意味になります。
object-oriented programming をそのまま解釈すると、「オブジェクトによって方向付けられたプログラミング」という意味になります。言い換えるとオブジェクトがプログラミングを方向付けているのです。オブジェクトの方を向いているという意味ではありません。しかし日本語の「オブジェクト指向」という言葉には、何が何を方向づけているのかという関係性が抜け落ちているように感じます。
ここで object は名詞です。ぼくは今までてっきり objective だと形容詞っぽいから object なんだろうと思っていましたが、改めて辞書を見てみると、そんなに簡単な話でもなさそうです。
辞書を見ると object にも objective にも名詞として「目標」とか「目的」という意味があるようです。できれば英語を母語とする人に印象を聞いてみたいところですが、オンラインの辞書ではこの二つの単語は微妙に違った定義がされています。
まず object ですが、これはみんなが知っているモノとしての意味「見たり触ったりできるもので比較的安定した形のもの (anything that is visible or tangible and is relatively stable in form)」が先に来ます。そして次に「何らかの行動や考えが向かう先にある物、人、事 (a thing, person, or matter to which thought or action is directed)」という説明がされています。(Dictionary.com より。以下も同じ。)
次に objective ですが、これはもう少し具体的に「努力や行動によって得られる、または達成できる事が意図されている何らかの物。目的、ゴール、目標 (something that one's efforts or actions are intended to attain or accomplish; purpose; goal; target)」という風に書かれています。
上の説明を信じるならば、objective という言葉には行動を伴うような少し重い意味が含まれるようです。実際 objectives-oriented という言葉も存在するようですが、これはビジネスでの成果目標や成果主義のような考え方を指すようです。(いま Google で検索して初めて知りました。)
上で見たように、object という言葉にも目的とか対象物のような意味がありますので、一概に「モノ」か「目的」かのどちらかに決めてしまうことはできないように思います。それに、単なるモノなら thing や body でもいいはずです。
ところでもう一つ忘れてはいけない、コンピュータプログラミングに関わる重要な「オブジェクト」が存在します。それは「オブジェクトコード」です。
最近の開発環境ではあまり意識することがないかもしれませんが、C や C++ でプログラミングすると、ビルドの際に「コンパイル」と「リンク」の 2 段階を経ています。
オブジェクトコードとはコンパイルの際に「コンパイラが出力する何か」です。他方、コンパイラに対する入力は何かと言うと「ソースコード」です。source とは何かが出てくる源です。ということはその反対側にあるオブジェクトコードは最終的な目的とする物、という雰囲気が自然に感じられますよね。
ではもう一度オブジェクト指向に話を戻すと、オブジェクトとは「メッセージの受け手(レシーバー)」と考えることもできます。Smalltalk という言語ではデータにまつわるあらゆる操作は全て「メッセージ式」という形で表されています。(Objective-C はこの Smalltalk の考え方を C の上に乗っけたものです。なお、Objective-C の Objective は名詞ではなく形容詞だと解釈するのが自然だと思います。)
もちろんそこに何らかの実体としてのデータがあることを前提としていますが、単なるモノというよりも目標、対象という意味が含まれているように感じないでしょうか。
もっというと、C++ のオブジェクトは必ずしもメモリ上に一定のデータを保持する必要すらありません。C++ のメソッド呼び出しは、実行時まで型が決定されない場合を除けば、コンパイル時にオブジェクトの型とメソッドによって実行されるコード(関数)が決定され、必ずしも this ポインタが何かを指しているとは限りません。
デバッガでプログラムを追ってみると、this ポインタが見当たらなくて悩んだことがある人も多いのではないでしょうか。そうなると、メソッドを呼ぶ対象としてのオブジェクトは、実体はなく方向付けだけをしていると考えることもできそうな気がしますが、どうでしょうか?
ある言葉を別の言葉に翻訳する時、ある訳語が正しくて別の訳語が間違っていると言い切ることはできない事が多いように感じます。例えば「すみません」を英語で言う時、“I’m sorry” か “Excuse me” と言います。これらは少し意味合いが違うので、英語で話す時は使い分けますが、日本語で「すみません」というとき「これは謝罪の意味です」「これは注意を喚起する意味です」といちいち考えませんよね?
英語の “I’m sorry” は「ごめんなさい」の意味と「お気の毒に」の意味があります。日本人には全然違う意味のことを同じ言葉で言っているように思えますが、英語話者にとってはそれなりに近い意味に感じるからこそ同じ音葉が使われるのだと思います。
コンピュータのプログラムである以上、あらゆるモノはただ存在するだけではなく、常に何らかの操作の対象であるはずです。(操作の対象にならない、またはなり得ないメモリはゴミと呼ばれます。)仮にオブジェクトに目的という意味があったからと言って、モノというのは誤訳だ、なんて言わなくていいと思います。もっと気楽に構えた上で、問題の本質に向き合う事が大事です。
この件については、すでに18年も前に Matzにっき(2003-08-06) で書いたことが全てだと思います。
つまり、継承をisa関係にしか使わないという原則に従っていれば問題はほぼなく、逆に一見使い勝手が良さそうに思えるからと言って、isa関係でない機能取り込みに継承を使うと痛い目に遭うということではないでしょうか。
Javaのような(C++からの流れで)抽象データ型から生まれたオブジェクト指向プログラミング言語では、クラスが単なるモジュールに見え、継承が他のモジュールからの機能取り込みに見えるのかもしれません。その延長で単なる機能取り込みに継承を使い、コードが複雑化してしまった上で、「羹(あつもの)に懲りて膾(なます)を吹く」状態が上記の継承忌避なのだと推測します。
継承を使うべきときには継承を使い、そうでない時にはそれ以外の手段(コンポジションとか)を使うのが正しい態度だと思います。
以前書いた投稿があります。
オブジェクト指向のプログラミングの問題点は何ですか?に対する山本 聡 (Satoshi Yamamoto)さんの回答
が、その回答は長ったらすぎるので、短めに書くと、
「オブジェクト指向のプログラムは難しいと言われる理由」は、
やれることがいろいろあり、また、それによって起きる制限もいろいろありすぎて、わけがわからない、から、だと思います。
継承だのインターフェースだの多態だのなんだのかんだのといろいろありすぎです。
そしてこれら全てが、どうでもいい概念です。
ifの分岐やforのループや、処理共通化、データを得て、表示を行う。プログラムの重要な概念ってこういうものだけですし、これで多くのアプリケーションの機能を作り出すことができるはずですが、
オブジェクト指向の概念というか、それらの機能はアプリケーションを作るという本質とはかけ離れた機能なので、どう使ったらいいのか本当のところが誰にもわからない、のでアレコレの派閥があってアレコレの議論が行われるので、決まった道がないために学ぶ人は苦労するということになったりします。
自転車置き場の議論として知られる『原子力発電所の建設ミーティングのときに、より重要なことは難しくて議論の対象にはならず発電所の自転車置き場の屋根の色はどうしようかと議論に花が咲く』という例え話がありますが、
オブジェクト指向であーだのこーだの言っているのは
以前書いた投稿があります。
オブジェクト指向のプログラミングの問題点は何ですか?に対する山本 聡 (Satoshi Yamamoto)さんの回答
が、その回答は長ったらすぎるので、短めに書くと、
「オブジェクト指向のプログラムは難しいと言われる理由」は、
やれることがいろいろあり、また、それによって起きる制限もいろいろありすぎて、わけがわからない、から、だと思います。
継承だのインターフェースだの多態だのなんだのかんだのといろいろありすぎです。
そしてこれら全てが、どうでもいい概念です。
ifの分岐やforのループや、処理共通化、データを得て、表示を行う。プログラムの重要な概念ってこういうものだけですし、これで多くのアプリケーションの機能を作り出すことができるはずですが、
オブジェクト指向の概念というか、それらの機能はアプリケーションを作るという本質とはかけ離れた機能なので、どう使ったらいいのか本当のところが誰にもわからない、のでアレコレの派閥があってアレコレの議論が行われるので、決まった道がないために学ぶ人は苦労するということになったりします。
自転車置き場の議論として知られる『原子力発電所の建設ミーティングのときに、より重要なことは難しくて議論の対象にはならず発電所の自転車置き場の屋根の色はどうしようかと議論に花が咲く』という例え話がありますが、
オブジェクト指向であーだのこーだの言っているのはこの自転車置き場の議論に近いことが起きます。誰もが大好きな議論ですが、そこにはソフトウェアの機能としての本質はなく、正解もないのでいつまでたっても議論が終わりません。
「クラスごとに纏まっていて見やすいように思えます」というのは
何に対して見やすいのか、ということですが、クラスにまとまるのはグローバルに散らばっているのよりかはマシなのですが、クラスメソッドを書くという時点で、クラスは小さなグローバル領域になっているだけなので、
グローバル変数とかグローバル関数とかだけでアプリケーションを作るのがなかなか困難なのと同じように
アプリケーションが複雑化してクラスがどうしても肥大化しなければいけない時に、一気に複雑さがまして、他人のコードを読むことが困難になるのが、オブジェクト指向です。
クラスの責務がなんたらかんたらと言い出す人もいるのですが、まあ終わらない議論に巻き込まれるだけになりますね。
ということで、副作用がない(あるいは少ない)、参照透過性が高くてテストしやすい関数型プログラミングが流行ってきているようです。
こちらは小さなグローバル領域などは作らないのでアプリケーションの肥大化に対して非常に強く複雑さを隠蔽する仕組みになります。
関数型プログラミングというのも、それなりにあやふやな概念なので議論を呼ぶみたいですが。そのあたりは詳しくないので、どこか他のQuoraで答えをみつけてください。
関数型というか昔ながらの構造化プログラミングですね。
オブジェクト指向を捨てた所に正解があるとは思ってもみませんでした。私、業界20年くらいというかプログラミング歴20年くらいですが、ようやく気が付きました。
ありがたいことに気がついてからは、オブジェクト指向で複雑にからまった状態のプログラムであっても、リファクタリングしたりしてもより簡単なコードをどんどん書けるようになって楽ですよ。
逆風というわけではないのですが、オブジェクト指向プログラミングが不要なケースが明確になったのだと思います。
- Webというステートレスな処理を扱う場合、オブジェクト指向で実装する意義が若干下がった。
- 分散処理などを書く場合、オブジェクト指向的な実装より関数型プログラミング的な指向の方がマッチしている(ことが多い)。単一のプログラムでもコア数の増加とともに、分散処理で実装する意義が増えてきている。
- DBの性能向上・ORMの機能の多様化でオブジェクト指向が以前は担当していた領域がDBレイヤーにいったことで相対的に価値が下がった。
オブジェクト指向はステートフルな処理系を書く場合に情報をカプセル化することで、堅牢なプログラムを実装するベストプラクティスです。まだまだ、この実装方法がマッチするシステムはたくさんあります。
一方、オブジェクト指向で実装しないほうが良いケースも見えてきました。特に分散処理では、In/Out以外の依存性を持たないストリーム指向がマッチするため、関数型で実装した方がきれいに実装できるケースが多いと思います。
プログラマーであれば、基本すべてツールですから、いろんな方法で実装できるようにしたいものです。
まず実装の継承は嫌いです。一般にも批判が多く、使われなくなくなってきています。新しい言語では特に。
次に、クラスが嫌いです。役割りを負いすぎています。
インスタンスメソッドが嫌いです。継承を使わない前提ではvtableは無用です。thisへの暗黙の依存を伴う、リファクタリングや関数合成をしにくくする扱いにくい存在です。
すべてがオブジェクトという考え方は嫌いです。「メソッド後置の統一」という見た目の効果を求めているなら、それならなぜ中置オペレータがあるのか。中置オペレータにしたならしたで、それは算術演算として交換法則にしたがうべきなのにメソッド選択権限がレシーバのみにあるのが嫌い(マルチメソッド/ダブルディスパッチが簡潔に表現できてるなら良し)。またどうせ静的型のジェネリクスや型変数はオブジェクトではあつかいきれない。(コンパイルタイムメッセージパッシング、てあるかな)
可変のインスタンス変数は嫌いです。オブジェクトが状態もつということを、あまりにも軽率に可能にしてしまいます。
モックとか嫌いです。滅んでほしいです。DIはもっと嫌いです。面倒くさいからです。
単一責任原則は嫌いです。早すぎる最適化であり、未来の「ひとつの理由」に常に対応できるという傲慢さを感じるからです。「マイクロサービス細かく分けすぎる厨」に通じるものを感じます。
開放封鎖原則は、クラス継承を使わない場合に何を意味するのだろう
まず実装の継承は嫌いです。一般にも批判が多く、使われなくなくなってきています。新しい言語では特に。
次に、クラスが嫌いです。役割りを負いすぎています。
インスタンスメソッドが嫌いです。継承を使わない前提ではvtableは無用です。thisへの暗黙の依存を伴う、リファクタリングや関数合成をしにくくする扱いにくい存在です。
すべてがオブジェクトという考え方は嫌いです。「メソッド後置の統一」という見た目の効果を求めているなら、それならなぜ中置オペレータがあるのか。中置オペレータにしたならしたで、それは算術演算として交換法則にしたがうべきなのにメソッド選択権限がレシーバのみにあるのが嫌い(マルチメソッド/ダブルディスパッチが簡潔に表現できてるなら良し)。またどうせ静的型のジェネリクスや型変数はオブジェクトではあつかいきれない。(コンパイルタイムメッセージパッシング、てあるかな)
可変のインスタンス変数は嫌いです。オブジェクトが状態もつということを、あまりにも軽率に可能にしてしまいます。
モックとか嫌いです。滅んでほしいです。DIはもっと嫌いです。面倒くさいからです。
単一責任原則は嫌いです。早すぎる最適化であり、未来の「ひとつの理由」に常に対応できるという傲慢さを感じるからです。「マイクロサービス細かく分けすぎる厨」に通じるものを感じます。
開放封鎖原則は、クラス継承を使わない場合に何を意味するのだろうと思っちゃいます。
その他、オブジェクト指向設計原則のそれぞれは、オブジェクト指向を使わなければもともと難なく解決できる場合も多いと感じます。
好きなところは…上のような知見を10年ぐらいかけて生みだせたところでしょうか。
Ueharaさんがなぜjavaは純粋なオブジェクト指向プログラミング言語ではないですか?に対するQuoraユーザーさんの回答で書いている通りに、純粋なオブジェクト指向言語ならいくらでもあるので、Javaが純粋でないというのは、「やり方さえ知っていれば純粋なオブジェクト指向言語にできたはずなのに、ちょっと間違った意思決定をしてしまって、オブジェクトでないものをシステムや言語の中にいれてしまったから」ということが理由です。
純粋だと言われるSmalltalkの場合、Booleanがオブジェクトなのはもちろんですが、「コンピューター全体」がオブジェクトの集合として表現されているわけです。「クラス」、「メソッド」、「メタクラス」と言ったものももちろん、クラスの集合を格納しておく「名前空間」と言ったものも当然ながらオブジェクトで、それらは実行環境の中で直接操作することができます。さらには、その実行環境もまたオブジェクトの集合なのです実行状態を表す「スタックフレーム」もオブジェクトであり、例えば「継続」を実装したければ、スタックをコピーして保持しておき、後でreturn:やresume:といったメソッドを呼び出して復帰したりもできます。
このことから導き出される最も重要な要素は、「論理的にはシステムの実行ということそのものが自分自身で記述されている」ということです。あるオブジェクトにメッセージ・オ
Ueharaさんがなぜjavaは純粋なオブジェクト指向プログラミング言語ではないですか?に対するQuoraユーザーさんの回答で書いている通りに、純粋なオブジェクト指向言語ならいくらでもあるので、Javaが純粋でないというのは、「やり方さえ知っていれば純粋なオブジェクト指向言語にできたはずなのに、ちょっと間違った意思決定をしてしまって、オブジェクトでないものをシステムや言語の中にいれてしまったから」ということが理由です。
純粋だと言われるSmalltalkの場合、Booleanがオブジェクトなのはもちろんですが、「コンピューター全体」がオブジェクトの集合として表現されているわけです。「クラス」、「メソッド」、「メタクラス」と言ったものももちろん、クラスの集合を格納しておく「名前空間」と言ったものも当然ながらオブジェクトで、それらは実行環境の中で直接操作することができます。さらには、その実行環境もまたオブジェクトの集合なのです実行状態を表す「スタックフレーム」もオブジェクトであり、例えば「継続」を実装したければ、スタックをコピーして保持しておき、後でreturn:やresume:といったメソッドを呼び出して復帰したりもできます。
このことから導き出される最も重要な要素は、「論理的にはシステムの実行ということそのものが自分自身で記述されている」ということです。あるオブジェクトにメッセージ・オブジェクトが送られた時に、対応するメソッド・オブジェクトを探し出し、それを起動するためにスタックフレーム・オブジェクトを割り当てて、メソッドが指定する動作に応じてそのフレームに値を書き込んだり読みだしたりすることが言語自身で記述されたプログラムとして行われているかのようになっているわけです。
ただ、もし本当にこのように動作しているのだとすれば、他の人の回答にある「遅いのではないか」という想像に基づく懸念も理解できるのですが、実用的な速度で動かすために、実際には上記の動作をより高速に行うためのインタープリター「も」用意されています。実行されているコードが上記のようなメタ機能を司るオブジェクトにアクセスしない限りは、それらのオブジェクトを実際に生成する必要はなく、インタープリターは諸処の工夫で高速化することができます。結果的には、一般的に使われているSmalltalkの処理系はPythonやRubyのものよりも何倍も高速です。もともとグラフィックスやリアルタイム音声処理をしようとしていた人々が作っていた言語ですので、それらをこなせるようになっているわけです。
別の言い方をすれば、他の言語で文字列処理やデータ処理のためにネイティブ・メソッドを書き、言語自体で書かれたコードの代わりに外部のものを呼び出すことによって同じ機能をより高速に提供できる、というところで、Smalltalkはそのようなデータ処理だけではなく言語実行全体をネイティブメソッドのように高速化したものも用意されていると言っても良いかもしれません。
tracing JITを用いたような実装系であれば、全てがオブジェクトであり、Cで書かれたようなプリミティブをなるべく必要としないということが速度向上の役に立ちます。近年流行っている技術からの類推で言えば、ウェブブラウザのDOMオブジェクトの動作はC++で書かれていて、もしJavaScriptからそれらのオブジェクトのプロパティに値を代入すると、 C++で書かれたコードが内部で動作するというようになっていますよね。React.jsなどが使っているvirtual DOMは、その部分を仮想化してJavaScriptで書いているというわけなのですが、このテクニックが効果的である一つの理由は、C++で書かれたDOMオブジェクト操作の部分を通らないJavaScriptの「実行トレース」を長くすることができるために、JITコンパイラがより効果的になるから、という面があります。Smalltalkですべてをオブジェクトにするというのは、わざわざvirtual DOMのようなものをあえて実装しなくてももともと全部がvirtual DOMみたいなものだったと言えるわけですし、tracing JITでなくても少ない機能の高速化に注力するだけで全体が早くなるという効果もあります。
システムとしては、「マウス」、「ディスプレイ」というものもオブジェクトであり、「ディスプレイ」のピクセル値を書き換えることにより画面を描画しますし、何が表示されているのかもピクセルを読み出すことにより確認できます。こちらもBitBltと呼ばれる高速化ルーチンがあります。理屈の上ではユーザーは何も気にしないでコードを書けばいつのまにか、Cで書いたのと同じようなパフォーマンスのグラフィックス操作ができる、という建前ですが、この辺はさすがに実際にどのようになっているのか知らないと速度は出ませんが。
別の方の回答にはメモリを使うのではないかというまた想像に基づいた回答がありますが、こちらもまた事実とは違います。もともと128kバイトしかメモリがなかったAltoコンピューターで動いていたということを忘れないでください。2019年現在でも、開発環境も全て含めたものでも50MBもあればゆうゆう動きます。大事なのは非常に小さなVMを使い、Arrayなどのメソッドも全てコンパクトなバイトコードとして保持できているということで、マシンコードとして保持するよりもメモリ効率が良いということです。2000年ごろにはDisneyがテーマパークでお客さん向けの携帯電子機器を使ったガイドを作ろうと実験していた時も、Smalltalkで書かれたプログラムだったからこそ動かすことができた、というような話もありました。「すべてがオブジェクト」だからこそメモリ効率がよくなっているわけですし、以下の論文に書かれているようにメモリーチップに不具合があり端末上で予期せぬエラーが発生した時にも、クラッシュせずにインタラクティブなデバッガーがエラーをハンドルできたために端末上でデバッグできた、というようなエピソードもあります。
http://www.vpri.org/pdf/tr2003002_parkspda.pdf
まとめると、もともとの質問である「なぜJavaは純粋なオブジェクト指向言語でないのか」といえば、もし純粋なオブジェクト指向言語というのであればせめて上記のようなレベルくらいにはなっていてほしいというところ、Javaではオブジェクトとして表現されていない要素があるから、ということになるでしょう。
オブジェクト指向プログラミングにおいて一番難しいのが継承の使い方です。安易に使うと、コードを読みにくくし、保守性を悪くします。
本当は継承はない方がいいくらいなのですが、ほとんどの言語では継承がサポートされているため、継承に向き合う必要があります(特にPython)。
フレームワークが提供するクラスを継承して使う場合ではなく、他の人に継承して使ってもらう基底クラスを設計する場合、自分は次の2つを心がけてます。
- 目的を明確化し、やれることを制限する。
- 階層を深くしない(多段継承を避ける)。
まず、基底クラスを作るときは、目的を明確にし、使える場面と使えない場面を明確化します。「基底クラスは大体使えるけど一部はそのまま使えないから継承して差分を実装する(オーバーライド)」のは非常に危険です。
「大体使えるけど一部はそのまま使えないから継承して差分を実装する」のはほとんどの場合場当たりのつぎはぎです。本来は、基底クラスを改善して、つぎはぎではなく、きちっと差分を定義して、Templateパターンを使うのがいいでしょう。
しかし現実的には非常に難しいです。なので、そういうときは他の人に「基底クラスを継承せずに別のクラスを作れ」と言っています。
もちろんロジックが重複しますが、重複したコードの方が解決が容易です。また経験上「たまたま同じ実
オブジェクト指向プログラミングにおいて一番難しいのが継承の使い方です。安易に使うと、コードを読みにくくし、保守性を悪くします。
本当は継承はない方がいいくらいなのですが、ほとんどの言語では継承がサポートされているため、継承に向き合う必要があります(特にPython)。
フレームワークが提供するクラスを継承して使う場合ではなく、他の人に継承して使ってもらう基底クラスを設計する場合、自分は次の2つを心がけてます。
- 目的を明確化し、やれることを制限する。
- 階層を深くしない(多段継承を避ける)。
まず、基底クラスを作るときは、目的を明確にし、使える場面と使えない場面を明確化します。「基底クラスは大体使えるけど一部はそのまま使えないから継承して差分を実装する(オーバーライド)」のは非常に危険です。
「大体使えるけど一部はそのまま使えないから継承して差分を実装する」のはほとんどの場合場当たりのつぎはぎです。本来は、基底クラスを改善して、つぎはぎではなく、きちっと差分を定義して、Templateパターンを使うのがいいでしょう。
しかし現実的には非常に難しいです。なので、そういうときは他の人に「基底クラスを継承せずに別のクラスを作れ」と言っています。
もちろんロジックが重複しますが、重複したコードの方が解決が容易です。また経験上「たまたま同じ実装だった」だけで、後から変わるケースも多いです。
継承が破綻する原因の多くは、継承が深くなり、ロジックが分散してしまうからです。なので自分は次の3階層までに制限しています。
- ルートクラス、あるいはフレームワークが提供する、安定したクラス
- 自前で実装する基底クラス
- 基底クラスから派生するクラス
理想的な例はJavaのDate and Time APIです。これほとんど継承を使っておらず、多段継承は全くしていません。finalを付けているので、これらのクラスからの継承が不可能です(ただし例外クラスは例外的に多段継承が必要です)。
多重継承は忌むべきものとされていますが、多段継承に比べるとまだマシです。インタフェースの継承か、mixinでいいです。mixinの例はRubyのEnumerableで、全てeachを用いて実装されています。
ダイヤモンド継承問題というのもあるので、これも知っておいてください。
多重継承の問題はこれくらいです。注意は必要ですが、避けるのは難しくありません。それよりも、多段継承に注意してください。
他の回答にもあるように、両者は排他的なものではないので苦に合わせるやり方も色々あります。
ただ、関数型プログラミングの「副作用がない」ことを突き詰めたとしても、もし「コンピューター全体」を自分のプログラムで記述したのだとすれば、どこかではデータの書き換えを行うことにはなります。マウスポインターの位置を順次読み込んで画面に絵を描く、というようなプログラムを作っているとして、新しいマウスポインターの位置が取得されたからといって、その位置を表す新しいイミュータブルなメモリ素子を作ったり、画面の画素を毎回新しく作ったりはできませんからね。
となると、コンピューター上のどこかではデータの書き換えが起こっているわけなのですが、そのような書き換えの相互作用がなるべく起こらないようにモジュール化する技法として、オブジェクトというものがあって、書き換えがあるとしてもそのオブジェクトの責任として行うように記述するという手法はとても有用です。
最初のオブジェクト指向言語と言われている、50年以上前に作られ始めたSmalltalkというシステムでは、入力デバイスや画面といったものもオブジェクトとして表現されていました。そのシステムの設計者たちはLisp的な関数型言語にも通暁していたので、「よくデザインされたオブジェクト指向プログラミングであれば、なるべく多くのメソッドは、メソッドの引数とインスタンス変数の現在の
他の回答にもあるように、両者は排他的なものではないので苦に合わせるやり方も色々あります。
ただ、関数型プログラミングの「副作用がない」ことを突き詰めたとしても、もし「コンピューター全体」を自分のプログラムで記述したのだとすれば、どこかではデータの書き換えを行うことにはなります。マウスポインターの位置を順次読み込んで画面に絵を描く、というようなプログラムを作っているとして、新しいマウスポインターの位置が取得されたからといって、その位置を表す新しいイミュータブルなメモリ素子を作ったり、画面の画素を毎回新しく作ったりはできませんからね。
となると、コンピューター上のどこかではデータの書き換えが起こっているわけなのですが、そのような書き換えの相互作用がなるべく起こらないようにモジュール化する技法として、オブジェクトというものがあって、書き換えがあるとしてもそのオブジェクトの責任として行うように記述するという手法はとても有用です。
最初のオブジェクト指向言語と言われている、50年以上前に作られ始めたSmalltalkというシステムでは、入力デバイスや画面といったものもオブジェクトとして表現されていました。そのシステムの設計者たちはLisp的な関数型言語にも通暁していたので、「よくデザインされたオブジェクト指向プログラミングであれば、なるべく多くのメソッドは、メソッドの引数とインスタンス変数の現在の値を使って関数的に値を計算するだけとし、メッセージを受け取ってインスタンス変数を書き換えるときも、最後のところで計算された結果を一括してインスタンス変数に書くようにするのが良い」という知見は持っていました。そのようなある意味当たり前の"best practice"が伝わらず、一つの値だけを直接変更するような"setter"が後々よく書かれてしまい、外からそのように書き換えが行えるようなひとまとめにしたデータでしかないものが「オブジェクト」と呼ばれるようになったために混乱をきたした、という経緯があります。
関数型プログラミングのメリットのひとつとしては「高階関数」、つまり関数そのものを受け渡してプログラムを書けるようにするというものがあります。高階関数ももちろんSmalltalkのかなり初期バージョンに近いものから存在しており、mapやreduceやfilterに該当する機能も当たり前に準備されていました。ただ、こちらも引数として渡す「ブロック」や関数は、本来インスタンス変数の書き換えをするようなものを渡すべきではない、という知見もありました。それはオブジェクトが自分の内臓を切り取って「好きにしてくれ」と他のオブジェクトに渡しているようなものである、というような喩えもしばしば使われていました。ただ、このようなスタイルもあまり深く考えられることなく、値の書き換えをするようなブロックを他のオブジェクトに渡すようなパターンもしばしば使われていましたが、これもあまり望ましくないスタイルだと言えます。
ちょっと長くなってしまいましたが、結論としてはオブジェクト指向言語という肩書きがついた言語が作られていた初期から、その前から存在していた関数型プログラミングとの良いところどりをしてプログラムを書く、という実装は存在した、ということになります。そして、ちゃんとした原則に従ってプログラムを書けばとても強力なプログラムが簡潔に書ける、ということでもあります。
嫌いというか問題と思うのは、さじ加減を越えて使うと害のほうが大きくなるのに、やり過ぎかどうかをわかるための指針を思想の中に含んでいないところです。ほかの「なんとか志向」もだいたいそうだと思いますが、オブジェクト指向は特に広く普及したので、害が大きくなっていると思います。さじ加減がわからないプログラミングの経験が少ないエンジニアは、常に多いので。
最近は、オブジェクト指向を多用しはじめて30年ぐらいたって、ようやく、副作用がだいぶはっきりしてきたかなと思います。 もうすぐ、副作用を避けながらうまく使う手段が言語化でき、共有可能になるんじゃないかな、と思ってます。そうなれば、経験が少ない人も、誤用を減らせる、、といいなあ。
近いうちに、といってもあと10年20年かかるでしょうけど、この壮大な実験の結果が総括されるのだと思います。
好きなところは、さじ加減を間違わずに設計できたら、すごく綺麗に決まることですね。 Java、Ruby、C#、JavaScriptなどを使った開発で、いい感じにAPIを整理できたときはとても嬉しいです。
オブジェクト指向プログラミングの構成要素ごとの有用性が再吟味され、良し悪しもわかってきて、是是非非で悪いところは適宜置きかえていった方がいいのでは、となってきたとき、「オブジェクト指向プログラミング」という言葉で指す総称的概念として扱うことの意義が侵食されてきているのではないでしょうか。「熟成して普及し、名前を呼ぶ必要がなくなった」というような状況に向かっているのかもしれません。
たとえば、昨今、「構造化プログラミング」ってわざわざ言いませんよね。「アスペクト指向プログラミング」という鳴り物入りででてきたやつが、今は「AspectJ使ってます」「DI使ってます」ぐらいで済むことになっています。DIコンテナなんて言うこともなくなりましたかね。Spring Boot使ってます、でおしまいです。
なので、クラスならクラス、継承なら継承、オブジェクトならオブジェクトという言語機能名称を、パーツとしてそれぞれ使うときにそう呼べば良いような気がします。「オブジェクト指向プログラミング」と総称して言う必要って、あるのかな、と。あるとしてそれって実体として何だっけかなと。
たとえば、自分では、おおむねクラスライブラリを呼びだしたりはするし(String#toUpperCase()とか)、ときおり自前のデータ構造のためにクラス定義もします。まれに継承もします。でも関数型でmapとfilterを連ねる方がよ
オブジェクト指向プログラミングの構成要素ごとの有用性が再吟味され、良し悪しもわかってきて、是是非非で悪いところは適宜置きかえていった方がいいのでは、となってきたとき、「オブジェクト指向プログラミング」という言葉で指す総称的概念として扱うことの意義が侵食されてきているのではないでしょうか。「熟成して普及し、名前を呼ぶ必要がなくなった」というような状況に向かっているのかもしれません。
たとえば、昨今、「構造化プログラミング」ってわざわざ言いませんよね。「アスペクト指向プログラミング」という鳴り物入りででてきたやつが、今は「AspectJ使ってます」「DI使ってます」ぐらいで済むことになっています。DIコンテナなんて言うこともなくなりましたかね。Spring Boot使ってます、でおしまいです。
なので、クラスならクラス、継承なら継承、オブジェクトならオブジェクトという言語機能名称を、パーツとしてそれぞれ使うときにそう呼べば良いような気がします。「オブジェクト指向プログラミング」と総称して言う必要って、あるのかな、と。あるとしてそれって実体として何だっけかなと。
たとえば、自分では、おおむねクラスライブラリを呼びだしたりはするし(String#toUpperCase()とか)、ときおり自前のデータ構造のためにクラス定義もします。まれに継承もします。でも関数型でmapとfilterを連ねる方がよっぽどロジックの中核だったりするので、「自分、今オブジェクト指向プログラミングしてるんだっけ?」と思います。ひょっとすると「たまにクラス定義してるだけ」じゃないか? と。
時間の問題、とまでは言えなさそうですが、じわじわとそうなっていくように思います。
将来的には、おそらく。
オブジェクト指向の中でも特に複雑なクラスベースの継承と多態、それがまずなくなっていくでしょう。
(これがなくなればもはやオブジェクト指向プログラミングとは呼べない気もします。)
継承がない言語がいくつかメジャー言語で出てきているので、継承や多態を使ったコードは移植性が悪くなってしまいます。また、移植性は除いても継承は全てのフィールドメソッドを継承してしまうという辛さがある機能で代替手段もあるため、書かれなくなっていくでしょう。
継承がない言語が登場して広く使われているのも、継承が必須ではない機能だったからです。
言語特性やライブラリ特性があるのですが、JavaScriptやTypeScriptでReactとか使っている開発者はどんどん増えていますが、それらを使っていると、クラスベースのオブジェクト指向(継承、多態)は、使う場面がまずないので使わないです。
使わないでも普通にアプリとか作れるもんですよ。
ECサイトエンジンとか、Webホワイトボードお絵かきツールとか、あと、何作ったっけ、いろいろ作ってますが、もうここ5年くらいは、クラスってのは使ってない気がします。thisの微妙すぎる挙動とか、以前は必死で学んだ気がしますが、使わないのでもう忘れました。
他の人がクラスベースで作ったコードを修
時間の問題、とまでは言えなさそうですが、じわじわとそうなっていくように思います。
将来的には、おそらく。
オブジェクト指向の中でも特に複雑なクラスベースの継承と多態、それがまずなくなっていくでしょう。
(これがなくなればもはやオブジェクト指向プログラミングとは呼べない気もします。)
継承がない言語がいくつかメジャー言語で出てきているので、継承や多態を使ったコードは移植性が悪くなってしまいます。また、移植性は除いても継承は全てのフィールドメソッドを継承してしまうという辛さがある機能で代替手段もあるため、書かれなくなっていくでしょう。
継承がない言語が登場して広く使われているのも、継承が必須ではない機能だったからです。
言語特性やライブラリ特性があるのですが、JavaScriptやTypeScriptでReactとか使っている開発者はどんどん増えていますが、それらを使っていると、クラスベースのオブジェクト指向(継承、多態)は、使う場面がまずないので使わないです。
使わないでも普通にアプリとか作れるもんですよ。
ECサイトエンジンとか、Webホワイトボードお絵かきツールとか、あと、何作ったっけ、いろいろ作ってますが、もうここ5年くらいは、クラスってのは使ってない気がします。thisの微妙すぎる挙動とか、以前は必死で学んだ気がしますが、使わないのでもう忘れました。
他の人がクラスベースで作ったコードを修正するときは仕方なく作る場合があったりするくらいです。
継承がなくなったあとはオブジェクト内のフィールドを使ったメソッドがなくなっていってほしいです。
メソッドは副作用があるためにコードの読み取りしにくくなり、テストも書きにくいので、
状態は全部引数として渡す形式の関数にしてメソッドを減らしていくと、もっとプログラムはシンプルになり、より生産性が高まります。
なので、私が書くプログラムは超読みやすいです。でもそういうプログラム書くと、誰にでも容易に引き継ぎしてもらえるので技術がないと思われるので、なかなか世渡りは難しいでんな。
メソッドありきのプライベート変数とかも減っていくとヨシです。
こうなるとますますオブジェクト指向から遠ざかり、オブジェクトは単なる階層データ構造になり状態の保存復帰も単純でやりやすくなります。
自分がクラスベースのオブジェクト指向言語を使っていたときの事をよーく思い出して、改めて考えてみたのですが、
何らかのツリー型のデータ構造を作るときにオブジェクト内にオブジェクトを持たせるのが普通で、連想配列内に連想配列をもたせたとしても、連想配列自体がDictionaryクラスだったりするので、どうしてもオブジェクトを意識する必要があるので、なかなか、オブジェクト指向を使わないといっても「それは無理!」となるんだとは思うのですが、
その階層型のデータ構造を維持する仕組みは、オブジェクト指向とは区分けして考えてもいいんじゃないかと思います。
構造体で階層データ構造をつくれなくはなさそうですが、その方が厄介ですよね。なので、階層型データ構造にクラスとかオブジェクトはつかわざるおえない。
オブジェクト指向が厄介なのは、
1. 継承(と多態)
2. オブジェクトがメソッドを持てる
3. 階層型データ構造
これらが全部オブジェクト指向に含まれるという所があって、
1.2.を使うのをやめていくと、オブジェクト指向から脱することができるかな、と考えています。
3.を捨てるには、階層データをオブジェクト以外で表現できたらいいですが、それは困難なので、まあ、そこは既存のままでいいだろうという所かなと。
JSがプログラミング言語としてよかったのは、オブジェクトの生成が「{}」だけでできて、ゆえにJSONが即座に階層データとしてオブジェクトになるということ、そしてGCでオブジェクトの破棄など全く何も考えずに済むので、階層データを即時に定義したりして破棄など考えずにコード組めるところもよかったのだろうな、と改めて思いました。
平野 雄一 さんからコメント頂いたので追記します。
ゲームのキャラクターや弾はインスタンス?
ということだったのですが、私はゲーム分野のプログラマーではないのですが、かなりシンプルなゲームの実装としてこれは参考になります。
純粋な JavaScript を使ったブロック崩しゲーム - ゲーム開発 | MDN
単純なフラグと当たり判定になっています。
でも、別にこれがブロックごとにオブジェクトのインスタンスと球がインスタンスでも、それは複雑になりようがないので、問題ないです。
オブジェクト指向のオブジェクトの生成(インスタンス)とか、とインスタンスがインスタンスを所有して、そのインスタンスも複数のインタンスからできている、というデータの階層構造的なオブジェクトの使い方は普通で、それは素直なプログラミングだと思います。
ツリー型のデータ構造としてのオブジェクトの利用については複雑なことはないので、有害だとは思わないです。
「オブジェクト指向」自体が様々な解釈をされ、その指すものが各自バラバラなのでなんとも言い難いところがありますが、自分なりの答えを。
ちなみに自分は言語的にはObjective-C、Java、Rubyあたりを意識していて(あとSmalltalkは使ったことないけど先駆者として)、C++には否定的です。
質問の答えを一言でいうと、構造化プログラミングだけでは不十分だからです。
構造化プログラミングと、オブジェクト指向プログラミングの一番の違いは、ここで書いたように、メソッドを動的に解決するか否かだと考えています。C++はデフォルトが静的であることが他の言語と大きく違う点です。
この違いは呼び出しメソッドを「静的」に解決するか、それとも「動的」に解決するかの違いです。静的に解決するなら単にポインタの加減算で済みますが、動的に解決する場合は仮想関数テーブルの検索が必要です。
もし動的かどうかではなく、単にデータと紐づくメソッドの存在だけなら、抽象データ型という概念になります。
この抽象データ型は文法こそオブジェクト指向プログラミング言語と同様ですし、重要な要素ではありますが、これだけなら概念的には構造化プログラミングの範囲に留まります。
そしてもし、ひたすらトップダ
「オブジェクト指向」自体が様々な解釈をされ、その指すものが各自バラバラなのでなんとも言い難いところがありますが、自分なりの答えを。
ちなみに自分は言語的にはObjective-C、Java、Rubyあたりを意識していて(あとSmalltalkは使ったことないけど先駆者として)、C++には否定的です。
質問の答えを一言でいうと、構造化プログラミングだけでは不十分だからです。
構造化プログラミングと、オブジェクト指向プログラミングの一番の違いは、ここで書いたように、メソッドを動的に解決するか否かだと考えています。C++はデフォルトが静的であることが他の言語と大きく違う点です。
この違いは呼び出しメソッドを「静的」に解決するか、それとも「動的」に解決するかの違いです。静的に解決するなら単にポインタの加減算で済みますが、動的に解決する場合は仮想関数テーブルの検索が必要です。
もし動的かどうかではなく、単にデータと紐づくメソッドの存在だけなら、抽象データ型という概念になります。
この抽象データ型は文法こそオブジェクト指向プログラミング言語と同様ですし、重要な要素ではありますが、これだけなら概念的には構造化プログラミングの範囲に留まります。
そしてもし、ひたすらトップダウンで分割を繰り返すことだけで正しいプログラムが作れるなら、構造化プログラミングだけで十分です。
しかし分割だけでは不十分なことが分かっています。
その理由としてプログラムの規模が大きくなっているのも1つですが、これはあくまで量の問題です。問題は「非同期(あるいはマルチスレッド)」、そしてそれに伴う「競合」、あるいは「例外処理」です。
昔と比べると非同期処理、マルチスレッドの範囲が増えています。バッチ処理はオフライン処理もありましたが、今は基本的にオンラインで並列に処理が走るようになっています。GUIは非同期処理でないと成立しません。HTTPも2から非同期になっています。
そして、分割した詳細の1つに非同期処理が入ると、全体に波及します。JavaScriptで言えばasync関数の呼び出し元はasyncであるか、Promiseをthen, catchで「例外処理」するかどちらかです。
また、分割した詳細で例外処理が必要な場合も、呼び出し元で適切に処理するか、トップで処理するか、どちらにしても全体に波及します。
そして「分割を繰り返すことだけ」で設計をすると、細部で全体に影響が出る問題が出たときに取り返しがつかなくなります。これがウォーターフォール開発が炎上しがちな理由です。
となると、「分割を繰り返すことだけ」のトップダウン設計手法には問題があり、全体と細部を同時に見る、トップダウンとボトムアップの組み合わせでの設計が必要です。
ボトムアップの設計によって全体に影響する問題がどのようなものかを早めに明らかにし、トップダウンの設計でその全体に影響する問題を仕組みで解決します。
トップダウンの設計は、例えばコアロジックを非同期処理も例外設計も不要な形で分離する場合もあれば、イベントソーシングのように競合が起きづらいアーキテクチャを選定する手もあります。
そのトップダウンとボトムアップの組み合わせでの設計に適しているのがメソッドを動的に解決する、オブジェクト指向プログラミングです。小さな部品から作り始めることも、部品ができたものと仮定して全体を作って動かしてみることもできます。
今のところはそんな感じで考えています。
仮にあなたがオブジェクト指向という考え方も名前も存在しない世界に転生したとして、効率と安全を考えて自然にあの形を自分で思いつくかというとかなり疑問です。あなたが自然(かつ唯一)だと思っているのも、誰かが思いついたものを学んで受け入れたのでそう考えるだけでしょう。
またその世界に、あの形以外のプログラミングの形が存在しえないと考えるのも不自然です。複数のプログラミングの形が存在する場合、それらを区別するためにも名前を付けるのは重要ではないかと思います。
昔々、デザインパターンという考え方が流行した時、その最大の功績はプログラミングにおいて度々登場するパターンに名前を付けたことで、みながそのパターンの存在を認識し、共通の認識で議論できるようになったことでした。それと同じようにオブジェクト指向という名前を付けることによって、みながそのパラダイムを認識できるようになり議論できるようになったのだと思います。
ただ、オブジェクト指向の場合、細部の定義が曖昧なままだったので、議論がすれ違いになりがちという問題があったのですが、それはそれとして。
関数型プログラミングは、言語はあまり関係なくて、以下のような考え方、スタイルなんですよ。
- 可変状態はバグの巣窟で、可読性・理解性・予測性・再利用性・スケーラビリティを損う、多くの害悪の故郷。無ければないほどよい。避けられないなら識別弁別して管理する。かろうじてそうできた時のみ存在を許す。
- 純粋関数及びその合成は上記を達成するための手段の一つとして使い勝手が良い
- それを達成しようとした時に「オブジェクト指向」と論理的な理由や必然性なく、何故かまとめられて総称して呼ばれる言語機能群のうちの多くが、あるいはそう名乗られて行われてきたアプローチの多くが邪魔になる、もしくは不要
- 例えば、クラス名の文脈の元でのメソッド名を考える時間が無駄。あるいはメソッドをまとめる単位として意味のあるクラス名を考える時間が無駄。誤解を生む名前をつけるわけには行かないのでプログラミングの難易度が上がってしまっている。果てはSRP原則とか言って際限なくクラス粒度が細かくなったり、苦肉の策としてデザパタなるものを生み出した。
- (クラスベースオブジェクト指向に関して)オブジェクト万能信仰から生まれたクラスの機能過剰さ。型、モジュール単位、ファイル分割単位、メソッドとデータの結合、抽象型、インターフェース、vtable保持、差分プログラミング、ユニットテスト単位を兼ね備えるなど、ないわー。
良い悪いはなくて、上のような考え方に沿お
関数型プログラミングは、言語はあまり関係なくて、以下のような考え方、スタイルなんですよ。
- 可変状態はバグの巣窟で、可読性・理解性・予測性・再利用性・スケーラビリティを損う、多くの害悪の故郷。無ければないほどよい。避けられないなら識別弁別して管理する。かろうじてそうできた時のみ存在を許す。
- 純粋関数及びその合成は上記を達成するための手段の一つとして使い勝手が良い
- それを達成しようとした時に「オブジェクト指向」と論理的な理由や必然性なく、何故かまとめられて総称して呼ばれる言語機能群のうちの多くが、あるいはそう名乗られて行われてきたアプローチの多くが邪魔になる、もしくは不要
- 例えば、クラス名の文脈の元でのメソッド名を考える時間が無駄。あるいはメソッドをまとめる単位として意味のあるクラス名を考える時間が無駄。誤解を生む名前をつけるわけには行かないのでプログラミングの難易度が上がってしまっている。果てはSRP原則とか言って際限なくクラス粒度が細かくなったり、苦肉の策としてデザパタなるものを生み出した。
- (クラスベースオブジェクト指向に関して)オブジェクト万能信仰から生まれたクラスの機能過剰さ。型、モジュール単位、ファイル分割単位、メソッドとデータの結合、抽象型、インターフェース、vtable保持、差分プログラミング、ユニットテスト単位を兼ね備えるなど、ないわー。
良い悪いはなくて、上のような考え方に沿おうとするかどうかです。
参考
もちろん可能です。両者は互いに直行する概念で同時に利用することができます。Scalaという言語なんてまさに関数型オブジェクト指向言語を謳っています。
オブジェクト指向プログラミングの一番重要なポイントはインターフェースと実装の分離です。関数を定義するときには引数には実際のクラスではなくて抽象クラス、interface、traitなどを渡すように指定します。このようにして利用者はそのクラスの内部実装を気にすることなく、インターフェース、つまり入出力の形式のみに気を付ければよく、また別の実装に切り替えることも容易です。
関数型プログラミングの最も重要な概念は副作用の分離であり、変数は上書きせず、極力純粋関数を使っていくというスタイルです。大雑把に言えば
- y = f(x)
- z = g(y)
- w = h(z)
- ...
のように式を書き続けていくことでプログラムを作るということです。
では両者を両立するにはどうすればいいのでしょうか?答えは簡単で、クラスは基本的にはコンストラクタで各要素に値をセットしたら以降は変更しないということに注意すればいいです。そしてクラスが実装するインターフェースも基本的には純粋関数のみです。こうすることで値の不変性を保ちつつも、クラスを用いて複数の変数をまとめ、同時に内部実装は隠蔽してインターフェースに集中するような開発をすることができます。
そしてこの方式を最もやりやすい言語は
脚注
もちろん可能です。両者は互いに直行する概念で同時に利用することができます。Scalaという言語なんてまさに関数型オブジェクト指向言語を謳っています。
オブジェクト指向プログラミングの一番重要なポイントはインターフェースと実装の分離です。関数を定義するときには引数には実際のクラスではなくて抽象クラス、interface、traitなどを渡すように指定します。このようにして利用者はそのクラスの内部実装を気にすることなく、インターフェース、つまり入出力の形式のみに気を付ければよく、また別の実装に切り替えることも容易です。
関数型プログラミングの最も重要な概念は副作用の分離であり、変数は上書きせず、極力純粋関数を使っていくというスタイルです。大雑把に言えば
- y = f(x)
- z = g(y)
- w = h(z)
- ...
のように式を書き続けていくことでプログラムを作るということです。
では両者を両立するにはどうすればいいのでしょうか?答えは簡単で、クラスは基本的にはコンストラクタで各要素に値をセットしたら以降は変更しないということに注意すればいいです。そしてクラスが実装するインターフェースも基本的には純粋関数のみです。こうすることで値の不変性を保ちつつも、クラスを用いて複数の変数をまとめ、同時に内部実装は隠蔽してインターフェースに集中するような開発をすることができます。
そしてこの方式を最もやりやすい言語はもちろんRustです!私はいつもRustで関数型オブジェクト指向プログラミングを実践しています。Rustは変数を宣言するときにmutを付けなければデフォルトで不変な変数として扱われます。また、関数に渡すときにも可変参照(&mut T)を極力を極力封印すれば関数型的な書き方を達成できます。またRustのTraitはJavaのInterfaceをより強力にしたものであり、特に関数の引数が複数のTraitを実装することを要請することもできます。
この回答を読んでいるあなた、今からRustを始めようではないか!
脚注
※質問者の意図が明かされたので内容を改めました。
可能かもしれません。が、困難な割にそれに見合う効果(“オブジェクト指向”を正しく理解できた!と納得させれる…とか?)があるとは思えません。むしろそうした試みは、これまで修正されずに複製・伝播してしまった“オブジェクト指向”の誤ったミームの強化を助け、本来それぞれのアイデアが持つ真のパワーを十分引き出せないまま埋もれさせてしまう危険性を個人的には危惧します。
繰り返しになりますが、すでに オブジェクト指向とは何ですか?に対する回答 にも書いたとおり、これら3つのオブジェクト指向は同じ「オブジェクト指向」と称していても、互いに異なる方針を持った考え方です。
端的には次のようにまとめられます。
- メッセージ指向のオブジェクト指向 …… メッセージング(メッセージ送信、メッセージパッシングとも)を通じて、決定の遅延の徹底するアイデア。メッセージを通じてどんなことをシミュレートするかが大事で、実働するオブジェクト(それを定義するクラス)それ自体やその中身には頓着しない(後で自由に変えられるようにする、それが可能であるように処理系などがサポートする)
- クラス指向のオブジェクト指向 …… 「クラス」という言語機能による抽象データ型の実現(カプセル化・継承・多態性)を通じて、ユーザーによる型定義を可能にするアイデア。その上で型安全なプログラミングを目指す。
- プロ
※質問者の意図が明かされたので内容を改めました。
可能かもしれません。が、困難な割にそれに見合う効果(“オブジェクト指向”を正しく理解できた!と納得させれる…とか?)があるとは思えません。むしろそうした試みは、これまで修正されずに複製・伝播してしまった“オブジェクト指向”の誤ったミームの強化を助け、本来それぞれのアイデアが持つ真のパワーを十分引き出せないまま埋もれさせてしまう危険性を個人的には危惧します。
繰り返しになりますが、すでに オブジェクト指向とは何ですか?に対する回答 にも書いたとおり、これら3つのオブジェクト指向は同じ「オブジェクト指向」と称していても、互いに異なる方針を持った考え方です。
端的には次のようにまとめられます。
- メッセージ指向のオブジェクト指向 …… メッセージング(メッセージ送信、メッセージパッシングとも)を通じて、決定の遅延の徹底するアイデア。メッセージを通じてどんなことをシミュレートするかが大事で、実働するオブジェクト(それを定義するクラス)それ自体やその中身には頓着しない(後で自由に変えられるようにする、それが可能であるように処理系などがサポートする)
- クラス指向のオブジェクト指向 …… 「クラス」という言語機能による抽象データ型の実現(カプセル化・継承・多態性)を通じて、ユーザーによる型定義を可能にするアイデア。その上で型安全なプログラミングを目指す。
- プロトタイプ指向のオブジェクト指向 …… メッセージ指向のオブジェクト指向のプログラミング(具体的にはSmalltalkのやり方)から、クラス(やメタクラス、そのメタクラス…)は言語機能には不要、オブジェクトはクラスのインスタンスである必要はなくすべてを他のオブジェクト(プロトタイプ。より語弊のない言い方ではペアレント)への委譲で済ませればよい、後にはメッセージすら不要、というように批判的に派生して構築された。Smalltalkと同じことをやるのには、オブジェクト(フレームとスロット。あと、プロトタイプ・ペアレントを接続する特殊なスロット)と関数(クロージャ)があれば十分シミュレートできるという考え方。
ここまで違うものを包摂する説明や定義に拘泥することはあまりよい結果をうまないことは、冒頭に記したとおりです。
それを承知の上で、あえて共通点を挙げるとすれば、同じ「オブジェクト」という言語機能(データの中に手続きを内包するのが特徴)を使用したプログラミングであることと、それゆえにお互いでお互いをシミュレート・模倣する、あるいはただ解釈を変える(例:クラス ⇄ クラス的な機能を組み込んだオブジェクト ⇄ 単にメソッドを括りだした委譲先オブジェクト、メンバー関数の動的なコール ⇄ メッセージ、など)ことを通じて制約はあるもののある程度似た考え方でのプログラミングが、オブジェクトを持たない言語よりはやりやすい(もちろん、享受できるメリットは限定されるが⸺)というくらいでしょうか。
これとはちょっと趣が違うかな?
他の回答でも述べられていますが、プログラミングの経験が浅い人が、本や雑誌の記事などからトップダウンで、オブジェクト指向はこういう概念であり、こういう設計手法だから、それを適用してプログラムを作ろう、という形では理解できないと思います。
私の場合、プログラミングが向上する過程は、以下の様でした。
- 最初はめちゃくちゃなプログラムを作る。
- 重複したコードを1つの関数にまとめるようになる。
- 関連した変数を構造体にまとめるようになる。
- 関数をコードが重複しているかどうかに加えて意味(コンセプト)で作るようになる。構造体を引数とする関数を作るようになる。
- 構造体を引数とする関数を、構造体(クラス)のメソッドとして分類、整理するようになる(ここでオブジェクト指向に足を踏み入れる)。
- 異なるクラスのオブジェクト間で同じメソッド呼び出しを定義し、使用するようになる(ポリモーフィズムを使う)。
このようにプログラムについて考え、改良していった結果が自然とオブジェクト指向に近づくことになっていました。私はこのような経験からの裏づけがあって初めて、本当にオブジェクト指向を理解できるようになると思います。やがてオブジェクト指向のトップダウンの知識と、経験によるボトムアップのプログラミングが1つになれば、オブジェクト指向マスターだと思います。
そうなるには時間が必要です。普通は2年くらいかかるでしょうか。もっとかもしれません。
他の回答でも述べられていますが、プログラミングの経験が浅い人が、本や雑誌の記事などからトップダウンで、オブジェクト指向はこういう概念であり、こういう設計手法だから、それを適用してプログラムを作ろう、という形では理解できないと思います。
私の場合、プログラミングが向上する過程は、以下の様でした。
- 最初はめちゃくちゃなプログラムを作る。
- 重複したコードを1つの関数にまとめるようになる。
- 関連した変数を構造体にまとめるようになる。
- 関数をコードが重複しているかどうかに加えて意味(コンセプト)で作るようになる。構造体を引数とする関数を作るようになる。
- 構造体を引数とする関数を、構造体(クラス)のメソッドとして分類、整理するようになる(ここでオブジェクト指向に足を踏み入れる)。
- 異なるクラスのオブジェクト間で同じメソッド呼び出しを定義し、使用するようになる(ポリモーフィズムを使う)。
このようにプログラムについて考え、改良していった結果が自然とオブジェクト指向に近づくことになっていました。私はこのような経験からの裏づけがあって初めて、本当にオブジェクト指向を理解できるようになると思います。やがてオブジェクト指向のトップダウンの知識と、経験によるボトムアップのプログラミングが1つになれば、オブジェクト指向マスターだと思います。
そうなるには時間が必要です。普通は2年くらいかかるでしょうか。もっとかもしれません。もっとも、こういうことすべてをすぐに直感的に理解するような天才的な人はいるかもしれませんが。
もともとのオブジェクト指向が目指していたものは、「オブジェクトと名付けたモジュール同士の疎結合」を推し進めることにより、複数のオブジェクトによって構成されたシステム全体の柔軟性を増すことでした。
ただ、継承というのはそのようなモジュール同士の結合の度合いとしてはとても密結合なわけです。気の利いたオブジェクト指向であればクラスもまたオブジェクトですが、継承はそのようなオブジェクトの組があったとき、一つのオブジェクトへの変更が局所的なもので終わらない、とみることができます。
もちろん、一方で多数あるオブジェクトをグループにまとめ、また「オブジェクトBはだいたいオブジェクトAと同じだけど、少しだけ違う」という差分プログラムをしたい、という需要はあるわけです。その場合も「小クラスのインスタンスを親クラスのインスタンスの代替として使ってもシステムがちゃんと動く」という性質(Liskov substitution principle (LSP))を維持したいわけですが、現在のプログラミング言語では継承があまりにも簡単にできてしまうために、その性質を満たさない形での継承が行われてしまいがちです。
そのために、継承して親クラスのメソッドを上書きする時に、ある種の契約を満たすことを保証する方法などの研究もいろいろとなされていました。Cliftonらによる"method family"のアイディア、つまり字
もともとのオブジェクト指向が目指していたものは、「オブジェクトと名付けたモジュール同士の疎結合」を推し進めることにより、複数のオブジェクトによって構成されたシステム全体の柔軟性を増すことでした。
ただ、継承というのはそのようなモジュール同士の結合の度合いとしてはとても密結合なわけです。気の利いたオブジェクト指向であればクラスもまたオブジェクトですが、継承はそのようなオブジェクトの組があったとき、一つのオブジェクトへの変更が局所的なもので終わらない、とみることができます。
もちろん、一方で多数あるオブジェクトをグループにまとめ、また「オブジェクトBはだいたいオブジェクトAと同じだけど、少しだけ違う」という差分プログラムをしたい、という需要はあるわけです。その場合も「小クラスのインスタンスを親クラスのインスタンスの代替として使ってもシステムがちゃんと動く」という性質(Liskov substitution principle (LSP))を維持したいわけですが、現在のプログラミング言語では継承があまりにも簡単にできてしまうために、その性質を満たさない形での継承が行われてしまいがちです。
そのために、継承して親クラスのメソッドを上書きする時に、ある種の契約を満たすことを保証する方法などの研究もいろいろとなされていました。Cliftonらによる"method family"のアイディア、つまり字面上同じメソッドシグネチャになっているという判別だけではなく、意味として同じ継承関係にあるメソッドを扱うための仕組みや、種々のコントラクトベースの使用記述などもありました。
ただ、現状はそのような仕組みに頼るよりは、なるべく継承は避ける、ということで話は大体済んでいるような感じはありますね。
ちなみに、Smalltalkという、オブジェクト指向という言葉が発明されるきっかけとなった言語では、当初継承機能はありませんでした。ですので、オブジェクト指向では継承がなくてはならない、という決まりはないということは言及しておく価値があるでしょう。
正しい使い方が非常に困難だからです。
例えば、Javaの標準クラスライブラリでも少なくとも2つ間違った継承の使い方があります。
上記のような
Timestamp
クラスとjava.util.Date
クラスの違いのため、Timestamp
値はjava.util.Date
のインスタンスとして考えないでください。Timestamp
とjava.util.Date
の継承関係は、型の継承ではなく、実装の継承を示します。
Properties
はHashtable
を継承するので、Properties
オブジェクトに対してput
メソッドおよびputAll
メソッドを適用できます。しかし、これらのメソッドを使用することは推奨されません。これらのメソッドを使うと、呼出し側はキーまたは値がStrings
ではないエントリを挿入できるからです。setProperty
メソッドを代わりに使用してください。
このように継承したクラスが継承元のクラスと同じように扱えないと問題があります。例えば次のような不自然な挙動になります。
- System.out.println(new java.util.Date(10000).before(new java.sql.Timestamp(20000))); /
正しい使い方が非常に困難だからです。
例えば、Javaの標準クラスライブラリでも少なくとも2つ間違った継承の使い方があります。
上記のような
Timestamp
クラスとjava.util.Date
クラスの違いのため、Timestamp
値はjava.util.Date
のインスタンスとして考えないでください。Timestamp
とjava.util.Date
の継承関係は、型の継承ではなく、実装の継承を示します。
Properties
はHashtable
を継承するので、Properties
オブジェクトに対してput
メソッドおよびputAll
メソッドを適用できます。しかし、これらのメソッドを使用することは推奨されません。これらのメソッドを使うと、呼出し側はキーまたは値がStrings
ではないエントリを挿入できるからです。setProperty
メソッドを代わりに使用してください。
このように継承したクラスが継承元のクラスと同じように扱えないと問題があります。例えば次のような不自然な挙動になります。
- System.out.println(new java.util.Date(10000).before(new java.sql.Timestamp(20000))); // true
- System.out.println(new java.util.Date(10000).before(new java.sql.Timestamp(10999))); // false
継承したクラスが継承元のクラスと同じように扱うためにはいくつかの条件を満たす必要があります。これをリスコフの置換原則と言います。
継承を使うためにはリスコフの置換原則を理解して、継承元の全ての振る舞いを変えていないか検証する必要があります。
でも誰もやりませんよね。継承を使うときって大体が継承元のクラスでは不足しているからと「パッチ」を当てるために使われています。メソッドの追加だけならまだしも、メソッドを完全に置き換えたりします。
しかも継承したクラスをさらに継承してパッチを当てることを繰り返します。すると全く理解できないコードになります。Djangoのクラスベースビューがそんな感じで、読み解くのに苦労しました。
そしてほとんどのケースでは移譲やインタフェースの定義といった、継承をしないパターンで十分です。初心者に使わせるには問題が多すぎるんですね。継承は。
私も継承はどちらかと言えば否定的ですね。
持論ですが継承には4つの使われ方があって
①機能の追加、②機能の変更、③インターフェース、④タスク管理
これらを区別なくまとめてしまっているのが「継承」の問題の一つに思います。
~
①機能の追加:継承の継承たる典型的な使い方。
②機能の変更:継承のさらなる高度な使い方。変更すると別の意図と目的を持ったクラスに生まれ変わる。改修作業で現状を触りたくないがために使われやすい。その場合本来必要のない継承が何重にも行われてしまいがち。
③インターフェース:継承とは本来別物。C++では純粋仮想関数を作って継承して実装するが、可読性を悪くする。複数のI/Fを持たせるのも容易ではない。
④タスク管理:継承とは本来別物。基底クラスを使ったタスク管理。仮想関数によって実行時解釈する機能をフルに使う。
もしこの4種類の継承が明確に分離されていたら、『継承』の意図が解るので継承を忌諱することもすくなくなると考えています。
- // 例
- class A :add B {}; //機能追加
- class A :update B {}; //機能変更
- class A :add B, update B {}; //機能追加&変更
- class A :interface B {} //インターフェースの追加
- class A :taskof B {} //タスク管理の追加
C++は、もともと複数の「方針」を混ぜて使えるようにということで、いろいろな機能を入れるというデザインであるため、オブジェクトではないものが言語のコンセプトの中にあるため、「完全なオブジェクト指向」ではないということが言えます。
純粋なオブジェクト指向だと言われるSmalltalkの場合、BooleanやIntegerがオブジェクトなのはもちろんですが、「コンピューター全体」がオブジェクトの集合として表現されています。「クラス」、「メソッド」、「メタクラス」と言ったものももちろん、クラスの集合を格納しておく「名前空間」と言ったものも当然ながらオブジェクトで、それらは実行環境の中で直接操作することができます。さらには、その実行環境もまたオブジェクトの集合なのです実行状態を表す「スタックフレーム」もオブジェクトであり、例えば「継続」を実装したければ、スタックをコピーして保持しておき、後でreturn:やresume:といったメソッドを呼び出して復帰したりもできます。
このことから導き出される最も重要な要素は、「論理的にはシステムの実行ということそのものが自分自身で記述されている」ということです。あるオブジェクトにメッセージ・オブジェクトが送られた時に、対応するメソッド・オブジェクトを探し出し、それを起動するためにスタックフレーム・オブジェクトを割り当てて、メソッドが指定する動作に応じてその
C++は、もともと複数の「方針」を混ぜて使えるようにということで、いろいろな機能を入れるというデザインであるため、オブジェクトではないものが言語のコンセプトの中にあるため、「完全なオブジェクト指向」ではないということが言えます。
純粋なオブジェクト指向だと言われるSmalltalkの場合、BooleanやIntegerがオブジェクトなのはもちろんですが、「コンピューター全体」がオブジェクトの集合として表現されています。「クラス」、「メソッド」、「メタクラス」と言ったものももちろん、クラスの集合を格納しておく「名前空間」と言ったものも当然ながらオブジェクトで、それらは実行環境の中で直接操作することができます。さらには、その実行環境もまたオブジェクトの集合なのです実行状態を表す「スタックフレーム」もオブジェクトであり、例えば「継続」を実装したければ、スタックをコピーして保持しておき、後でreturn:やresume:といったメソッドを呼び出して復帰したりもできます。
このことから導き出される最も重要な要素は、「論理的にはシステムの実行ということそのものが自分自身で記述されている」ということです。あるオブジェクトにメッセージ・オブジェクトが送られた時に、対応するメソッド・オブジェクトを探し出し、それを起動するためにスタックフレーム・オブジェクトを割り当てて、メソッドが指定する動作に応じてそのフレームに値を書き込んだり読みだしたりすることが言語自身で記述されたプログラムとして行われているかのようになっているわけです。
ただ、もし本当にこのように動作しているのだとすれば、他の人の回答にある「遅いのではないか」という想像に基づく懸念も理解できるのですが、実用的な速度で動かすために、実際には上記の動作をより高速に行うためのインタープリター「も」用意されています。実行されているコードが上記のようなメタ機能を司るオブジェクトにアクセスしない限りは、それらのオブジェクトを実際に生成する必要はなく、インタープリターは諸処の工夫で高速化することができます。結果的には、一般的に使われているSmalltalkの処理系はPythonやRubyのものよりも何倍も高速です。もともとグラフィックスやリアルタイム音声処理をしようとしていた人々が作っていた言語ですので、それらをこなせるようになっているわけです。
別の言い方をすれば、他の言語で文字列処理やデータ処理のためにネイティブ・メソッドを書き、言語自体で書かれたコードの代わりに外部のものを呼び出すことによって同じ機能をより高速に提供できる、というところで、Smalltalkはそのようなデータ処理だけではなく言語実行全体をネイティブメソッドのように高速化したものも用意されていると言っても良いかもしれません。
tracing JITを用いたような実装系であれば、全てがオブジェクトであり、Cで書かれたようなプリミティブをなるべく必要としないということが速度向上の役に立ちます。近年流行っている技術からの類推で言えば、ウェブブラウザのDOMオブジェクトの動作はC++で書かれていて、もしJavaScriptからそれらのオブジェクトのプロパティに値を代入すると、 C++で書かれたコードが内部で動作するというようになっていますよね。React.jsなどが使っているvirtual DOMは、その部分を仮想化してJavaScriptで書いているというわけなのですが、このテクニックが効果的である一つの理由は、C++で書かれたDOMオブジェクト操作の部分を通らないJavaScriptの「実行トレース」を長くすることができるために、JITコンパイラがより効果的になるから、という面があります。Smalltalkですべてをオブジェクトにするというのは、わざわざvirtual DOMのようなものをあえて実装しなくてももともと全部がvirtual DOMみたいなものだったと言えるわけですし、tracing JITでなくても少ない機能の高速化に注力するだけで全体が早くなるという効果もあります。
システムとしては、「マウス」、「ディスプレイ」というものもオブジェクトであり、「ディスプレイ」のピクセル値を書き換えることにより画面を描画しますし、何が表示されているのかもピクセルを読み出すことにより確認できます。こちらもBitBltと呼ばれる高速化ルーチンがあります。理屈の上ではユーザーは何も気にしないでコードを書けばいつのまにか、Cで書いたのと同じようなパフォーマンスのグラフィックス操作ができる、という建前ですが、この辺はさすがに実際にどのようになっているのか知らないと速度は出ませんが。
まとめると、もともとの質問である「なぜJavaは純粋なオブジェクト指向言語でないのか」といえば、もし純粋なオブジェクト指向言語というのであればせめて上記のようなレベルくらいにはなっていてほしいというところ、Javaではオブジェクトとして表現されていない要素があるから、ということになるでしょう。
(という回答は、実は同様な質問にたいする回答
なぜjavaは純粋なオブジェクト指向プログラミング言語ではないですか?に対する大島 芳樹 (Yoshiki Ohshima)さんの回答
からの抜粋です。よろしければそちらもご覧ください。)
あと、Ueharaさんの回答にある
C++が完全なオブジェクト指向じゃないのはなぜですか?に対するQuoraユーザーさんの回答
の中で、「syntaxはオブジェクトか、メッセージ送信の演算子はオブジェクトか、vsync割り込みや時間経過概念はオブジェクトか、などを問いたい。」というところがありますが、Smalltalkに関して言えば、シンタックスやメッセージ送信の演算子はオブジェクトとしての表現があり、それらにメッセージを送ってなんらかの結果を受け取ることはできます。vsync割り込みは普通の処理系では抽象化されて隠蔽されていますが、もしvsyncをハンドルする必要がある場合は、vsyncがオブジェクトして見えるようなprimitiveを書くことによって導入することになるでしょう。時間経過の概念を明示的に表現することには大いに賛成です。つまり論理時間に応じてシステムの状態が決まるようにする、ということで、Smalltalkは伝統的にはそういうことはしていませんが、FRPをSmalltalkで書き時間経過の概念をユーザーランドでオブジェクトとして実現すれば良いとも言えますね。ただ、この「あれはどうだ、これはどうだ」という問いは、ある時点で「完全」という言葉の言葉遊びとなるとも思いますし、自分でオブジェクトとして書けば良い、という万能の答えが待っているような気もします。
クラスを用いて実現される疑似並列処理がプログラムの再利用にあたって便利だからです。プログラムの組み合わせの幅が広がるからと言い換えてもいいと思います。
例えば、ダイクストラの構造化プログラミングでは、プログラムはフローチャートに翻訳できるものに限定されます。つまり、計算プロセスが単線であるもの限定のプログラム技法なのですが、これの不便な点は、2つ検証済みの異なるプログラムA,Bが与えられたとき、頑張って両機能を提供する新たなプログラムCを実装しようとするときに、単純にプログラムA,Bを結合できない(単純に再利用することができない)というところにあります。
当たり前の話なので分かりにくいかもしれません。フローチャートを想像しながら読んでもらえれば幸いなのですが、あるフローチャートに翻訳できるプログラムAともう一つのフローチャートに翻訳できるプログラムBがあるとして、両機能を提供するプログラムCというのは、その2つのフローチャートを単純にくっつけたものでは「ない」のです。
変数名、関数名の衝突、if文の分岐の必要性などがあるので、整合性を保てるようにかならず手直しする必要があります。つまり、構造化プログラミングはあんまりモジュール性(カプセル化)がよろしくないので、純粋な手続とか関数以外は、分業した上で組み合わせるということがしづらいのです。
一方でクラスの場合は、一般にカプセル化と、呼び出し
クラスを用いて実現される疑似並列処理がプログラムの再利用にあたって便利だからです。プログラムの組み合わせの幅が広がるからと言い換えてもいいと思います。
例えば、ダイクストラの構造化プログラミングでは、プログラムはフローチャートに翻訳できるものに限定されます。つまり、計算プロセスが単線であるもの限定のプログラム技法なのですが、これの不便な点は、2つ検証済みの異なるプログラムA,Bが与えられたとき、頑張って両機能を提供する新たなプログラムCを実装しようとするときに、単純にプログラムA,Bを結合できない(単純に再利用することができない)というところにあります。
当たり前の話なので分かりにくいかもしれません。フローチャートを想像しながら読んでもらえれば幸いなのですが、あるフローチャートに翻訳できるプログラムAともう一つのフローチャートに翻訳できるプログラムBがあるとして、両機能を提供するプログラムCというのは、その2つのフローチャートを単純にくっつけたものでは「ない」のです。
変数名、関数名の衝突、if文の分岐の必要性などがあるので、整合性を保てるようにかならず手直しする必要があります。つまり、構造化プログラミングはあんまりモジュール性(カプセル化)がよろしくないので、純粋な手続とか関数以外は、分業した上で組み合わせるということがしづらいのです。
一方でクラスの場合は、一般にカプセル化と、呼び出しをしてもプロセスが消滅しないという性質のおかげで、クラスとしてプログラムA,とプログラムBを記述してしまえば、それぞれ独立にオブジェクトを宣言して疑似並列的に実行させることができます。この時、プログラムAとプログラムBで相互に衝突する変数名、関数名などの調整は不要です。
つまり、クラスとしてカプセル化した状態でプログラムを提供してしまえば、中の変数名の衝突とかを気にする必要なしに、そのプログラムを再利用することができるという利点があるのです。これはダイクストラの構造化プログラミングではありえません。
こういうプログラムの再利用性がよいという理由で、クラス機構を持ったプログラミング言語が必要とされているという面があると思います。
質問者ですがすでにご回答を多く頂いており、たいへんありがたく思います。
この質問の背景は、sumimさんの別の質問への回答
を読み、もっともだと思いつつ、でも世界のそうそうたるコンピュータサイエンスの泰斗らが、初期の段階で「OOPという概念総称が不適切であり不要で、別の概念をまぜこぜにしたものだ」とはなぜ思いつかなかったか、思いついても訂正を実行しえなかったことについて「やはりそれぞれのオブジェクト指向には、違いはあれど、やはり共通する本質的な特徴もある」ように思ったからです。だからこそ、オブジェクト指向という名称は受け入れられ、広まったのではないかと。違いはあるにせよ、あるからこそ、それをしっかりと認識しつつ、共通部分を今一度確認したいというのがこの質問の動機です。
可能であり、その共通部分は以下のように説明できます。
オブジェクト指向と呼ばれる仕組みに共通するのは、呼び出し側で処理実体を特定せずに、呼び出しを実行でき、呼び出しに使われる(レシーバ、ターゲット、引数)が、処理実体を選ぶことである。
まあレイトバインディングのことですが、これを詳しく説明します。オブジェクト指向での機能実行は、クラス指向であろうがメッセージ指向であろうがプロトタイプ指向であろうが、常に以下のようになります。
呼び出し側:
- (1)実行したい処理をあらわすためのシグネチャS(スコープから特定されるメソッド名+引数の
脚注
質問者ですがすでにご回答を多く頂いており、たいへんありがたく思います。
この質問の背景は、sumimさんの別の質問への回答
を読み、もっともだと思いつつ、でも世界のそうそうたるコンピュータサイエンスの泰斗らが、初期の段階で「OOPという概念総称が不適切であり不要で、別の概念をまぜこぜにしたものだ」とはなぜ思いつかなかったか、思いついても訂正を実行しえなかったことについて「やはりそれぞれのオブジェクト指向には、違いはあれど、やはり共通する本質的な特徴もある」ように思ったからです。だからこそ、オブジェクト指向という名称は受け入れられ、広まったのではないかと。違いはあるにせよ、あるからこそ、それをしっかりと認識しつつ、共通部分を今一度確認したいというのがこの質問の動機です。
可能であり、その共通部分は以下のように説明できます。
オブジェクト指向と呼ばれる仕組みに共通するのは、呼び出し側で処理実体を特定せずに、呼び出しを実行でき、呼び出しに使われる(レシーバ、ターゲット、引数)が、処理実体を選ぶことである。
まあレイトバインディングのことですが、これを詳しく説明します。オブジェクト指向での機能実行は、クラス指向であろうがメッセージ指向であろうがプロトタイプ指向であろうが、常に以下のようになります。
呼び出し側:
- (1)実行したい処理をあらわすためのシグネチャS(スコープから特定されるメソッド名+引数の型など)の特定
- (2)引数Aの形成
- (3)S+Aを「呼び出し(メッセージor Invocation)」と呼ぶ。ただし実体としてメッセージというオブジェクト等を存在させるかどうかは実装依存
- (4)Aの中からレシーバOの選出(マルチメソッドの場合はOの候補は複数)★
- (5)OがS+Aから処理本体を特定★
呼び出され側:
- (6)処理本体の実行
ここでは、object.method(a1,a2)という呼び出しのとき、objectはAの一部、つまり引数と同列として一般に見做しています。(マルチメソッドを実現するCLOSやGroovyなどのオブジェクト指向言語をカバーするため)。
OはS+Aから処理本体を特定する機能をもっており「オブジェクト」と呼びます。
これに対して、非オブジェクト指向、もしくはオブジェクト指向以前は以下となります。
呼び出し側:
- (1)実行したい処理をあらわすためのシグネチャS(スコープから特定されるメソッド名+引数の型など)の特定
- (2)引数Aの形成
- (3)SとAから処理本体を特定
呼び出され側:
- (4)処理本体の実行
トップダウンの定義になるかどうかは別として、★をもつことが、ボトムアップの観察としては、オブジェクト指向なる概念についての例外のない、漏れのない共通の特徴であると言えます。
ちなみにHaskellなどの言語の、OOPにインスパイアされて発案されたという「型クラス」は以下のようになります。
呼び出し側:
- (1)実行したい処理をあらわすためのシグネチャS(スコープから特定されるメソッド名+引数、返り値の型など)の特定
- (2)引数Aの形成
- (3)SとAからV(「S,A→処理実体」という対応表)を形成
- (4)Vを引いて、SとAから処理本体を特定
呼び出され側:
- (6)Vを(暗に)引数に追加して、処理本体の実行
このVがオブジェクトと呼ばれるデータに予め組み込まれているのがオブジェクト指向、Vが呼び出す側のスコープで非明示的に決定されていてデータに対して直接処理を呼び出すのが非オブジェクト指向、Vとデータが呼び出しのタイミングで結合するのが型クラスです。
ちなみに型クラスでは、手続きの集合Vを求めるためのメソッドシグネチャSに戻り値の型が含まれていてターゲット型推論に親和するのも特徴です。
質問では包摂と言ってるのに共通部分をいうのは反則かもとも思いましたが、回答を書く前はあまり突き詰めて考えていませんでした。
なお、本回答を通じて、これをOOPの定義としたい、そうであるべきだ、とか大それたことを言いたいわけではなく、昨今ある種の反動としてOOPは実体のないもの、定義のないもの、世界はそして騙された、と言うぐらいまでの向きがもしあるとしたら、それは言いすぎであって、そこまででもないんだよ、コア概念と言えるかはわかりませんが、共通のものはしっかりとあるんだよと言いたいということです。
その有用性や功罪は別の話ですが、それが具体的に、精密に、どんな機能についての議論かを認識しなければ世間話の域を出ないでしょう。
以下、コメントいただいた点からの、背景をもう少し説明するための追記です。
現在の、もしくは今後の問題として建設的であるために、私は、OOPという総称を具体的に意味ある言葉であるかのように使うのではなく、それを暗に陽に構成してきた、具体的な個別機能(メッセージ抽象、継承、操作の集合による代入互換性の静的・動的チェック、レイトバインディング、ほか)の方を、プログラマや技術者は意識すべきだ、という意見を持っております
。そうすることで、実体のない藁人形としてのOOP批判や擁護ではない、衒学的ではない、日々のプログラミングで実用上の意味やメリットのある議論や、他言語との建設的な比較 ができると思います。この質問は、そのために、OOP構成要素、部品をはっきりさせたかった、という動機によります。
その上でレイトバインディングは名称から結合タイミングだけの特徴と認識されそうですが、そうではなく、データと操作実体の結合のありさまそのものなので「OOP」という単語が指す概念に意味がもしあるとしたら、その中核的本質に近い概念だと思っており、全てのOOP概念に一貫して存在し、OOPの形以外には他に存在しないユニークなものであるという立場で本回答を記述しました。
特に型クラスとの類似性、意味的な関係性を示すことは、関数型プログラミングスタイルとの不毛な疑似対立を避けるために重要だと思っています。
脚注
はい、不当ですね。
しかし、私もたまに原理主義者みたいなのに噛みつかれる事がありますが、マトモに相手するだけ無駄ですよ、話なんて通じませんし。
個人的に思うのは、達人に近づけば近づく程に、常に意識しなければならん事は、「プログラムなんて、動いてナンボ」という事だと思います。
(初心者だと危険な考えだけど、達人であれば逆にこの考えが必要)
何らかのポリシーに則ったコーディングはたしかに大事たけど、結局の所、最も大事なのはチャンスを逃さないタイミングでのアプリリリースなんですね。
原理主義的な能書きばかり垂れてて手が遅いヤツ(一昔前の私だったりするけど)を見るとイライラします。
アンタが組んでるのは一体何のためのプログラムだ?という話です。