オブジェクト指向プログラミングというものは、失敗する要素や要因ではなく、ただただ不完全なものなのだと考えます。その不完全なもので何でもやろうとするから失敗するのだと。
オブジェクト指向の長所は構造化・体系化による情報整理です。これを代替するものはありません。
別の一面として、オブジェクト指向は、その名の通り、現実世界をすべてオブジェクトでモデル化するという体のコンセプトに端を発しています。これを万能だと思い違ったところに悲劇があるのだと考えます。
現実世界には、オブジェクト以外にリレーション(関係性)があります。それ以外にも捨象されている要素があるかもしれません。オブジェクト指向に関係性が足りていないと気づいて出てきたのがデザインパターンでしょう。しかしこれも完全ではありませんでした。これさえあれば解決できるという普遍性や網羅感に欠けるため、デザインパターンは嫌われるのだと思います。
さらに、ドメインの複雑性(状態遷移・副作用)を隠蔽・カプセル化で解決できなかった反省から注目を集めたのが、関数型プログラミングであり、immutable なデータコンテナです。
これらはドメインにある複雑性(状態遷移・副作用)から、複雑さを帯びないきれいな部分(参照透過的な部分・immutable 変数)を切り出して、その部分をシンプルに扱うことで全体の複雑さ(オブジェクトの状態遷移の組み合わせ爆発)を軽減して
オブジェクト指向プログラミングというものは、失敗する要素や要因ではなく、ただただ不完全なものなのだと考えます。その不完全なもので何でもやろうとするから失敗するのだと。
オブジェクト指向の長所は構造化・体系化による情報整理です。これを代替するものはありません。
別の一面として、オブジェクト指向は、その名の通り、現実世界をすべてオブジェクトでモデル化するという体のコンセプトに端を発しています。これを万能だと思い違ったところに悲劇があるのだと考えます。
現実世界には、オブジェクト以外にリレーション(関係性)があります。それ以外にも捨象されている要素があるかもしれません。オブジェクト指向に関係性が足りていないと気づいて出てきたのがデザインパターンでしょう。しかしこれも完全ではありませんでした。これさえあれば解決できるという普遍性や網羅感に欠けるため、デザインパターンは嫌われるのだと思います。
さらに、ドメインの複雑性(状態遷移・副作用)を隠蔽・カプセル化で解決できなかった反省から注目を集めたのが、関数型プログラミングであり、immutable なデータコンテナです。
これらはドメインにある複雑性(状態遷移・副作用)から、複雑さを帯びないきれいな部分(参照透過的な部分・immutable 変数)を切り出して、その部分をシンプルに扱うことで全体の複雑さ(オブジェクトの状態遷移の組み合わせ爆発)を軽減しています。
しかし、切り出した余りとなる汚い部分は依然オブジェクト指向の守備範囲に押しやられているだけであり、これらのパラダイムはドメインの複雑性を完全解決するものではないのです。オブジェクト指向を補完するものではあっても、取って代わるものではありません。
このように考えると見えてくるものがあります。オブジェクト指向は、依然残る複雑性という解決できない汚い部分を引き受ける「汚れ役」なのです。
オブジェクト指向自体に欠陥があるとは言えません。欠陥と思われている部分を解決する代替がないのですから。この汚い部分をスパッと解決してくれるパラダイムが登場しない限り、複雑さという課題の最後の引き受け手として、今後もオブジェクト指向がベースとならざるを得ないと思います。
オブジェクト指向は、代案も示されないのに、ただ引き受け手として居るだけでケチをつけられる与党のような存在です。「俺が稼げないのは、与党の政策が悪いからだ」といわんばかりの冤罪を押し付けられています。失敗の真の原因は、手段や能力の見積誤り、他責、他力本願といった認知特性ではないでしょうか。
失敗と断定できるような立場でもないので「そうなんでしょうか?」には回答できませんが、一プログラマとして読んだ場合はこの記事に納得できる部分が多かったです。
Quoraでこの質問に気付く少し前にちょうどその記事を読んで、その感想をツイートしていました。
私はまさにこの記事が猛烈に批判している「アラン・ケイが唱えたものとは違う」OOPを崇めて取り入れてきたプログラマです。(OO初体験はNeXT時代のObjective-Cなので、最初はまだアラン・ケイ寄りだったかもしれない)
今は、たとえば設計時にGoFのデザインパターンを当てはめてみようとかまったく考えていません。(語彙としてCommandやAdapterを思いつくことはあっても)「GoF/OOPを採用することで設計品質を上げよう」という発想にはならないのです。
この記事の中では、特に「低いSN比」「ミュータブル状態」という言葉にうんうん頷きました。
私は食えるプログラミング言語が好きです。JavaもC#も、案件があれば使い続けるでしょう。ですから「お前は結局どっちなんだ?」と聞かれてもごめんなさいとしか言えませんが、
- 今後、設計必須要件としてOOPを含めることはない。
- 今後、新しい言語の採用要件としてOOPを謳っている必要はない。
ことだけは確かだと考えます。
規模とか金額は、ただセンセーショナルにするための笛太鼓です。どうやって計算したのか、どうやって検証するのか、というツッコミを入れる必要すら感じません。
更に、ある種の失敗は必要なのです。失敗は成功の母です。
それを踏まえて、今はいらんな、と思います。でも、回り道はしたけど、無駄ではなかったなとも思います。(Reactのクラスコンポーネントを関数コンポーネント+Hooksで書き換えて、みるみるコードがシンプルになっていくのを体感しながら。)
(追記) 記事はとても長いですが、自分なりに一言でまとめると以下になります:
ソフトウェアの複雑さに向き合い戦う、という観点で、OOPはほとんど貢献できなかった。むしろそれを増大させた分のほうが大きい
参考
プログラムに「スズメ」や「カラス」「ハト」が登場します。
「鳥」クラスが「飛ぶ」メソッドを持つとします、この仮定は一見自然で人間にもわかりやすい「リーダブル」な表明です。この前提を用いて継承を用いプログラムを構成することにしました。
さて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のような無駄な制約をたかが実装手法がもたらすというのはデザイナーが最も忌み嫌うことですが、語られるようであまり語られない真実でしょう。
もちろんマテリアルの特性や、どのような設計が有益なプロダクトを構成しうるのかについて「無知蒙昧」であるならばよい設計にはなれませんが、継承に対しては「無知」であったほうがよい設計になれるのではないかと思います。設計行為に対する感受性(センス)を腐らせうる、それが継承の最も根源的な問題です。
オブジェクト指向とは何ですか?で書いた“メッセージングの「オブジェクト指向プログラミング」”について回答します。これは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同様、指摘されたらそのタイミングで期待される振る舞いを記述したメソッドを追加すればよいのです!)
一方で、「嫌い」とまではいきませんが、このメッセージングの考え方でのコーディングを推し進めたとき、処理系にもう一工夫あると便利なのにな…としばしば思うのは、同じメッセージを送って同じ結果が返ると分かっている場合は、前の結果を保持してそれを返して欲しいところです。もっと踏み込むと、我々がメッセージを送るだけで適切なアルゴリズムを選択してよきに計らってくれる、そんなシステムや処理系が理想です。
そこまでいかずとも、関数型でいうところの参照透明性のような利便性があると、メッセージングの記述をもっと読み下しやすく仕様に近づけられるのではないかななどと想像します。
逆風というわけではないのですが、オブジェクト指向プログラミングが不要なケースが明確になったのだと思います。
- Webというステートレスな処理を扱う場合、オブジェクト指向で実装する意義が若干下がった。
- 分散処理などを書く場合、オブジェクト指向的な実装より関数型プログラミング的な指向の方がマッチしている(ことが多い)。単一のプログラムでもコア数の増加とともに、分散処理で実装する意義が増えてきている。
- DBの性能向上・ORMの機能の多様化でオブジェクト指向が以前は担当していた領域がDBレイヤーにいったことで相対的に価値が下がった。
オブジェクト指向はステートフルな処理系を書く場合に情報をカプセル化することで、堅牢なプログラムを実装するベストプラクティスです。まだまだ、この実装方法がマッチするシステムはたくさんあります。
一方、オブジェクト指向で実装しないほうが良いケースも見えてきました。特に分散処理では、In/Out以外の依存性を持たないストリーム指向がマッチするため、関数型で実装した方がきれいに実装できるケースが多いと思います。
プログラマーであれば、基本すべてツールですから、いろんな方法で実装できるようにしたいものです。
質問の中で、まず「オブジェクト指向という言葉」に関して言えば、ある研究者がすでに存在していた種々の技術をまとめて子供のようなエンドユーザーでも使えるものにしようとしていたときに、懐疑的な人から「一体何をしようとしているんだ」と聞かれ、勢いに任せて「オブジェクト指向だよ」と言った、というのが発端です(1970年ごろのことだと思います)。
「オブジェクト」という言葉は、OSの分野などではメモリに割り当てられた複合的なデータ、というものを指す言葉として「オブジェクト指向」以前から使われていたのですが、そのものそのものが自律的なものとして扱うのだ、という意図が込められていたわけです。
プログラムをより良いやり方で組織立って書こうという努力は、コンピューターの性能が上がってきてからようやく人々が考えられるようになった、というようなものではなく、もちろんプログラミングというものが始まった時から常に考えられてきたことです。オブジェクト指向に関して言えば、その大きなインスピレーションの一つに、1961年以前から米空軍で発明されて使われていたデータの格納方式があります。
あちこちの基地の間をテープでやり取りしていたデータがどんどん複雑化していくという問題が当時からあり、今では名前を残していない誰かが、「テープの先頭から決まったオフセットのところに、後に続く読み出しや書き込みのためのプロシージャへのポインタ
質問の中で、まず「オブジェクト指向という言葉」に関して言えば、ある研究者がすでに存在していた種々の技術をまとめて子供のようなエンドユーザーでも使えるものにしようとしていたときに、懐疑的な人から「一体何をしようとしているんだ」と聞かれ、勢いに任せて「オブジェクト指向だよ」と言った、というのが発端です(1970年ごろのことだと思います)。
「オブジェクト」という言葉は、OSの分野などではメモリに割り当てられた複合的なデータ、というものを指す言葉として「オブジェクト指向」以前から使われていたのですが、そのものそのものが自律的なものとして扱うのだ、という意図が込められていたわけです。
プログラムをより良いやり方で組織立って書こうという努力は、コンピューターの性能が上がってきてからようやく人々が考えられるようになった、というようなものではなく、もちろんプログラミングというものが始まった時から常に考えられてきたことです。オブジェクト指向に関して言えば、その大きなインスピレーションの一つに、1961年以前から米空軍で発明されて使われていたデータの格納方式があります。
あちこちの基地の間をテープでやり取りしていたデータがどんどん複雑化していくという問題が当時からあり、今では名前を残していない誰かが、「テープの先頭から決まったオフセットのところに、後に続く読み出しや書き込みのためのプロシージャへのポインターを入れ、そして一緒に送っているデータを読み書きするためのプロシージャを同じテープの中に格納して送れば良いじゃないか」というアイディアを実現して広めたわけです。このようなテープは、データとそれを扱うためのプロシージャが文字通りひとまとめにされており、共通したインターフェイスからそのプロシージャにアクセスできたわけですね。つまり今で言うようなオブジェクト指向のオブジェクトの片鱗を持っていたわけエス。
このようなアイディアは、聞かされてみれば「誰でも自然に思いつく」と思われるかもしれませんが、発明としてはよくある話で、最初に思いついて実装するには何年もかかったものです。
オブジェクト指向という言葉が生まれる他のインスピレーションとしては、インターネットがありました。それは遠隔地にあるコンピュータ同士がメッセージを送り合うことにより動作するというものですが、こちらはネットワークというのはどこかが故障したりするものだから、効率重視をして上手くいくときに動けば良い、という仕組みとは正反対のポリシーで作るべきであるという考えに基づいていました。今では当たり前かもしれませんが、当時の一般的な考えとは逆行していたわけです。でも、この設計のおかげで、インターネットは五十年以上前に立ち上がった時から一度も全体をリブートして立ち上げ直す、というようなことをする必要なく、10^10個というようなノードが参加したネットワークに有機的に拡大したわけです。これこそは本当に「当たり前ではないけれども天才たちが見事に発明したもの」の例だと思います。
オブジェクト指向を謳ったプログラミング言語も、元々はこのようなスケーリングの可能性を持たせ、例えば一部の機能が不全を起こしても全体としてはクラッシュしない、というような特徴を持っているべきだとされていたわけです。もしお使いの言語がそのような特性を持っていないのだとすれば、それは十分安全ではないわけですね。そして、そのような特性を持たせるためには、システムを作るときにそれなりのビジョンと努力が必要となります。
Simulaという、オブジェクト指向という言葉が生まれる前から存在した言語もあります。こちらも素晴らしい発明でしたし、オブジェクト指向という言葉を生んだ言語に影響を与えましたが、やはりいろいろと先行する技術要素についても知っており、かつビジョンもある、というところでようやく生まれた概念であるとは思います。
というわけで、名前としてはある研究者がふと思いついていった言葉、というのが理由ですが、ただその裏にはかなり深いビジョンと歴史の積み重ねとひらめきがあり、なかなか当たり前のように思いつくものではないように思います。
データ指向プログラミングがくるんじゃないかなと思います。
オブジェクト指向も関数型も、所詮はプログラマが書きやすい手法であって、ハードウェアが計算するのには全く向いていません。ハードウェアの性能を限界まで引き出すためにデータ指向プログラミングがあります。
- #include <cstdio>
- #include <vector>
- #include <chrono>
- struct Data
- {
- int pos;
- int move;
- int data[100]; // ステータスとか想定 コメントアウトしてみよう
- };
- int main()
- {
- std::vector<Data> data(10000000);
- auto start = std::chrono::high_resolution_clock::now();
- // データ初期化
- for(size_t i = 0; i < data.size(); i++)
- {
- data[i].pos = (i*123) % 55;
- data[i].move =(i*55) % 122;
- }
- // 場所を更新
- for(size_t i = 0; i < data.size(); i++)
- {
- data[i].pos += data[i].move;
- }
- // 時間計測
- auto end = std::chrono::high_resolution_clo
データ指向プログラミングがくるんじゃないかなと思います。
オブジェクト指向も関数型も、所詮はプログラマが書きやすい手法であって、ハードウェアが計算するのには全く向いていません。ハードウェアの性能を限界まで引き出すためにデータ指向プログラミングがあります。
- #include <cstdio>
- #include <vector>
- #include <chrono>
- struct Data
- {
- int pos;
- int move;
- int data[100]; // ステータスとか想定 コメントアウトしてみよう
- };
- int main()
- {
- std::vector<Data> data(10000000);
- auto start = std::chrono::high_resolution_clock::now();
- // データ初期化
- for(size_t i = 0; i < data.size(); i++)
- {
- data[i].pos = (i*123) % 55;
- data[i].move =(i*55) % 122;
- }
- // 場所を更新
- for(size_t i = 0; i < data.size(); i++)
- {
- data[i].pos += data[i].move;
- }
- // 時間計測
- auto end = std::chrono::high_resolution_clock::now();
- auto time = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count() /1000.f;
- printf("time=%fs", time);
- return 0;
- }
①オブジェクト指向では普通ですよね?時間は?
- time=0.193000s
②Data構造体のint data[100]; をコメントアウトしてみると?
- time=0.034000s
なんと5倍以上高速になりました。
なぜこんなことが起こるんでしょうか?これは①ではposとmoveのメモリ上の位置がとても離れているため、メモリからデータを読みに行く必要があります。これが非常に遅い。②ではCPUのキャッシュにデータが残っているのでメモリからデータを読む回数を抑えられています。キャッシュに比べてメモリの読み込みは100倍とか200倍遅いとか言われてます。
データ指向設計のもっと詳しい説明はここを読んでみてください。
http://tech.cygames.co.jp/archives/2843/
GPGPUやスパコンではこの手法でないと速度が全く出ません。またSIMDなどベクトル演算でCPU命令を限界まで使い切るなら必然的にデータ指向になります。最近ではゲームエンジンのUnityではこの手法を取り入れたECS(Entity Component System)というものもあります。いずれは一般的な考え方になるでしょう。CPUのコア数が増え、相対的にメモリボトルネックになるほどこの考えは重要になってきます。
//===============================================
10/30追記
さらに、データ指向はモジュール性が高いので仕様変更に強いという利点があります。
オブジェクト指向でこんなデータ構造になっていたとしましょう。
- struct EnemyBase
- {
- EnemyParam enemyparam;
- Status s;
- Hoge h;
- Piyo p;
- virtual void update() = 0;
- };
- // 空中敵
- struct FlyingEnemy : EnemyBase
- {
- FlyParam flyparam;
- virtual void update();
- };
- // 地上敵
- struct GroundEnemy : EnemyBase
- {
- GroundParam groundparam;
- virtual void update();
- };
- // 水中敵
- ...
次の敵は空も飛ぶし水も潜るよ(命名:SuperEnemy)って言われたときにどう実装しましょうか?EnemyBaseクラスに○○Paramと関係する関数を全部ぶち込む?結構な修正になりますよね?
データ指向で書くと?
- struct SuperEnemyData
- {
- EnemyParam* enemyparam;
- FlyParam* flyparam;
- GroundParam* groundparam;
- SeaParam* seaparam;
- Status* s;
- Hoge* h;
- Piyo* p;
- };
- // 空中敵の挙動
- struct FlyingEnemyFunc
- {
- void update(EnemyParam* enemyparam, FlyParam* flyparam);
- };
- // 地上敵の挙動
- struct GroundEnemyFunc
- {
- void update(EnemyParam* enemyparam, GroundParam* groundparam);
- };
- // 水中敵の挙動
- ...
データ指向で作っておけば○○Paramを用意して各々の関数に渡してやれば大体の対応は完了します。
さらにデータ指向は依存関係が分かりやすいので移植も楽にできたりすると思います。っていうか普通のCっぽくなります。コンテキスト指向とでも呼びましょうか。
個人的には現在でもオブジェクト指向よりデータ指向のほうが利点が多いと思うんですよね。普及する上での問題は、なんだかんだでオブジェクト指向がうまく行ってしまってるから、大規模プロジェクトで使われるなどの実績がない、データ指向をサポートした言語が無い、とかですかね?
あ、データ指向はオブジェクト指向を置き換えるものではなく補完する関係にあると思います。GUIとかはオブジェクト指向のほうが書きやすそうだなと思います。
大失敗! という煽りはある一面を誇張しすぎですが、示唆の多い記事だと思います。
OOPにあまり夢を見ないというか、抽象的に表現された売り文句にあまり惑わされずに使うようにしています。
テストのしにくさは、直交性の高いデザインを心かげたり、かなり早い段階でテスト計画を立てるようにしています。
時々やる小技は、外部からアクセスされないようにデザインしている公開メソッドとして内部ルーチンをテストするようにしたりしています。
ユーティリティー関数をオブジェクトの状態に依存しないスタティック関数として書いて、単体の自動化テストを簡単にすることです。
もう一つの注意は、現実世界の「もの」をデザインする、という幻想というか売り文句から自由になることです。
基本的には(多くの場合)リファレンスに記載されているように動くだけの話です。
以前書いた投稿があります。
オブジェクト指向のプログラミングの問題点は何ですか?に対する山本 聡 (Satoshi Yamamoto)さんの回答
が、その回答は長ったらすぎるので、短めに書くと、
「オブジェクト指向のプログラムは難しいと言われる理由」は、
やれることがいろいろあり、また、それによって起きる制限もいろいろありすぎて、わけがわからない、から、だと思います。
継承だのインターフェースだの多態だのなんだのかんだのといろいろありすぎです。
そしてこれら全てが、どうでもいい概念です。
ifの分岐やforのループや、処理共通化、データを得て、表示を行う。プログラムの重要な概念ってこういうものだけですし、これで多くのアプリケーションの機能を作り出すことができるはずですが、
オブジェクト指向の概念というか、それらの機能はアプリケーションを作るという本質とはかけ離れた機能なので、どう使ったらいいのか本当のところが誰にもわからない、のでアレコレの派閥があってアレコレの議論が行われるので、決まった道がないために学ぶ人は苦労するということになったりします。
自転車置き場の議論として知られる『原子力発電所の建設ミーティングのときに、より重要なことは難しくて議論の対象にはならず発電所の自転車置き場の屋根の色はどうしようかと議論に花が咲く』という例え話がありますが、
オブジェクト指向であーだのこーだの言っているのは
以前書いた投稿があります。
オブジェクト指向のプログラミングの問題点は何ですか?に対する山本 聡 (Satoshi Yamamoto)さんの回答
が、その回答は長ったらすぎるので、短めに書くと、
「オブジェクト指向のプログラムは難しいと言われる理由」は、
やれることがいろいろあり、また、それによって起きる制限もいろいろありすぎて、わけがわからない、から、だと思います。
継承だのインターフェースだの多態だのなんだのかんだのといろいろありすぎです。
そしてこれら全てが、どうでもいい概念です。
ifの分岐やforのループや、処理共通化、データを得て、表示を行う。プログラムの重要な概念ってこういうものだけですし、これで多くのアプリケーションの機能を作り出すことができるはずですが、
オブジェクト指向の概念というか、それらの機能はアプリケーションを作るという本質とはかけ離れた機能なので、どう使ったらいいのか本当のところが誰にもわからない、のでアレコレの派閥があってアレコレの議論が行われるので、決まった道がないために学ぶ人は苦労するということになったりします。
自転車置き場の議論として知られる『原子力発電所の建設ミーティングのときに、より重要なことは難しくて議論の対象にはならず発電所の自転車置き場の屋根の色はどうしようかと議論に花が咲く』という例え話がありますが、
オブジェクト指向であーだのこーだの言っているのはこの自転車置き場の議論に近いことが起きます。誰もが大好きな議論ですが、そこにはソフトウェアの機能としての本質はなく、正解もないのでいつまでたっても議論が終わりません。
「クラスごとに纏まっていて見やすいように思えます」というのは
何に対して見やすいのか、ということですが、クラスにまとまるのはグローバルに散らばっているのよりかはマシなのですが、クラスメソッドを書くという時点で、クラスは小さなグローバル領域になっているだけなので、
グローバル変数とかグローバル関数とかだけでアプリケーションを作るのがなかなか困難なのと同じように
アプリケーションが複雑化してクラスがどうしても肥大化しなければいけない時に、一気に複雑さがまして、他人のコードを読むことが困難になるのが、オブジェクト指向です。
クラスの責務がなんたらかんたらと言い出す人もいるのですが、まあ終わらない議論に巻き込まれるだけになりますね。
ということで、副作用がない(あるいは少ない)、参照透過性が高くてテストしやすい関数型プログラミングが流行ってきているようです。
こちらは小さなグローバル領域などは作らないのでアプリケーションの肥大化に対して非常に強く複雑さを隠蔽する仕組みになります。
関数型プログラミングというのも、それなりにあやふやな概念なので議論を呼ぶみたいですが。そのあたりは詳しくないので、どこか他のQuoraで答えをみつけてください。
関数型というか昔ながらの構造化プログラミングですね。
オブジェクト指向を捨てた所に正解があるとは思ってもみませんでした。私、業界20年くらいというかプログラミング歴20年くらいですが、ようやく気が付きました。
ありがたいことに気がついてからは、オブジェクト指向で複雑にからまった状態のプログラムであっても、リファクタリングしたりしてもより簡単なコードをどんどん書けるようになって楽ですよ。
英語では「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には必須ではない」という指摘は本末を転倒しています。
脚注
私はC言語とJavaが得意なので、オブジェクト指向プログラミング言語Javaの弱点を、構造化プログラミング言語Cに比べて挙げてみます。
—
(1) クラスを定義しないといけない
Cは、関数が基本単位です。何が入力となって、何が出力となるかを考えれば、どんなプログラムでも書くことができます。(main関数も、プログラムの引数が入力になって、プログラムの終了コードが出力となる関数の一つです)
比べてJavaは、クラスが基本単位です。どのクラスが処理を担当するかを決めねばなりません。ユーティリティ関数をたくさん書いて、それを組み合わせてプログラミングする人にとっては面倒です。具体的には、Javaの場合はクラスメソッドを使うことになると思いますが、メソッドを細かくすればするほど、クラスの数が増えます。クラスの名前を考えるのが面倒です。メソッドを実行する際に、いちいちクラス修飾子を指定しないといけないのも面倒です。
Javaの方が、コードの記述量が増えると思います。
—
(2) 状態がある
オブジェクト指向で書こうとすると、メンバ変数を使うことになります。メンバ変数は、オブジェクトの状態です。オブジェクトが状態を持つと、メンバ関数を実行する度に異なる結果が返ってくるようになります。
C言語でいうグローバル変数です。バグの温床になります。
もちろんJavaの場合は、カプセル化されているためメンバ変数が管理されてい
私はC言語とJavaが得意なので、オブジェクト指向プログラミング言語Javaの弱点を、構造化プログラミング言語Cに比べて挙げてみます。
—
(1) クラスを定義しないといけない
Cは、関数が基本単位です。何が入力となって、何が出力となるかを考えれば、どんなプログラムでも書くことができます。(main関数も、プログラムの引数が入力になって、プログラムの終了コードが出力となる関数の一つです)
比べてJavaは、クラスが基本単位です。どのクラスが処理を担当するかを決めねばなりません。ユーティリティ関数をたくさん書いて、それを組み合わせてプログラミングする人にとっては面倒です。具体的には、Javaの場合はクラスメソッドを使うことになると思いますが、メソッドを細かくすればするほど、クラスの数が増えます。クラスの名前を考えるのが面倒です。メソッドを実行する際に、いちいちクラス修飾子を指定しないといけないのも面倒です。
Javaの方が、コードの記述量が増えると思います。
—
(2) 状態がある
オブジェクト指向で書こうとすると、メンバ変数を使うことになります。メンバ変数は、オブジェクトの状態です。オブジェクトが状態を持つと、メンバ関数を実行する度に異なる結果が返ってくるようになります。
C言語でいうグローバル変数です。バグの温床になります。
もちろんJavaの場合は、カプセル化されているためメンバ変数が管理されているとか、オブジェクトがスコープから外れると消えたりするので、C言語のグローバル変数ほど酷くはありません。
それでもバグの少ないコードを書こうという意識の少ないプログラマのコードを見ると、どういう動作をするか頭が痛くなります。便利だからという理由でメンバ変数を多用しているからです。
IDEのデバッガを使ってメンバ変数の状態を覗くことが必須になります。
—
(3) 設計が必要
私のスキルの問題かもしれませんが、C言語の場合は、顧客の要件を聞いただけで特に設計を行わなくても、直接プログラミングすることができます。
Javaの場合は、オブジェクト指向設計が必要です。UML等を使って、どのクラスにメソッドを割り振ろうとか考えねばなりません。
逆に他人が書いたJavaのソースコードを読む場合は、設計資料が必須。設計資料が無い場合は、クラスの設計思想を知るために、自分でUMLを書くことになります。
オブジェクト指向は難しいです。
—
以上、オブジェクト指向の弱点について、私の主観でした。
—追記ここから
オブジェクト指向は素晴らしいです。
上に挙げた弱点は、そうではないことに気づきました。具体的には、、、
(1) クラスは、メソッドをまとめる物です。メソッドをクラスに整理し、直接表に出ないメソッドをprivateにすれば、その分考えないといけないメソッドを削減できます。
(2) クラスは、無理にインスタンス化する必要はありません。staticメソッドで処理を書き、状態を覚えておく必要に駆られた時に初めてインスタンス変数を使いインスタンス化するようにすれば、バグの温床になることはありません。
(3) 私のスキルの問題でした。オブジェクト指向でも、いきなりコードを書くことはできます。
つまるところ、オブジェクト指向の弱点は、「習得するのが難しい」この一点です。
Niwa Yosuke 氏による回答
が現在この質問への回答として評価・露出が高く、内容については誤りがあると思うので回答します。
本来QuoraのQAでは直接的な回答が望ましく、他者の回答とのクロスオーバーの議論はプラットフォームの特性よりあまり求められているものではないと観察していて、当初は当該回答のコメントで反論を書いていたところ、こちらに質問しながらもコメントがブロックされるというような不合理な対応があったのでこちらで切り出します。
まず、前提として、当該回答の骨子であるだろう
さらに、ドメインの複雑性(状態遷移・副作用)を隠蔽・カプセル化で解決できなかった反省から注目を集めたのが、関数型プログラミングであり、immutable なデータコンテナです。
この言及は、次に指摘するとおりの事実誤認から逆算されたもので、総体としてはそのような「時流」があったにせよ、関数型がImmutableであるという原理原則は、数学がImmutableの原理原則であるのと全く同じで、まるでオブジェクト指向の反省からアンチテーゼとしてそれがあるというような書き方はご本人がそのつもりはない、ということだろうと誤解を招く書き方であると感じる。
これらはドメインにある複雑性(状態遷移・副作用)から、複雑さを帯びないきれいな部分(参照透過的な部分
Niwa Yosuke 氏による回答
が現在この質問への回答として評価・露出が高く、内容については誤りがあると思うので回答します。
本来QuoraのQAでは直接的な回答が望ましく、他者の回答とのクロスオーバーの議論はプラットフォームの特性よりあまり求められているものではないと観察していて、当初は当該回答のコメントで反論を書いていたところ、こちらに質問しながらもコメントがブロックされるというような不合理な対応があったのでこちらで切り出します。
まず、前提として、当該回答の骨子であるだろう
さらに、ドメインの複雑性(状態遷移・副作用)を隠蔽・カプセル化で解決できなかった反省から注目を集めたのが、関数型プログラミングであり、immutable なデータコンテナです。
この言及は、次に指摘するとおりの事実誤認から逆算されたもので、総体としてはそのような「時流」があったにせよ、関数型がImmutableであるという原理原則は、数学がImmutableの原理原則であるのと全く同じで、まるでオブジェクト指向の反省からアンチテーゼとしてそれがあるというような書き方はご本人がそのつもりはない、ということだろうと誤解を招く書き方であると感じる。
これらはドメインにある複雑性(状態遷移・副作用)から、複雑さを帯びないきれいな部分(参照透過的な部分・immutable 変数)を切り出して、その部分をシンプルに扱うことで全体の複雑さ(オブジェクトの状態遷移の組み合わせ爆発)を軽減しています。
ここが間違いで、根底には関数型は状態遷移・副作用がうまく処理できない、「きれいな部分・Immutable」のみを処理する、という誤解がある。
しかし、切り出した余りとなる汚い部分は依然オブジェクト指向の守備範囲に押しやられているだけであり、これらのパラダイムはドメインの複雑性を完全解決するものではないのです。オブジェクト指向を補完するものではあっても、取って代わるものではありません。
関数型はImmutableで状態遷移が扱えないのだから、Mutableな「汚い部分」残りの部分は非宣言型でオブジェクト指向の守備範囲でカバーするしかない、つまり関数型のパラダイムはドメインの複雑性を完全解決するものではない(よくわからない言及だが、おそらく関数型だけで全部できないということを言いたいのだと思う)といことらしい。
基本的に、関数型は状態遷移・副作用がうまく処理できない、「きれいな部分・Immutable」のみを処理する、という誤解はずっと前からあって、自分はこんな初歩的な知見の欠落は、昨今のReactからはじまる宣言型UI、別の文脈の用語でいうとFRPのWebUI世界での成功、裏を返すとDOM、文字通りオブジェクトモデルで、オブジェクト指向時代のオトシゴの関数型化で、とっくに誤解はなくなった、と思っていましたが、まだこんなことを言ってる人がいて、そこそこ高評価も受けている、と愕然としました。
やたら「自身のドメイン」だとか私自身の専門分野のように言われているが、別に私はWebプログラマーではないし、単に広大膨大なエコでの著名な事象なので事例として挙げたまでです。
別に私はゲームプログラマーでもないが、最近のゲームエンジンでの関数型化、特にUnrealのVerse言語がHaskell開発者を招いた関数型言語実装でFRPになっていることも事例として挙げました。
これは、
これらはドメインにある複雑性(状態遷移・副作用)から、複雑さを帯びないきれいな部分(参照透過的な部分・immutable 変数)を切り出して、その部分をシンプルに扱うことで全体の複雑さ(オブジェクトの状態遷移の組み合わせ爆発)を軽減しています。
しかし、切り出した余りとなる汚い部分は依然オブジェクト指向の守備範囲に押しやられているだけであり、これらのパラダイムはドメインの複雑性を完全解決するものではないのです。オブジェクト指向を補完するものではあっても、取って代わるものではありません。
という当該回答者が書いた「彼がそう思ってる事実」を反証するものです。
なぜならば、WebUIのReact系ライブラリもゲームエンジンの関数型言語もFRPで、ドメインにある複雑性(状態遷移・副作用)をか宣言的にImmutableに扱うから。
繰り返しますが、状態遷移が関数型で扱えず、オブジェクト指向に押しやらなければいけない、という事実などありません。机上の空論ではなく、リアルな事例として状態遷移の権化みたいな「ドメイン」であるWebUIとゲームエンジンの実情をわかりやすく列挙したまでです。私がそれらのドメインの専門家であるから我田引水したのではありません。
したがって、
Web 系は大きな分野ですが、それはなぜかといったら、放送になり代わりマスにリーチする BtoC 媒体だからです。しかし、大衆受けするから、よく目に触れるからといって Web 系だけで IT が成り立っていると思い上がるのはいただけません。
というのは、典型的なストローマン論法で、誰もマス、大衆受けするからWeb系だけでITが成り立っている、思い上がっているなど論を展開している人は自分も含めいないし、単に、彼が状態遷移を扱えないから、という間違った知識の最もわかりやすい反証としての事実を提示されたまでだということを理解したほうが良いし、他者が言ってもいないことで非難すべきではないですね。
あなたが知らないであろう基幹業務系では、激しく深化した「ビジネスの構造データとロジックの複雑さ」を扱うためにオブジェクト指向プログラミングの「階層構造化」と「差分プログラミング」がどうしても必要なのですよ。(逆に Web 系では私もオブジェクト指向の必要性をさほど感じません。)
程度によるが、どの程度のレベルの基幹業務系の話ですか?あなたが使うC#でやる程度のやつですよね?
WebUI、ゲームエンジンの話を出したのは私がそのドメインの専門家であるから、という理由ではないが、別に基幹業務系の話だってできる。
似て非なる派生パターンのロジックやデータコンテナが何十もあるのに継承を使うなと? 業務ロジック最小単位を僅か1つ書くのにメソッドチェーンをいくつつなげると思っていらっしゃるのですかね。C や PHP のように method1(method2(medhod3(…))) と書けとでもおっしゃるのでしょうか?
オブジェクト指向のメソッドチェーンは、単に代数構造の別の表現であり、多分彼が知らない事実とは、それは関数型のFunctorやMonadで代替できるので、
F#なら、 F |> method1 |> method2 |> method3 とメソッドチェーンと同じようにパイプライン演算のFunctor代数構造で書くだろうし、
Haskellなら、 M >>= method1 >>= method3 >>= method3 と Monadの代数構造で書くでしょう。
OOのメソッドチェーンの表記が数学の代数構造の表現の1スタイルにすぎず、それは関数型では単に二項演算で書けるということもご存じないんでしょう?
アンチ・オブジェクト指向派という存在は、オブジェクト指向の使いどころを分かっていないながら、適合性の高くない Web でムリヤリ使ってみて嫌いになった人たちのことではないかと私は邪推しています。Web 系の一部の人のオブジェクト指向の叩きっぷりはまあ偏執的です。
まさに邪推で、間違い。
アンチ・オブジェクト指向派という存在は、ろくに関数型コードを書けないくせに、それは上記のメソッドチェーン云々の極めて初歩的な誤解しかいてないことからも判別できるが、中途半端に反論してくる人々にうんざりしているのだと思う。
これは、まさに質問の「オブジェクト指向プログラミング -- 1兆ドル規模の大失敗」 という記事にも書かれていて「反論が浅い部分にとどまる」「擁護者はろくに関数型コードが書けない連中しかいない」とあるとおりです。
たとえば次のような言及もそうだ。
そのように揶揄されるくらい、基幹業務系のデータやロジックは Web 系に比べてはるかに複雑なのですよ。オブジェクト指向のアシストなく、データ型を明示的に統制しない動的型付け言語などでは扱えない世界なのです。
なんで、オブジェクト指向でなく関数型、という話で、動的型付け言語で扱えないとかいう話になるのかさっぱり理由がわからないが、わかることは、何も理解してないんだろうな。。ということくらいですね。
現代の関数型言語は、静的型付け言語で、WebUIですらJavaScriptでなくTypeScriptを使う。
まあ、自分の知っている世界がすべてだと思って Quora で鼻息荒く噛みついてくるのはあなただけに限らず、 Web 系には多くいますけどね。BtoC が世の中のすべてだと思ってしまうところは就職前の学生さんに似てますね。あなたが私の論を理解できないのは、あなたの知らない世界があるということです。
そのまま返しましょう。
そういった事情を踏まえず、世界の半面しか知らないのに自分の価値観だけで上から目線で断じるから軽薄な意見だなぁと感じたまでです。
事情というか、基本的な知見がなく、プログラミング世界の殆どを知らないのに、軽薄な意見を書いてるのは自分ではなく当該回答者だと断定せざるをえない。
というか、コメント拝見していると多分C#使いの人なんでしょ?同じ.NETフレームワークにある関数型言語のほうのF#は使えないんですか?F#使えてたたらこんなトンチンカンな問答は出ようもないと思うのですが。「FRPのことは知ってます」みたいに「F#のことはもちろん知っています」とは言いそうだが、使えないのは明白だ。関数型プログラミングのことなど何も知らない。
なにかまだいいたいことがあるのならば、コメントでの反論を歓迎します。
今日、A君はB君の家に遊びに行く予定です。
A「そろそろ行くか。先にB君に連絡を入れておこう」
B「・・・」
「二足歩行で行くね」とは言いません。なぜなら二足歩行は特別でもないし、普段からしていることだからです。
要するに言わんでもわかるやろということです。
—
では、こちらの犬はどのように説明しますか?
「犬という役割に飽いてしまった犬」または「二足歩行している犬」ですね。これを見て、「犬が歩いてる」と説明しても、聞き手は四足歩行を想像するでしょう。
では、「歩く」という言葉の装飾として「人が四足歩行する」は不適切で、「犬が二足歩行する」が適しているのは何故なのでしょうか。
—
我々は無意識に、会話する相手との間にある、共通認識について考えながら会話をしています。
部屋に入って来るなり,妹がぼくに話しかけてきた。①ふり返って、妹を見た。
①は主語が省略されています。さて、①の主語は何でしょうか?
正解は「僕」です。
例文から想像する図としてはこうですね。
これを次のように想像する方は「誰が振り返ったんだ?」と思うかもしれません。
厳密には、部屋の中に兄や友達などが居るかもしれませんが、むしろそれこそ省略せず説明すべき事項だと、人は無意識に考えるわけです。
要するに「なんの説明も無ぇってことは自分が振り返ったんだろうな」ということです。
つまり極端な話、B君の反応にもある通り、普段二足歩行している人が「四足歩行で行く
今日、A君はB君の家に遊びに行く予定です。
A「そろそろ行くか。先にB君に連絡を入れておこう」
B「・・・」
「二足歩行で行くね」とは言いません。なぜなら二足歩行は特別でもないし、普段からしていることだからです。
要するに言わんでもわかるやろということです。
—
では、こちらの犬はどのように説明しますか?
「犬という役割に飽いてしまった犬」または「二足歩行している犬」ですね。これを見て、「犬が歩いてる」と説明しても、聞き手は四足歩行を想像するでしょう。
では、「歩く」という言葉の装飾として「人が四足歩行する」は不適切で、「犬が二足歩行する」が適しているのは何故なのでしょうか。
—
我々は無意識に、会話する相手との間にある、共通認識について考えながら会話をしています。
部屋に入って来るなり,妹がぼくに話しかけてきた。①ふり返って、妹を見た。
①は主語が省略されています。さて、①の主語は何でしょうか?
正解は「僕」です。
例文から想像する図としてはこうですね。
これを次のように想像する方は「誰が振り返ったんだ?」と思うかもしれません。
厳密には、部屋の中に兄や友達などが居るかもしれませんが、むしろそれこそ省略せず説明すべき事項だと、人は無意識に考えるわけです。
要するに「なんの説明も無ぇってことは自分が振り返ったんだろうな」ということです。
つまり極端な話、B君の反応にもある通り、普段二足歩行している人が「四足歩行で行くわ」と言うのは適していると言うことですね。
—
ですので、例えば次のような「オブジェクト指向以外があるかもしれない」場合には「オブジェクト指向」という言葉を用いて会話することになるでしょう。
- 普段手続き形プログラミングをすることが多いベンダーに対して指示をするとき。
- 手続き形プログラムで組まれることが多いシステムを、オブジェクト指向で組んで欲しいとき。
—
ということで、ご質問への回答は、
選択肢としてオブジェクト指向以外の何かがある時に、差別化のために便利な言葉です。
としたいと思います。
—
例文出典:
オブジェクト指向プログラミングの構成要素ごとの有用性が再吟味され、良し悪しもわかってきて、是是非非で悪いところは適宜置きかえていった方がいいのでは、となってきたとき、「オブジェクト指向プログラミング」という言葉で指す総称的概念として扱うことの意義が侵食されてきているのではないでしょうか。「熟成して普及し、名前を呼ぶ必要がなくなった」というような状況に向かっているのかもしれません。
たとえば、昨今、「構造化プログラミング」ってわざわざ言いませんよね。「アスペクト指向プログラミング」という鳴り物入りででてきたやつが、今は「AspectJ使ってます」「DI使ってます」ぐらいで済むことになっています。DIコンテナなんて言うこともなくなりましたかね。Spring Boot使ってます、でおしまいです。
なので、クラスならクラス、継承なら継承、オブジェクトならオブジェクトという言語機能名称を、パーツとしてそれぞれ使うときにそう呼べば良いような気がします。「オブジェクト指向プログラミング」と総称して言う必要って、あるのかな、と。あるとしてそれって実体として何だっけかなと。
たとえば、自分では、おおむねクラスライブラリを呼びだしたりはするし(String#toUpperCase()とか)、ときおり自前のデータ構造のためにクラス定義もします。まれに継承もします。でも関数型でmapとfilterを連ねる方がよ
オブジェクト指向プログラミングの構成要素ごとの有用性が再吟味され、良し悪しもわかってきて、是是非非で悪いところは適宜置きかえていった方がいいのでは、となってきたとき、「オブジェクト指向プログラミング」という言葉で指す総称的概念として扱うことの意義が侵食されてきているのではないでしょうか。「熟成して普及し、名前を呼ぶ必要がなくなった」というような状況に向かっているのかもしれません。
たとえば、昨今、「構造化プログラミング」ってわざわざ言いませんよね。「アスペクト指向プログラミング」という鳴り物入りででてきたやつが、今は「AspectJ使ってます」「DI使ってます」ぐらいで済むことになっています。DIコンテナなんて言うこともなくなりましたかね。Spring Boot使ってます、でおしまいです。
なので、クラスならクラス、継承なら継承、オブジェクトならオブジェクトという言語機能名称を、パーツとしてそれぞれ使うときにそう呼べば良いような気がします。「オブジェクト指向プログラミング」と総称して言う必要って、あるのかな、と。あるとしてそれって実体として何だっけかなと。
たとえば、自分では、おおむねクラスライブラリを呼びだしたりはするし(String#toUpperCase()とか)、ときおり自前のデータ構造のためにクラス定義もします。まれに継承もします。でも関数型でmapとfilterを連ねる方がよっぽどロジックの中核だったりするので、「自分、今オブジェクト指向プログラミングしてるんだっけ?」と思います。ひょっとすると「たまにクラス定義してるだけ」じゃないか? と。
「大規模な業務システム」を作る際に起こることを考えてみます。私は一方的にソフトウェア開発の人なので、ソフトウェアを中心に考えます。
すんげぇバカみたいなところから入ります。まず、「大規模」なんですから、一人では作らないでしょう。多分、実際にそのシステムにおいてプログラミング作業をする人は数十人、数百人、数千人、数万人……
……としますと、明らかに分業が必要ですね。システムを部分に分けて、それを統合する、という開発を行う必要があります。ソフトウェアの「設計」です。
「業務システム」ですと、導入企業におけるいろいろな業務をこなすシステムであるはずです。「業務」のための「システム」のはずです。
それは「給与計算」「人事管理」「仕入れの管理」「顧客の管理」「動員数の管理」「お客さんの口座の管理(金融)」「お客さんの講座の管理(教育系サービス)」と、いろいろ大きな分野に分けられると思われますし、その中で、人事ならさらに分解して「出退勤の管理」「給与の管理」とか分けていくことになりますね。これまた、分業であり、設計が絡みます。
ここでは、このときに必要な「設計」によって出来るソフトウェアの単位を「モジュール」とでも呼んどきましょう。
大きいモジュールの中に小さなモジュールが相互に連結して、1体のソフトウェアとして動作し、その大きなモジュール同士も連携して、最終的には「大規模な業務システム」になります。そう
「大規模な業務システム」を作る際に起こることを考えてみます。私は一方的にソフトウェア開発の人なので、ソフトウェアを中心に考えます。
すんげぇバカみたいなところから入ります。まず、「大規模」なんですから、一人では作らないでしょう。多分、実際にそのシステムにおいてプログラミング作業をする人は数十人、数百人、数千人、数万人……
……としますと、明らかに分業が必要ですね。システムを部分に分けて、それを統合する、という開発を行う必要があります。ソフトウェアの「設計」です。
「業務システム」ですと、導入企業におけるいろいろな業務をこなすシステムであるはずです。「業務」のための「システム」のはずです。
それは「給与計算」「人事管理」「仕入れの管理」「顧客の管理」「動員数の管理」「お客さんの口座の管理(金融)」「お客さんの講座の管理(教育系サービス)」と、いろいろ大きな分野に分けられると思われますし、その中で、人事ならさらに分解して「出退勤の管理」「給与の管理」とか分けていくことになりますね。これまた、分業であり、設計が絡みます。
ここでは、このときに必要な「設計」によって出来るソフトウェアの単位を「モジュール」とでも呼んどきましょう。
大きいモジュールの中に小さなモジュールが相互に連結して、1体のソフトウェアとして動作し、その大きなモジュール同士も連携して、最終的には「大規模な業務システム」になります。そういうことにしておきます。
この想像に基づいてシステムを考えますと、モジュールによって相互に連結したソフトウェアの塊であり、それは複雑であり、ひょっとすると数万人が「実際にプログラムする」だけで関わる「どでかい野郎」です。
私がこの回答を考えるうえで、今すでに中で「モジュール」という考え方を取り入れました。モジュールという考え方を取り入れず、全部一体で開発するというのはないんでしょうか。……ないんですかね??
うーん……ソフトウェアや、概ね人間が作る「どでかい野郎」の類はそういう「モジュール」ベースでしか作れていないのではないかな、と思います。それはソフトウェアに限らず、ビルでも橋でも、自治体でも政府でも、概ねそれぞれ担当する場所が決められて、それらの相互連携を意識して作られますね。
え、そういうもんじゃない例があるかって?はい、多分、自然に類するものはむしろ分割して設計なんてされてないというケースが支配的ではないかなと思います。全部一体となって相互連動するなかでクッソ長い期間をかけて全体が調整されて一つのものとして整形されます。
「今後は、この木の実を食べる生き物が要るので、そのようになります!」なんていう設計は、人間はできていません(「神様」がそうしているのかもしれませんけど)。小学生が朝顔の色水を作るのすら、モジュールにならんのです。あ、このあたりは私の妄想に基づいてますよ。
さて、自然という御大層なものを考察するには私の脳はソフトウェアエンジニア過ぎます。あくまで「大規模」な「業務」「システム」という、ヒトが作るものについての話なので、「モジュール」です「モジュール」。モゥジュゥ〜ル!
最近とあるゲーム機の充電兼ディスプレイ出力用の「ドック」が壊れちゃいましてね。興奮すると頭から炎が吹き出るキャラが登場するリングがフィットするフィットネスゲームを遊んでいたんですが、それで遊ぶにはそのドックがゲーム機本体とは別に必要でして、つまり運動できないんですよ。モジュールどうしの連携が悪いと、人間のシステムは動作せず、私の体重はシステマティックに増え続ける。あ、また話がそれた。
モジュールの相互の連携が大事であり、業務システムを構築する際にはそのモジュールを一つづつ作るわけには参りません。数万人が「まずNo1〜No10の方、1ヶ月作業をお願いします」「次、No11〜No20の方、No1〜No10の方が完成させたモジュールを使って次のモジュールをお願いします」なんてやりません。多分数百〜数千のモジュールは、それぞれ並行して作られる。ソフトウェアのモジュールですから、プログラミングは並行して行われるです。
当たり前な話してるって?……まぁまぁ
「質問です〜」はい?「並行して作られるプログラムですが、どうやって相互に連結して動くことを保証するのでしょうか?」ってプログラマNo2303が疑問に思います。多分
2303番、何を当たり前なことを……。関係するモジュール間でやり取りする内容や、物理的な形状を先に決めておくんですよ。「インターフェース」とか「インターフェース仕様書」って言葉があるんですよ。
Interfaceですよ。雑誌じゃないですよ。あなたJavaをお使いでしょ? implements って書くでしょ?え、書かない? じゃ、extends?
「モジュール」と「インターフェース」……ヒトが作りし「どでかい野郎」で共通して必要な考え方と、なんか偶然にプログラミングJavaの言葉が混ざってしまった。それはともかくです!
一緒に一つのものを達成するためには協業のルールが大事です。てか、必須です!
みんなで並行して作成して、自分なりに正しいかをテストして(ユニットテスト)、あとでがっちゃんこして、隣同士で「えー、言ったとおりに動いてないじゃん」「えー、そんなこと『インターフェース』の『仕様書』に書いてないじゃん」ってやりとりを行い(インテグレーションテスト)、だいたい全体として揃ってきたら「せーの、で全部のモジュールに関係する『Mr.サタン』さんの情報を流しますので、おかしかったら言ってね」「おちたー!なんで?隣のモジュールとはうまくいったのに?ナンデ?」(システムテスト)ってやって、
ヽ(=´▽`=)ノおめでとう、「大規模な業務システムが動いた」
ってなるんだなぁ、って思います。
課題が起きた場合にどのように解決するかは業界、ソフトウェア開発技法によって違うでしょうが、「大規模な業務システムを作る」上での大事なこととしてどの業界でも「モジュール」と「インターフェース」は使いそうだなぁ、っていうのが、私の考えです。
うぇーい長い。
質問の回答に近づけましょう orz
「大規模な業務システムを作る」上で何がほとんど必須不可欠か?
人が営利事業なんかで行う正常な大規模作業であれば、上述の通り分業があるわけです。趣味で一人でOperating Systemを作る場合と違って、納期と人件費がありますし。あるいは、一部のモジュールは外の既製品が優秀だったら、調達することもあります。GUIに日本の道路の情報表示したい?自分で地図データは作らないと思いますね。○○リン!○○リン!
いずれにしても、それと結合する外のモジュールや部分的なシステムとは何らかの規格、インターフェースでやりとりする取り決めをします。
必須不可欠なのはおそらくは分業、それに連動する「モジュール」「インターフェース」です。これは、「大規模な業務システム」には必須と考えて良いでしょう。
で、あれ、Javaが必要なんでしたっけ。オブジェクト思考が必要なんでしたっけ……
「Javaのようなオブジェクト指向言語」と言われて思い出すのは、私が初めてJavaを1.4あたりで勉強した際に「オブジェクト指向言語とは、カプセル化、継承、多態性である!」なんて教わったことなんですよ。ご質問された方はこの3つのことをおっしゃっているのでしょうか?
でも、さっきのわたしの妄想に「カプセル化」「継承」「多態性」なんてどこにも入ってないッス。私にはソフトウェア開発の経験は10年くらいしかないですが、3要素のどれも別に「必須不可欠」ということでもなくて、ちょっと漏れてても、まぁ対策はあるよなと。
というより、たいてい最近のプログラミング言語は意識的にかそうでなくか、昔のオブジェクト指向言語の金言に、厳密には従っていないことのほうが多いんじゃないでしょうか。TypeScriptでもHaskelでもRustでもいいんですが。
でも「インターフェース」は……必要なんです。Javaのinterfaceのことだけではないです。protobufでもいいんです。OpenAPI、あぁ愛しのOpenAPI。API … Application Programming Interface。おお、インターフェース!!
未だに「オブジェクト指向」がなんであるか、私は実はよくわかっていないんですよ。昔は「カプセル化」「継承」とか色々言われていたんですが、PythonやRubyはOOPの概念を十分持っているはずなのに本気のカプセル化はほとんどできない。Go言語に「継承」って、似たものはあったはずですが、ダイヤモンド継承はちと現実的ではないかなと思います。多態性……リスコフの置換原則は勉強になった!それだけだ!
まぁそれは冗談です。とはいえ、真剣な話、ご質問のおっしゃります……
「Javaとかのオブジェクト指向言語」「でないと」「大規模な業務システムを作るのが難しい」という命題は「質問の意味が、よく、わからん……」というのが正直なところです。PythonもOOP出来るし、Goは……OOPではないと私は思いますが、インターフェース的なのはあるので頑張れるし、Googleを含む多数のIT企業が割と大きな規模で実戦で使っています。Linux KernelはOOPとは言えないC言語とアセンブラで、OOPの機能は揃ってないけど、OOPっぽく責任分界点をうまく切る設計をして、めっちゃソースコードが大きい中でもめっちゃ使われていて、あれを「大規模システム」と言わなかったら何が大規模なんだろう、ってレベルにかなり近いですね。COBOLだって出来るんですよ、……多分!
こういった「OOPじゃなくても大規模でやってるんだぜ」という例について、いずれのプログラミング言語、プラットフォームにも言えることがあると思います。
「インターフェースは大事なんだぜ」という認識は当然のごとく持っていて、言語の設計にはインターフェースを形成する方法が含まれていることです。
C言語レベルのバイナリの世界でもABI(Application Binary Interface)の変化には敏感なはず。そして、何らかのレベルでインターフェースがちゃんと定義されていて、利用者と実装者の間で合意が形成され、それが何らかの形で維持されていないと、システムの維持はできないし、それこそ「大規模」なら、稼働する前に共同開発中に空中分解するはずです。
「オブジェクト指向言語でないと」……オブジェクト指向はそのインターフェースの定義において、ソフトウェア開発で大事な考え方を提供しています。再利用性(「GoF」と称される名著は『オブジェクト指向における再利用のためのデザインパターン」とあります!)であるとかそういうものも含めて、世の中への影響はとても大きいものだったはず。
でも、それが「大規模な業務システムを作る」上で「必須」OR「重要」かと言われると、その中の「インターフェースを切って作業を分担し、がっちゃんこできること」がもっと致命的に重要で、必須なんじゃないかなと私は思います。
オブジェクト指向言語と呼ばれる場合、自然とそういうことをやりやすくする仕組みがいくつかは整っていて、それによって業務システム上でのメリットは「多い」と思われます。特にJavaは「業務システム」の業界では結構盛り上がりが長かったと思いますので、結果としてご質問の通り「Javaのような」ってなったんだとおもうんですけど。
なげーよ。まとめます
Javaとかのオブジェクト指向言語でないと大規模な業務システムを作るのが難しいのでしょうか?
→
大事なのはオブジェクト志向かどうかではなく、インターフェースを介したシステムの疎結合化だというのが私の意見です。オブジェクト指向言語はその面で良い面が言語機能として揃っていることが多いゆえ、有利なことも多いですが、直接オブジェクト指向言語と称されない言語でもインターフェースをうまく切ることは出来、事実としてそのようにして大規模システムが成功裏に開発され、維持・運用されている例は多数あります。難しくなるかどうかの分解点は採用される言語が「オブジェクト指向言語かどうか」ではないと思います
似たような質問への回答をここに書きました。
これはオブジェクト指向でプログラミングする必要がある!と思う場合は、どんなアルゴリズムを作ろうとした時でしょうか?に対する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 という記法でアクセスする。
ただそれだけのことです。
この件については、すでに18年も前に Matzにっき(2003-08-06) で書いたことが全てだと思います。
つまり、継承をisa関係にしか使わないという原則に従っていれば問題はほぼなく、逆に一見使い勝手が良さそうに思えるからと言って、isa関係でない機能取り込みに継承を使うと痛い目に遭うということではないでしょうか。
Javaのような(C++からの流れで)抽象データ型から生まれたオブジェクト指向プログラミング言語では、クラスが単なるモジュールに見え、継承が他のモジュールからの機能取り込みに見えるのかもしれません。その延長で単なる機能取り込みに継承を使い、コードが複雑化してしまった上で、「羹(あつもの)に懲りて膾(なます)を吹く」状態が上記の継承忌避なのだと推測します。
継承を使うべきときには継承を使い、そうでない時にはそれ以外の手段(コンポジションとか)を使うのが正しい態度だと思います。
誰がオブジェクト指向を理解するために、動物>哺乳類>犬のようなアナロジーを持ちこんだのでしょうね?
こういったアナロジーはまったく理解の役に立たなかったのではないかと思います。
私はオブジェクト指向に出会う前は、C言語でプログラミングをしていました。C言語でプログラミングをしていると、大抵は問題領域に合わせて構造体を作り、それを関数に渡して内部のデータを操作するというようなことが起ります。そして関数間でそのデータを引き回すのです。
嘗て、「データ構造+アルゴリズム=プログラム」という古典的な名著がありました。そう言われるくらいにデータ構造とアルゴリズムというのはプログラムにおいて切っても切れない関係にあるわけです。
データ構造とアルゴリズムをひとつのパッケージに入れてしまおうというのがオブジェクト指向の発端だと想像しています。
そうすることの利点は、あるコードの集合体を一箇所にまとめて何に関心のあるコードなのか明確することができる。また、それに対して名前をつけることができる。ということです。こうしてコードが表わすある概念に名前をつけて整理するわけですね。これがクラスです。
そうなんです。名前を付けられることはとても重要なことなんです。
名前をつけることで概念を具現化するわけです。そしてもう一歩先に進むと概念同士の関係を構造化することができます。
それが最初に出てきた、動物 < 哺乳類 < 犬のよう
誰がオブジェクト指向を理解するために、動物>哺乳類>犬のようなアナロジーを持ちこんだのでしょうね?
こういったアナロジーはまったく理解の役に立たなかったのではないかと思います。
私はオブジェクト指向に出会う前は、C言語でプログラミングをしていました。C言語でプログラミングをしていると、大抵は問題領域に合わせて構造体を作り、それを関数に渡して内部のデータを操作するというようなことが起ります。そして関数間でそのデータを引き回すのです。
嘗て、「データ構造+アルゴリズム=プログラム」という古典的な名著がありました。そう言われるくらいにデータ構造とアルゴリズムというのはプログラムにおいて切っても切れない関係にあるわけです。
データ構造とアルゴリズムをひとつのパッケージに入れてしまおうというのがオブジェクト指向の発端だと想像しています。
そうすることの利点は、あるコードの集合体を一箇所にまとめて何に関心のあるコードなのか明確することができる。また、それに対して名前をつけることができる。ということです。こうしてコードが表わすある概念に名前をつけて整理するわけですね。これがクラスです。
そうなんです。名前を付けられることはとても重要なことなんです。
名前をつけることで概念を具現化するわけです。そしてもう一歩先に進むと概念同士の関係を構造化することができます。
それが最初に出てきた、動物 < 哺乳類 < 犬のような関係です。動物で例えると分らなくなるので、プログラミングの概念で考えると解りやすいでしょう。
たとえば、プロトコル < TCPとかプロトコル < HTTPとか(え?HTTPとTCPではレイヤが違うんじゃないの?というつっこみが入りそうですが、プロトコルの抽象化のしかたによってはこういうのもありますよ。)
TCPはプロトコルの一種とかHTTPはプロトコルの一種というように言うことができるのです。これを多態性といいます。
TCPであってもHTTPであってもプロトコルとして扱う分には同じように扱えるわけです。こうした考えかたを使えば、例えばある通信ソフトウェアにおいてプロトコルのレイヤを差しかえて使うことができるようになるわけです。
プロトコルの話は一例ですが、このように概念に名前をつけて概念を構造化するのがクラスです。
時間の問題、とまでは言えなさそうですが、じわじわとそうなっていくように思います。
将来的には、おそらく。
オブジェクト指向の中でも特に複雑なクラスベースの継承と多態、それがまずなくなっていくでしょう。
(これがなくなればもはやオブジェクト指向プログラミングとは呼べない気もします。)
継承がない言語がいくつかメジャー言語で出てきているので、継承や多態を使ったコードは移植性が悪くなってしまいます。また、移植性は除いても継承は全てのフィールドメソッドを継承してしまうという辛さがある機能で代替手段もあるため、書かれなくなっていくでしょう。
継承がない言語が登場して広く使われているのも、継承が必須ではない機能だったからです。
言語特性やライブラリ特性があるのですが、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
単純なフラグと当たり判定になっています。
でも、別にこれがブロックごとにオブジェクトのインスタンスと球がインスタンスでも、それは複雑になりようがないので、問題ないです。
オブジェクト指向のオブジェクトの生成(インスタンス)とか、とインスタンスがインスタンスを所有して、そのインスタンスも複数のインタンスからできている、というデータの階層構造的なオブジェクトの使い方は普通で、それは素直なプログラミングだと思います。
ツリー型のデータ構造としてのオブジェクトの利用については複雑なことはないので、有害だとは思わないです。
オブジェクト指向の「流行り」というのは本当にすごかったんですよ。1990年代中盤は、例えばOOPSLA(Object-Oriented Programming, Systems, Languages and Applications: 変な名前ですが、ダジャレ好きな私の知り合いによる命名です)というオブジェクト指向を扱った最高峰の学術会議では、言語の細かな機能の提案などに関する学術的な発表が行われている一方で、巨大な展示会場が併設されて、「オブジェクト指向」をうたう製品を持っている会社が何十社もブースを構え、チュートリアルセッションも大盛況で、参加者が5000人に以上にもなるという、それはそれは大流行りしていたものだったわけです。今でもSIGGRAPHはさらに大きな展示会場付きだったりインストレーション・アートがあったりして大きいですが、OOPSLAもそこまでではないものの似たような雰囲気もありました。
当時のジョークとしては「オブジェクト指向というのは、友達みんながやったことあると言っていて、やってみたらすごく良かったと言ってはいるけれど、本当にやっている子は実はそれほどいないという、高校生がアレについて語っているようなものだ」というものがありました。
「オブジェクト指向」に関する市場規模は90年代よりも現代の方がはるかに大きいわけで、オブジェクト指向は「廃れてきた」というわけではな
オブジェクト指向の「流行り」というのは本当にすごかったんですよ。1990年代中盤は、例えばOOPSLA(Object-Oriented Programming, Systems, Languages and Applications: 変な名前ですが、ダジャレ好きな私の知り合いによる命名です)というオブジェクト指向を扱った最高峰の学術会議では、言語の細かな機能の提案などに関する学術的な発表が行われている一方で、巨大な展示会場が併設されて、「オブジェクト指向」をうたう製品を持っている会社が何十社もブースを構え、チュートリアルセッションも大盛況で、参加者が5000人に以上にもなるという、それはそれは大流行りしていたものだったわけです。今でもSIGGRAPHはさらに大きな展示会場付きだったりインストレーション・アートがあったりして大きいですが、OOPSLAもそこまでではないものの似たような雰囲気もありました。
当時のジョークとしては「オブジェクト指向というのは、友達みんながやったことあると言っていて、やってみたらすごく良かったと言ってはいるけれど、本当にやっている子は実はそれほどいないという、高校生がアレについて語っているようなものだ」というものがありました。
「オブジェクト指向」に関する市場規模は90年代よりも現代の方がはるかに大きいわけで、オブジェクト指向は「廃れてきた」というわけではなく、その技術があまりにも当たり前になったために、わざわざ展示を見に行ったり、あるいは先端の発表を追っておかないと乗り遅れる、というような危機感を巻き起こすことがなくなったとみるべきだと思います。
OOPSLAの系譜を踏む会議SPLASHはオブジェクト指向のみにフォーカスするのではなく、ユーザーインターフェイスと言語の関係など周辺分野に関するワークショップも併設してそれで500人程度の参加者という感じになっています。
そういうことを踏まえると、関数型というのは基礎的な研究は成熟しており(そもそもオブジェクト指向よりも古くからあるものですし)、今からそのような盛り上がりをわざわざ見せるようなものではないのではないか、という気はします。これからくるのはもっと「自動プログラミング」的なものや、「一般向け量子コンピュータ用言語」みたいに、上記の高校生のたとえのように「遅れてるー」とまわりに言われたくない、というおそれを皆が抱くようなものになるのではないかと思います。
あんまりそういう印象はありませんね。昔は「オブジェクト指向プログラミングは万能で、これを使わないのはどうかしている」と業界全体がかなり熱狂していた時代が過ぎ去って、「まあ、便利なこともあるけど、万能ではないよね」とか「オブジェクト指向よりも関数型プログラミングのほうが有効なことも多いよね」いう冷静かつ正常な状態に至ったことを、衰退とか逆風と感じている人がいるということではないでしょうか。
ハイプ・サイクルでいう「幻滅期」をすぎて「安定期」に入った状態だと思います。
昨今、 JSON をやり取りすることが多いんですが、JSON で入ってきたデータの塊にメソッドを生やしてオブジェクトにするっていうのは出来なくはないけど割と面倒です。まして、1つのJSONが複数のエンティティを含んでいるなんてことはよくあるわけで。
例えば、こういう JSON がやってくる。
- input = { users: [
- {id: 1, name: '太郎', age: 15},
- {id: 2. name: '二郎', age: 16}
- ]}
オブジェクト脳だと、JSONをファクトリに渡して必要なインスタンスを全部作る、となるかもしれませんね。
- users = input.users.map(x => new User(x))
- users.forEach(x => { if(x.isBirthday()) x.ageIncrement() })
そんで、、元の形の JSON に戻して返却、と。。
- res.json({ users: users.map(x => x.asJson()) })
だるいっす、、、ダルダルっす!
関数脳だとこうなります。
- input.users.forEach(x => { if(userIsBirthday(x)) userAgeIncrement(x) })
- res.json(input)
こっちのが楽ちんに見えますね。純粋関数狂の人は 「input を改変すんな!
昨今、 JSON をやり取りすることが多いんですが、JSON で入ってきたデータの塊にメソッドを生やしてオブジェクトにするっていうのは出来なくはないけど割と面倒です。まして、1つのJSONが複数のエンティティを含んでいるなんてことはよくあるわけで。
例えば、こういう JSON がやってくる。
- input = { users: [
- {id: 1, name: '太郎', age: 15},
- {id: 2. name: '二郎', age: 16}
- ]}
オブジェクト脳だと、JSONをファクトリに渡して必要なインスタンスを全部作る、となるかもしれませんね。
- users = input.users.map(x => new User(x))
- users.forEach(x => { if(x.isBirthday()) x.ageIncrement() })
そんで、、元の形の JSON に戻して返却、と。。
- res.json({ users: users.map(x => x.asJson()) })
だるいっす、、、ダルダルっす!
関数脳だとこうなります。
- input.users.forEach(x => { if(userIsBirthday(x)) userAgeIncrement(x) })
- res.json(input)
こっちのが楽ちんに見えますね。純粋関数狂の人は 「input を改変すんな!コピーしろ」とか言いそうですが、 JSON をネットワーク経由でやり取りするときにどうせコピー発生するからまあいいでしょ、細けーことは気にすんな。
この例で明らかなように、 Web 開発とオブジェクト指向はバリバリ相性悪いです。オブジェクトを作ってもその寿命があまりに短いので、生成と破棄のコストばかりかかる。一方で、サーバーのなかで生き続ける環境とか、アプリの中で使う長寿命なインスタンスは、オブジェクト指向で扱う方が向きます。
まとめると、短寿命で小さいデータは関数型、長寿命でデカいインスタンスはオブジェクトですね。
余談ですが go とか rust だと JSON のような純粋データに対してメソッドを後付けする仕組みがあって、 class 作って new しなくても x.isBirthday() のようにメソッドを呼ぶことができます。
- input.users.foreach(|x| if x.is_birthday() { x.age_increment() } )
言語も進化してますなあ。やはり新しい言語は良いものですな。
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ではオブジェクトとして表現されていない要素があるから、ということになるでしょう。
オブジェクト指向でも特に「メッセージングのオブジェクト指向」に軸足を置いて学ぼうとするなら、断然、本家のSmalltalkをお薦めします。
普及していないからインストールが大変なのでは…と敬遠される向きもあるようですが^^;、Smalltalkはその誕生以来、処理系と開発環境がGUI付きのOSモドキとして今流行りの仮想化&一体化された状態で配布されるので、IDE込みで考えれば他の人気の言語と同等か、ことによるとそれらよりはるかに簡単に環境の構築が可能です。特に Squeak Smalltalk やその派生の Pharo Smalltalk のインストールは本質的にzipファイルの展開だけで済ませられます。
- Squeak/Smalltalk | Downloads (WinやLinuxではzipを展開、macOSではdmgからコピー)
- Pharo - download (ホストOS用のランチャーをダウンロードして起動後、使用したいバージョンをCreateしてからLaunch)
さらにSqueakに限っては、仮想マシンをJavaScriptで記述したSqueakJSという実装があり、そのデモとして用意された下のリンクから、ワンクリックでWebブラウザ内からGUI込みの本格的なSmalltalkを(非常に古いバージョンかつ機能を絞ったものではありますが─)実に簡単に動かすことができます。
オブジェクト指向でも特に「メッセージングのオブジェクト指向」に軸足を置いて学ぼうとするなら、断然、本家のSmalltalkをお薦めします。
普及していないからインストールが大変なのでは…と敬遠される向きもあるようですが^^;、Smalltalkはその誕生以来、処理系と開発環境がGUI付きのOSモドキとして今流行りの仮想化&一体化された状態で配布されるので、IDE込みで考えれば他の人気の言語と同等か、ことによるとそれらよりはるかに簡単に環境の構築が可能です。特に Squeak Smalltalk やその派生の Pharo Smalltalk のインストールは本質的にzipファイルの展開だけで済ませられます。
- Squeak/Smalltalk | Downloads (WinやLinuxではzipを展開、macOSではdmgからコピー)
- Pharo - download (ホストOS用のランチャーをダウンロードして起動後、使用したいバージョンをCreateしてからLaunch)
さらにSqueakに限っては、仮想マシンをJavaScriptで記述したSqueakJSという実装があり、そのデモとして用意された下のリンクから、ワンクリックでWebブラウザ内からGUI込みの本格的なSmalltalkを(非常に古いバージョンかつ機能を絞ったものではありますが─)実に簡単に動かすことができます。
- SqueakJS (←このリンクをクリック! 基本的には大抵のWebブラウザで動きますがCtrl+Vでコード等のペーストができて動きも速いChromeを推奨します)
前述の通りこのMini Squeak 2.2は、非常に古く、かつ、機能を限定されたSmalltalkの処理系ですが、それでもSmalltalk発祥のMVCの基本を古い文献を手に学んだり、しくみを調べたり、それに基づいて簡単なプログラムを組んだりするのには十分です。
- 「使わないと損をする Model-View-Controller」(Smalltalkの古典的MVCの解説)のサンプルコードを SqueakJS のデモ画面(Mini Squeak 2.2)で動かす
- Mini Squeak 2.2 (Webブラウザで動作する、古く、最小構成に近いSmalltalk)の古典的 MVC でタイマーを実装する
70~80年代には、それで快適に仕事をするには1000万円級の高性能マシンが必須…などと言われていたSmalltalkが、かくも気軽に遊び感覚でサクサク動かすことができるようになるなんて! 本当に良い時代になりました。ぜひ、お試しあれかし。
この質問を投稿する時に「投稿」ボタンってありましたよね。
その隣に「質問を保存するSQL文を構築する」ってボタンと「構築されたSQL文を編集する」「構築されたSQL文を実行する」ってボタンが付いてたら嬉しいですか?
他人の作ったシステムを使う時に、利用側視点で無駄に手続きが増えるだけのインターフェイスは表示されてると邪魔なんです。
もう一つの理由が安全性とテストの単純化です。
「SQLを編集する」を可能にしていると、不正なSQLが入ってくる可能性が出てきます。どんなSQLが入ってくるかを実行時に確認する手間が格段に増えていますよね。
さて、この話しは、エンドユーザーとQuoraのシステムの間のことです。
しかし、複数人でのシステム開発では、プログラマ自身もまた他のプログラマにとってのユーザーになりえます。
更に巨大なシステムになってくると、何十年という長い保守の中で、累計何百人と開発に関わっていたりして、もはやプログラムには属人性は微塵もなく、全く信頼関係のない人同士がプログラミング言語を通じてのみお互いの疎通をしているという事もありえます。
カプセル化はそのような開発需要の中で生まれてきたはずなので、個人レベルや小規模で利用していても価値を感じられなくても可怪しくありません。
自分は個人でUnityでC#を書いていることが多く、カプセル化には直接あまりお世話になってませんが、Unityのライブラリ
この質問を投稿する時に「投稿」ボタンってありましたよね。
その隣に「質問を保存するSQL文を構築する」ってボタンと「構築されたSQL文を編集する」「構築されたSQL文を実行する」ってボタンが付いてたら嬉しいですか?
他人の作ったシステムを使う時に、利用側視点で無駄に手続きが増えるだけのインターフェイスは表示されてると邪魔なんです。
もう一つの理由が安全性とテストの単純化です。
「SQLを編集する」を可能にしていると、不正なSQLが入ってくる可能性が出てきます。どんなSQLが入ってくるかを実行時に確認する手間が格段に増えていますよね。
さて、この話しは、エンドユーザーとQuoraのシステムの間のことです。
しかし、複数人でのシステム開発では、プログラマ自身もまた他のプログラマにとってのユーザーになりえます。
更に巨大なシステムになってくると、何十年という長い保守の中で、累計何百人と開発に関わっていたりして、もはやプログラムには属人性は微塵もなく、全く信頼関係のない人同士がプログラミング言語を通じてのみお互いの疎通をしているという事もありえます。
カプセル化はそのような開発需要の中で生まれてきたはずなので、個人レベルや小規模で利用していても価値を感じられなくても可怪しくありません。
自分は個人でUnityでC#を書いていることが多く、カプセル化には直接あまりお世話になってませんが、Unityのライブラリや購入したアセットがpublicメソッドとフィールドだったりしたら、ドキュメントを読む時間が何倍にもなっているでしょうね。
永続化の観点が違うからです。
テーブルは構造と永続化の単位を定義しています。
対して、クラスは構造を定義していますが、永続化の単位ではありません。永続化の単位はインスタンスです。
クラスは個に特化しており、テーブルは群に特化しているため、クラスの考え方をテーブルに当てはめてしまうと、パフォーマンスに影響したり、そもそもリレーションが出来なくなります。
例として売上伝票を考えてみます。ここで言う売上伝票は、売上日などのヘッダー部と、商品などの明細部があると仮定します。
売上伝票の台紙はクラス、記入済みの伝票はインスタンスと言えます。
では、記入済み伝票を綴じたバインダーはテーブルかというと、近い存在ではありますが違います。バインダーは記入済み伝票リストのインスタンスです。テーブルではありません。
テーブルは売上伝票のヘッダーと明細を分けて永続化しています。ここがクラスとは決定的に違います。
仮に、ヘッダー部と明細部を切り離して、別々のバインダーに綴じたら、テーブルと言えるかもしれません。でも現実世界でそんな事はしませんよね。ヘッダーと明細は本来不可分です。明細はヘッダーとセットでなければ存在価値がありません。
データベースは群を扱うことに特化しています。ヘッダー部と明細部は独立しています。ヘッダーを読み込まなければ明細にアクセスできない、なんて非効率なことはしません。
ある商品の売上を持つ売上伝票を検索
永続化の観点が違うからです。
テーブルは構造と永続化の単位を定義しています。
対して、クラスは構造を定義していますが、永続化の単位ではありません。永続化の単位はインスタンスです。
クラスは個に特化しており、テーブルは群に特化しているため、クラスの考え方をテーブルに当てはめてしまうと、パフォーマンスに影響したり、そもそもリレーションが出来なくなります。
例として売上伝票を考えてみます。ここで言う売上伝票は、売上日などのヘッダー部と、商品などの明細部があると仮定します。
売上伝票の台紙はクラス、記入済みの伝票はインスタンスと言えます。
では、記入済み伝票を綴じたバインダーはテーブルかというと、近い存在ではありますが違います。バインダーは記入済み伝票リストのインスタンスです。テーブルではありません。
テーブルは売上伝票のヘッダーと明細を分けて永続化しています。ここがクラスとは決定的に違います。
仮に、ヘッダー部と明細部を切り離して、別々のバインダーに綴じたら、テーブルと言えるかもしれません。でも現実世界でそんな事はしませんよね。ヘッダーと明細は本来不可分です。明細はヘッダーとセットでなければ存在価値がありません。
データベースは群を扱うことに特化しています。ヘッダー部と明細部は独立しています。ヘッダーを読み込まなければ明細にアクセスできない、なんて非効率なことはしません。
ある商品の売上を持つ売上伝票を検索する方法を考えてみてください。クラスとテーブルではアプローチが全く異なることなります。
もう一つ、気にかけて欲しい事があります。ポリモーフィズムです。インターフェース、基底クラス、と読み替えても構いません。
売上伝票を例で言えば、明細部は商品、数量、金額さえ作れば、あとは取引先毎に好きに欄を変えていいよ、という状態です。
クラスの場合、明細を格納する型をインターフェースのリストとすることで、どの取引先の伝票にも対応出来ます。
一方、テーブルは被害が甚大です。表現できないわけではないですが、とにかく構造が複雑になります。ここは長くなるのでSQLアンチパターン、ポリモーフィズムで検索ください。
なお、データマートは上記のポリモーフィズムと似ていますが別物です。
他の回答にもあるように、両者は排他的なものではないので苦に合わせるやり方も色々あります。
ただ、関数型プログラミングの「副作用がない」ことを突き詰めたとしても、もし「コンピューター全体」を自分のプログラムで記述したのだとすれば、どこかではデータの書き換えを行うことにはなります。マウスポインターの位置を順次読み込んで画面に絵を描く、というようなプログラムを作っているとして、新しいマウスポインターの位置が取得されたからといって、その位置を表す新しいイミュータブルなメモリ素子を作ったり、画面の画素を毎回新しく作ったりはできませんからね。
となると、コンピューター上のどこかではデータの書き換えが起こっているわけなのですが、そのような書き換えの相互作用がなるべく起こらないようにモジュール化する技法として、オブジェクトというものがあって、書き換えがあるとしてもそのオブジェクトの責任として行うように記述するという手法はとても有用です。
最初のオブジェクト指向言語と言われている、50年以上前に作られ始めたSmalltalkというシステムでは、入力デバイスや画面といったものもオブジェクトとして表現されていました。そのシステムの設計者たちはLisp的な関数型言語にも通暁していたので、「よくデザインされたオブジェクト指向プログラミングであれば、なるべく多くのメソッドは、メソッドの引数とインスタンス変数の現在の
他の回答にもあるように、両者は排他的なものではないので苦に合わせるやり方も色々あります。
ただ、関数型プログラミングの「副作用がない」ことを突き詰めたとしても、もし「コンピューター全体」を自分のプログラムで記述したのだとすれば、どこかではデータの書き換えを行うことにはなります。マウスポインターの位置を順次読み込んで画面に絵を描く、というようなプログラムを作っているとして、新しいマウスポインターの位置が取得されたからといって、その位置を表す新しいイミュータブルなメモリ素子を作ったり、画面の画素を毎回新しく作ったりはできませんからね。
となると、コンピューター上のどこかではデータの書き換えが起こっているわけなのですが、そのような書き換えの相互作用がなるべく起こらないようにモジュール化する技法として、オブジェクトというものがあって、書き換えがあるとしてもそのオブジェクトの責任として行うように記述するという手法はとても有用です。
最初のオブジェクト指向言語と言われている、50年以上前に作られ始めたSmalltalkというシステムでは、入力デバイスや画面といったものもオブジェクトとして表現されていました。そのシステムの設計者たちはLisp的な関数型言語にも通暁していたので、「よくデザインされたオブジェクト指向プログラミングであれば、なるべく多くのメソッドは、メソッドの引数とインスタンス変数の現在の値を使って関数的に値を計算するだけとし、メッセージを受け取ってインスタンス変数を書き換えるときも、最後のところで計算された結果を一括してインスタンス変数に書くようにするのが良い」という知見は持っていました。そのようなある意味当たり前の"best practice"が伝わらず、一つの値だけを直接変更するような"setter"が後々よく書かれてしまい、外からそのように書き換えが行えるようなひとまとめにしたデータでしかないものが「オブジェクト」と呼ばれるようになったために混乱をきたした、という経緯があります。
関数型プログラミングのメリットのひとつとしては「高階関数」、つまり関数そのものを受け渡してプログラムを書けるようにするというものがあります。高階関数ももちろんSmalltalkのかなり初期バージョンに近いものから存在しており、mapやreduceやfilterに該当する機能も当たり前に準備されていました。ただ、こちらも引数として渡す「ブロック」や関数は、本来インスタンス変数の書き換えをするようなものを渡すべきではない、という知見もありました。それはオブジェクトが自分の内臓を切り取って「好きにしてくれ」と他のオブジェクトに渡しているようなものである、というような喩えもしばしば使われていました。ただ、このようなスタイルもあまり深く考えられることなく、値の書き換えをするようなブロックを他のオブジェクトに渡すようなパターンもしばしば使われていましたが、これもあまり望ましくないスタイルだと言えます。
ちょっと長くなってしまいましたが、結論としてはオブジェクト指向言語という肩書きがついた言語が作られていた初期から、その前から存在していた関数型プログラミングとの良いところどりをしてプログラムを書く、という実装は存在した、ということになります。そして、ちゃんとした原則に従ってプログラムを書けばとても強力なプログラムが簡潔に書ける、ということでもあります。
「オブジェクト指向」自体が様々な解釈をされ、その指すものが各自バラバラなのでなんとも言い難いところがありますが、自分なりの答えを。
ちなみに自分は言語的には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で「例外処理」するかどちらかです。
また、分割した詳細で例外処理が必要な場合も、呼び出し元で適切に処理するか、トップで処理するか、どちらにしても全体に波及します。
そして「分割を繰り返すことだけ」で設計をすると、細部で全体に影響が出る問題が出たときに取り返しがつかなくなります。これがウォーターフォール開発が炎上しがちな理由です。
となると、「分割を繰り返すことだけ」のトップダウン設計手法には問題があり、全体と細部を同時に見る、トップダウンとボトムアップの組み合わせでの設計が必要です。
ボトムアップの設計によって全体に影響する問題がどのようなものかを早めに明らかにし、トップダウンの設計でその全体に影響する問題を仕組みで解決します。
トップダウンの設計は、例えばコアロジックを非同期処理も例外設計も不要な形で分離する場合もあれば、イベントソーシングのように競合が起きづらいアーキテクチャを選定する手もあります。
そのトップダウンとボトムアップの組み合わせでの設計に適しているのがメソッドを動的に解決する、オブジェクト指向プログラミングです。小さな部品から作り始めることも、部品ができたものと仮定して全体を作って動かしてみることもできます。
今のところはそんな感じで考えています。
オブジェクト指向プログラミングにおいて一番難しいのが継承の使い方です。安易に使うと、コードを読みにくくし、保守性を悪くします。
本当は継承はない方がいいくらいなのですが、ほとんどの言語では継承がサポートされているため、継承に向き合う必要があります(特にPython)。
フレームワークが提供するクラスを継承して使う場合ではなく、他の人に継承して使ってもらう基底クラスを設計する場合、自分は次の2つを心がけてます。
- 目的を明確化し、やれることを制限する。
- 階層を深くしない(多段継承を避ける)。
まず、基底クラスを作るときは、目的を明確にし、使える場面と使えない場面を明確化します。「基底クラスは大体使えるけど一部はそのまま使えないから継承して差分を実装する(オーバーライド)」のは非常に危険です。
「大体使えるけど一部はそのまま使えないから継承して差分を実装する」のはほとんどの場合場当たりのつぎはぎです。本来は、基底クラスを改善して、つぎはぎではなく、きちっと差分を定義して、Templateパターンを使うのがいいでしょう。
しかし現実的には非常に難しいです。なので、そういうときは他の人に「基底クラスを継承せずに別のクラスを作れ」と言っています。
もちろんロジックが重複しますが、重複したコードの方が解決が容易です。また経験上「たまたま同じ実
オブジェクト指向プログラミングにおいて一番難しいのが継承の使い方です。安易に使うと、コードを読みにくくし、保守性を悪くします。
本当は継承はない方がいいくらいなのですが、ほとんどの言語では継承がサポートされているため、継承に向き合う必要があります(特にPython)。
フレームワークが提供するクラスを継承して使う場合ではなく、他の人に継承して使ってもらう基底クラスを設計する場合、自分は次の2つを心がけてます。
- 目的を明確化し、やれることを制限する。
- 階層を深くしない(多段継承を避ける)。
まず、基底クラスを作るときは、目的を明確にし、使える場面と使えない場面を明確化します。「基底クラスは大体使えるけど一部はそのまま使えないから継承して差分を実装する(オーバーライド)」のは非常に危険です。
「大体使えるけど一部はそのまま使えないから継承して差分を実装する」のはほとんどの場合場当たりのつぎはぎです。本来は、基底クラスを改善して、つぎはぎではなく、きちっと差分を定義して、Templateパターンを使うのがいいでしょう。
しかし現実的には非常に難しいです。なので、そういうときは他の人に「基底クラスを継承せずに別のクラスを作れ」と言っています。
もちろんロジックが重複しますが、重複したコードの方が解決が容易です。また経験上「たまたま同じ実装だった」だけで、後から変わるケースも多いです。
継承が破綻する原因の多くは、継承が深くなり、ロジックが分散してしまうからです。なので自分は次の3階層までに制限しています。
- ルートクラス、あるいはフレームワークが提供する、安定したクラス
- 自前で実装する基底クラス
- 基底クラスから派生するクラス
理想的な例はJavaのDate and Time APIです。これほとんど継承を使っておらず、多段継承は全くしていません。finalを付けているので、これらのクラスからの継承が不可能です(ただし例外クラスは例外的に多段継承が必要です)。
多重継承は忌むべきものとされていますが、多段継承に比べるとまだマシです。インタフェースの継承か、mixinでいいです。mixinの例はRubyのEnumerableで、全てeachを用いて実装されています。
ダイヤモンド継承問題というのもあるので、これも知っておいてください。
多重継承の問題はこれくらいです。注意は必要ですが、避けるのは難しくありません。それよりも、多段継承に注意してください。
まず実装の継承は嫌いです。一般にも批判が多く、使われなくなくなってきています。新しい言語では特に。
次に、クラスが嫌いです。役割りを負いすぎています。
インスタンスメソッドが嫌いです。継承を使わない前提ではvtableは無用です。thisへの暗黙の依存を伴う、リファクタリングや関数合成をしにくくする扱いにくい存在です。
すべてがオブジェクトという考え方は嫌いです。「メソッド後置の統一」という見た目の効果を求めているなら、それならなぜ中置オペレータがあるのか。中置オペレータにしたならしたで、それは算術演算として交換法則にしたがうべきなのにメソッド選択権限がレシーバのみにあるのが嫌い(マルチメソッド/ダブルディスパッチが簡潔に表現できてるなら良し)。またどうせ静的型のジェネリクスや型変数はオブジェクトではあつかいきれない。(コンパイルタイムメッセージパッシング、てあるかな)
可変のインスタンス変数は嫌いです。オブジェクトが状態もつということを、あまりにも軽率に可能にしてしまいます。
モックとか嫌いです。滅んでほしいです。DIはもっと嫌いです。面倒くさいからです。
単一責任原則は嫌いです。早すぎる最適化であり、未来の「ひとつの理由」に常に対応できるという傲慢さを感じるからです。「マイクロサービス細かく分けすぎる厨」に通じるものを感じます。
開放封鎖原則は、クラス継承を使わない場合に何を意味するのだろう
まず実装の継承は嫌いです。一般にも批判が多く、使われなくなくなってきています。新しい言語では特に。
次に、クラスが嫌いです。役割りを負いすぎています。
インスタンスメソッドが嫌いです。継承を使わない前提ではvtableは無用です。thisへの暗黙の依存を伴う、リファクタリングや関数合成をしにくくする扱いにくい存在です。
すべてがオブジェクトという考え方は嫌いです。「メソッド後置の統一」という見た目の効果を求めているなら、それならなぜ中置オペレータがあるのか。中置オペレータにしたならしたで、それは算術演算として交換法則にしたがうべきなのにメソッド選択権限がレシーバのみにあるのが嫌い(マルチメソッド/ダブルディスパッチが簡潔に表現できてるなら良し)。またどうせ静的型のジェネリクスや型変数はオブジェクトではあつかいきれない。(コンパイルタイムメッセージパッシング、てあるかな)
可変のインスタンス変数は嫌いです。オブジェクトが状態もつということを、あまりにも軽率に可能にしてしまいます。
モックとか嫌いです。滅んでほしいです。DIはもっと嫌いです。面倒くさいからです。
単一責任原則は嫌いです。早すぎる最適化であり、未来の「ひとつの理由」に常に対応できるという傲慢さを感じるからです。「マイクロサービス細かく分けすぎる厨」に通じるものを感じます。
開放封鎖原則は、クラス継承を使わない場合に何を意味するのだろうと思っちゃいます。
その他、オブジェクト指向設計原則のそれぞれは、オブジェクト指向を使わなければもともと難なく解決できる場合も多いと感じます。
好きなところは…上のような知見を10年ぐらいかけて生みだせたところでしょうか。
嫌いというか問題と思うのは、さじ加減を越えて使うと害のほうが大きくなるのに、やり過ぎかどうかをわかるための指針を思想の中に含んでいないところです。ほかの「なんとか志向」もだいたいそうだと思いますが、オブジェクト指向は特に広く普及したので、害が大きくなっていると思います。さじ加減がわからないプログラミングの経験が少ないエンジニアは、常に多いので。
最近は、オブジェクト指向を多用しはじめて30年ぐらいたって、ようやく、副作用がだいぶはっきりしてきたかなと思います。 もうすぐ、副作用を避けながらうまく使う手段が言語化でき、共有可能になるんじゃないかな、と思ってます。そうなれば、経験が少ない人も、誤用を減らせる、、といいなあ。
近いうちに、といってもあと10年20年かかるでしょうけど、この壮大な実験の結果が総括されるのだと思います。
好きなところは、さじ加減を間違わずに設計できたら、すごく綺麗に決まることですね。 Java、Ruby、C#、JavaScriptなどを使った開発で、いい感じにAPIを整理できたときはとても嬉しいです。
おっと、ご質問の条件にマッチする感じの私にフィードが来ましたよ。
私は業務自動化みたいなことを対象にプログラミングをしていまして、その時にオブジェクト指向で現実世界をプログラミングに投影するようなプログラミングをします。人、モノ、コトをオブジェクトで表現して、個人や集団や組織を人、ファイルやデータをモノ、タスクやワークフローをコト、みたいに扱います。
カプセル化こそオブジェクト指向の意味…と狂信的には思いませんが、カプセル化は文字通りのカプセルの意味があり、ポリモーフィズム・多態性と融合することで非常に強力に私をサポートしてくれます。
そんな私にとってカプセル化のメリットは「責任を局所化させることができる」「責任を果たせないなら切り捨てて交換することができる」ことです。
カプセル化は隠ぺいだったり変更への影響を抑えられるだったり再利用性が高まるだったり言われますが、私はそう思って取り組んでいったところ「言ってることはそうだなって思うんだけど、見方が逆じゃねぇか。」って思っちゃいまして、概ねそれでうまくいっています。個人製作においてはという属人的な範囲になってしまいますが。
冒頭に現実世界を投影するように私はプログラミングをすると書きました。仕事があって担当者を付けてやってもらうけれど、その担当者が何かの都合で満足する成果を出せなくなったら別の人を探してきて交代することがあります。仕事がコトで
おっと、ご質問の条件にマッチする感じの私にフィードが来ましたよ。
私は業務自動化みたいなことを対象にプログラミングをしていまして、その時にオブジェクト指向で現実世界をプログラミングに投影するようなプログラミングをします。人、モノ、コトをオブジェクトで表現して、個人や集団や組織を人、ファイルやデータをモノ、タスクやワークフローをコト、みたいに扱います。
カプセル化こそオブジェクト指向の意味…と狂信的には思いませんが、カプセル化は文字通りのカプセルの意味があり、ポリモーフィズム・多態性と融合することで非常に強力に私をサポートしてくれます。
そんな私にとってカプセル化のメリットは「責任を局所化させることができる」「責任を果たせないなら切り捨てて交換することができる」ことです。
カプセル化は隠ぺいだったり変更への影響を抑えられるだったり再利用性が高まるだったり言われますが、私はそう思って取り組んでいったところ「言ってることはそうだなって思うんだけど、見方が逆じゃねぇか。」って思っちゃいまして、概ねそれでうまくいっています。個人製作においてはという属人的な範囲になってしまいますが。
冒頭に現実世界を投影するように私はプログラミングをすると書きました。仕事があって担当者を付けてやってもらうけれど、その担当者が何かの都合で満足する成果を出せなくなったら別の人を探してきて交代することがあります。仕事がコトで、担当者が人。仕事で成果が出ないのが担当者に原因があるなら、担当者には休養のため退場してもらって別の可能な人に担当してもらうような感じです。それは担当者が交代可能な塊の責任をもって果たしてくれていたから、塊の責任を別の人に交代することできます。それを可能してくれるのが、カプセル化の概念で、「責任を局所化させることができ」「責任が果たされなければ切り捨て交換できる」というメリットです。
カプセル化を使うことで、私はプログラムの中に組織で仕事をする会社のようなものを作っています。別のパラダイムも活用しますが、私はどうもデータ処理や再現性の確保にしか活用を見出せないようで、オブジェクト指向プログラミングを幹にしたプログラミングを続けています。
多くの場合は、オブジェクト指向を使い、なるだけ不変(immutable)にするとバランスが良いです。理由は2つあります。
- オブジェクト指向で書いた方が読みやすい
- 可変(mutable)が多いほど、バグが生じやすくなる
まず1.ですが、オブジェクト指向のメソッド呼び出しは human.say("hello") のように、語順が自然言語に近いため読みやすいです。
一方で、関数型の「副作用をなくす」という考え方は重要です。例えばJavaのCalendarクラスはsetterがあり、トラブルの元になっていました。
なのでEffective Javaでは、このようなsetterを避け、不変にすべきと書かれています。
不変オブジェクトを多く使うことでメンテナンスしやすいコードになります。小さいオブジェクトをたくさん用意するのでメモリは多く使いますが、そこを気にするのは「早すぎる最適化」ですね。
逆に全体として関数型で書いた方が良いケースもあります。パイプラインアーキテクチャを採用する場合です。
この場合各フィルターが独立しているので、関数型で書いた方がいいです。
自分の経験ではちょっと前に、GitHubの
多くの場合は、オブジェクト指向を使い、なるだけ不変(immutable)にするとバランスが良いです。理由は2つあります。
- オブジェクト指向で書いた方が読みやすい
- 可変(mutable)が多いほど、バグが生じやすくなる
まず1.ですが、オブジェクト指向のメソッド呼び出しは human.say("hello") のように、語順が自然言語に近いため読みやすいです。
一方で、関数型の「副作用をなくす」という考え方は重要です。例えばJavaのCalendarクラスはsetterがあり、トラブルの元になっていました。
なのでEffective Javaでは、このようなsetterを避け、不変にすべきと書かれています。
不変オブジェクトを多く使うことでメンテナンスしやすいコードになります。小さいオブジェクトをたくさん用意するのでメモリは多く使いますが、そこを気にするのは「早すぎる最適化」ですね。
逆に全体として関数型で書いた方が良いケースもあります。パイプラインアーキテクチャを採用する場合です。
この場合各フィルターが独立しているので、関数型で書いた方がいいです。
自分の経験ではちょっと前に、GitHubのGraphQL APIから必要なデータを取得して、データを加工し、Googleスプレッドシートに書いたり、Slackに投げるプログラムを書いたことがあります。
最初はオブジェクト指向で書いていたのですが、処理が分散してイマイチでした。そこで関数型を意識したコードで書き直したらだいぶスッキリしました。
なので結論としてはこうです。
- ビジネス寄りの場合(自然言語に近いもの)はオブジェクト指向。ただしできるだけ不変にする。
- 純粋なデータ処理など、技術寄りのプログラムは関数型で書く。
関数型言語と対比させるべきは、オブジェクト指向言語というよりも手続型言語ですね。関数型とオブジェクト指向は両立できます。
関数型言語か手続型言語かという区別によって選択されているというよりも、自分が慣れ親しんだ言語や競技プログラミングで動作や速度を保証・推奨されている言語を選んだ結果、たまたま C++ や Python や Java といった手続型オブジェクト指向言語になったというのが、直接の理由になると思います。
LINQ という関数型プログラミングの DSL をも備える手続型オブジェクト指向言語である C# でたまに AtCoder をやっている身から関数型プログラミングと競プロとの関係に言及すると、関数型プログラミングが競プロに活きる場面は 50 : 50 かなと思います。
関数型プログラミングは、大きなデータセットの一括変換 (map) や絞り込み (filter) を宣言的に記述できるという点では有利です。手続型が for 文を書いて、条件分岐を書いて、インデックスを指定して配列を操作して、カウンタをインクリメントして … などと逐次的にステートメント記述する場面で、関数型は集合演算メソッドとちょっとしたラムダ式を書くだけで、必要とするデータセットを簡単に得られるということはよくあります。
.Where() → .Select() / .Zip() → .GroupBy() /
関数型言語と対比させるべきは、オブジェクト指向言語というよりも手続型言語ですね。関数型とオブジェクト指向は両立できます。
関数型言語か手続型言語かという区別によって選択されているというよりも、自分が慣れ親しんだ言語や競技プログラミングで動作や速度を保証・推奨されている言語を選んだ結果、たまたま C++ や Python や Java といった手続型オブジェクト指向言語になったというのが、直接の理由になると思います。
LINQ という関数型プログラミングの DSL をも備える手続型オブジェクト指向言語である C# でたまに AtCoder をやっている身から関数型プログラミングと競プロとの関係に言及すると、関数型プログラミングが競プロに活きる場面は 50 : 50 かなと思います。
関数型プログラミングは、大きなデータセットの一括変換 (map) や絞り込み (filter) を宣言的に記述できるという点では有利です。手続型が for 文を書いて、条件分岐を書いて、インデックスを指定して配列を操作して、カウンタをインクリメントして … などと逐次的にステートメント記述する場面で、関数型は集合演算メソッドとちょっとしたラムダ式を書くだけで、必要とするデータセットを簡単に得られるということはよくあります。
.Where() → .Select() / .Zip() → .GroupBy() / .Chunk() → .SelectMany() → .Distinct() → .OrderBy() → .ToArray() / .ToDictionary() と変換するなどは日常茶飯事であり、この流れで処理の下地を作るのは関数型が圧倒的に速いです。
一方で、最大値を採るなどのデータセットの集約 (reduce) をしつつ、それと各要素とを絡めて処理するなど、集約データや変遷していくデータを活用するのは不利です。手続型であればイテレーション・ブロック外変数に .ChangeMax() を掛けるなど、クロージャ的に副作用を使って簡潔に書けます。また、イテレーションの途中でフロー制御や断面操作を大きく変えるときや多重ループが必要なときなども関数型より手続型の方が優れています。
というわけで私は、副作用を使う必要がなく参照透過的に記述できる、と即判断できるところは LINQ で、そうではないところは手続型のイテレーションで記述するハイブリッド戦略を採っています。関数型・手続型の両方を利用出来て、すぐに使い分けられることが競プロには重要だと考えます。
逡巡したり、判断を誤ってコード書き換えに時間を費やしてしまうことも … (To T
競技が終わった後には、手続型で記述したところを関数型に変更できないかリファクタリングを試みます。関数型へのリファクタリングにより、非常にシンプルになってわかり易くなることもあれば、逆に難しくわかりにくくなって手続型に戻すこともあります。
事後のリファクタリングにおいてすら、長く試行錯誤した上で関数型を採用すべきではないという結論になることもあるため、時間の限られる競技において始めからすべて関数型で記述するのは困難なのではないかと思います。
自分もオブジェクト指向に対する盲目的な信頼を置いていましたが、色々学んでいるうちにもしかしたらそうかもという気持ちになってきています。
一つはReact(flux)の存在です。状態の管理を一局集中させて、複雑さを隠蔽しようという考え方です。
一つはGoの台頭です。オブジェクト指向は可能ですがJavaのような複雑な継承を行えないようにしています。実際標準ライブラリが継承を使ってる形跡がなく、読みやすくなっています。
書いているうちに思い出してきました。継承されているとドキュメントが読み辛いんですよ。実装箇所を求めて、定義ジャンプを繰り返すことになりIDE支援がないと読みにくいことこの上ないです。
でもUnityを触ってるとあれはオブジェクト指向に向いているフレームワークだと感じます。あれは仮想空間における実際のオブジェクトを扱っているわけですしw