7/23/2019

オブジェクト指向プログラミング -- 1兆ドル規模の大失敗

CodeIQのブログより。🤔

なぜ、OOPから移行する時なのか

Ilya Suzdalnitski

OOPは、多くの人にコンピューターサイエンスの重要資産と考えられています。コード構成(code organization)に対する究極のソリューション。すべての問題の終焉。私たちのプログラムを書くための唯一の本当の方法。自分自身をプログラムするという真なる唯一神から私たちに授けられました…

それまでは、そうではなく、抽象化の負担、そして無差別に共有されるミュータブルなオブジェクトの複雑なグラフによって、人々は屈し始めています。現実世界の問題を解決するのではなく、「抽象化」と「デザインパターン」について考えるのに貴重な時間と頭脳が費やされています。

非常に著名なソフトウェアエンジニアを含め、多くの人々がオブジェクト指向プログラミングを批判してきました。驚くことに、OOP自身の発明者でさえ、今やOOPの有名な批判者です!

すべてのソフトウェア開発者の最終的な目標は、信頼できるコードを書くことです。コードにバグがあり信頼性が低いなら、何も問題はありません。そして、信頼できるコードを書くための最善の方法とはどんなものですか? 単純さです。単純さとは、複雑さの反対です。従って、ソフトウェア開発者としての私たちの最優先の責任は、コードの複雑さを減らすことです。

免責事項

正直なところ、私はオブジェクト指向の熱狂的ファンではありません。もちろん、この記事には偏りがあります。しかし、私にはOOPを嫌う理由があります。

私は、OOPに対する批判は非常に敏感な話題であると理解しています - おそらく多くの読者を怒らせるでしょう。しかし、私は正しいと思うことをやっています。 私の目標は、怒らせることではなく、OOPがもたらす問題についての意識を高めることです。私はアラン・ケイのOOPを批判しているのではありません - 彼は天才です。OOPが彼の設計通りに実装されていることを願っています。私は、OOPに対する最新のJava/C#のアプローチを批判しているのです。

私が怒っていることも認めます。非常に怒っています。私は、OOPが非常に上級の技術的立場にある人たちを含む多くの人々によってコード構成の事実上の標準と見なされているのは明らかに間違っていると思います。多くの主流の言語がOOP以外のコード構成に代わる他の方法を提供していないのもまた間違っています。

くそ、私がOOPプロジェクトに取り組んでいる間、私自身、多くの事に格闘していました。そして、なぜ私がこれほど苦労していたのか、私にはまったく分かりません。もしかして、私が優秀ではなかったのか? いや、私はもう少しデザインパターンを学ぶ必要があったのです(私は思った)! 結局、私は完全に燃え尽きました。

この記事では、オブジェクト指向から関数型プログラミングへの10年に渡る私自分の経験に基づく旅を要約します。全部見ました。残念ながら、私がどれほど努力しても、OOPのユースケースは見つかりません。個人的にも、OOPプロジェクトが維持するには余りにも複雑になったため、失敗するのを見た事があります。

TLDR

オブジェクト指向プログラムは正しいものに代わりとして提供されています...
-- コンピュータサイエンスのパイオニア、エドガー・ダイクストラ

オブジェクト指向プログラミングは、手続き型コードベースの複雑さを管理することを念頭に置いて作成されました。言い換えれば、それはコード構成を改善することになっていました。OOPが単なる手続き型プログラミングより優れているという客観的でオープンな証拠はありません。

残念ながら、OOPは対処しようとしていた唯一のタスクで失敗します。それは理論上は良さそうです - 私たちは動物、犬、人間などのきれいな階層構造を持っています。しかし、アプリケーションの複雑さが増し始めると、それは全くうまくいかなくなります。複雑さを軽減する代わりに、ミュータブル状態を無差別に共有することを奨励するため、その多数の設計パターンでさらなる複雑さをもたらします。OOPは、リファクタリングやテストなどの一般的な開発方法を不必要に困難にします。

私に賛成できない人もいるかも知れませんが、実際のところ、今のOOPは正しく設計されていません(Haskell/FPとは対照的に)。それは適切な研究機関から出たことはありません。私はゼロックスや他の企業を「適切な研究機関」とは見なしません。OOPには、それを裏付けるための何十年もの厳密な科学的研究がありません。一方、ラムダ計算には、関数型プログラミングのための完全な理論的基礎を提供します。OOPはそれに匹敵するものは何もありません。OOPは主として「起こった事(just happened)」です。

OOPを使用することは、特に更地の(greenfield)プロジェクトでは、短期的には無害のようです。しかし、OOPを使用することによる長期的な影響は何ですか? OOPは時限爆弾であり、将来的にコードベースが十分に大きくなったときに爆発するように設定されています。

プロジェクトが遅れ、締め切りが間に合わず、開発者が燃え尽きて、新しい機能を追加することが不可能に近づくようになります。組織はコードベースを「レガシーコードベース」として分類し、開発チームは書き直しを計画します。

OOPは人間の脳にとって自然なものではありません。私たちの思考プロセスは物事を「実行する(doing)」ことを中心としています - 散歩に出かけたり、友達と話したり、ピザを食べたりします。 私たちの脳は、世界を抽象オブジェクトの複雑な階層に組織化するのではなく、物事を行うために進化しました。

OOPコードは非決定的です - 関数型プログラミングとは異なり、同じ入力に対して同じ出力が得られることは保証されていません。これはプログラムについての推論を非常に困難にします。単純化しすぎた例として、2+2またはcalculator.Add(2,2)の出力はほぼ4ですが、3、5、さらには1004になる可能性もあります。微妙な、しかし深遠な方法での計算の結果、Calculatorオブジェクトの依存関係が変わる可能性があります。

弾力性のあるフレームワークの必要性

これは奇妙に思えるかも知れませんが、プログラマとしては、信頼できるコードを書くことに自分自身を信頼するべきではありません。個人的には、私は自分の仕事の基礎となる強力なフレームワークなしでは良いコードを書くことができません。はい、いくつかの非常に特別な問題(例えばAngularやASP.Net)に関係するフレームワークがあります。

ソフトウェアフレームワークについては話しているのではありません。私は、フレームワークのより抽象的な辞書定義について話しています。「不可欠なサポート構造」 - コード構成やコードの複雑さへの取り組みなど、より抽象的なものに関心を持つフレームワークです。オブジェクト指向プログラミングと関数型プログラミングはどちらもプログラミングのパラダイムですが、どちらも非常に高度なフレームワークです。

選択を制限する

C++は恐ろしい[オブジェクト指向]言語だ... そして、プロジェクトをCに限定すれば、理想主義的なクソの「オブジェクトモデル」でモノをぶち壊したりしないという意味だ。
-- Linuxの開発者、リーナス・トーバルズ

リーナス・トーバルズは、C++とOOPに対する率直な批判で広く知られています。彼が100%正しいことは、プログラマが選択できる選択肢を制限することです。実際、プログラマが選択する選択肢が少なければ少ないほど、コードはより弾力的になります。上記の引用で、リーナス・トーバルズは、コードの基礎となる良いフレームワークを持つことを強く推奨しています。

多くの人は道路の制限速度を嫌いますが、人々が急死して死亡するのを防ぐためには不可欠です。同様に、良いプログラミングフレームワークは私たちがばかげたことをするのを妨げるメカニズムを提供すべきです。

良いプログラミングフレームワークは、信頼できるコードを書くのに役立ちます。まず第一に、それは以下のことを提供することによって複雑さを減らすのを助けるべきです:

  1. モジュール性と再利用性
  2. 適切な状態分離
  3. 高いSN比

残念なことに、OOPは、正しい種類の制限を課すことなく、開発者にあまりにも多くのツールと選択肢を提供します。OOPはモジュール性を扱い再利用性を改善することを約束していますが、その約束を守ることはできません(詳細は後で説明します)。OOPコードは共有されたミュータブル状態の使用を奨励しています。これは安全ではないことが証明されています。 OOPは一般的に多くの定型的コード(低いSN比)を必要とします。

関数型プログラミング

関数型プログラミングとは正確には何ですか? 一部の人は、学界でしか適用できず、「現実の世界」には適さない、非常に複雑なプログラミングパラダイムと考える人もいます。これは事実とはまるでかけ離れています!

はい、関数型プログラミングは強力な数学的基礎を持ち、その基礎をラムダ計算に取り入れています。 しかし、そのアイデアのほとんどは、より主流のプログラミング言語の弱点への対応として浮上してきました。関数は関数型プログラミングの中心的な抽象概念です。適切に使用されると、関数はOOPには見られないレベルのコードのモジュール性と再利用性を提供します。nullabilityの問題に対処する設計パターンを特徴とし、エラー処理の優れた方法を提供します。

関数型プログラミングが本当にうまくいく1つのことは、信頼できるソフトウェアを書くのに役立つということです。デバッガの必要性はほぼ完全に消えます。うん、あなたのコードをステップスルーして変数を見る必要はありません。私は個人的には、デバッガには長い間触れていません。

一番良いところ? 関数の使い方をすでに知っていれば、あなたはすでに機能的なプログラマーです。これらの機能を最大限に活用する方法を学ぶ必要があります!

OOPがすべて間違っています

私はずっと以前にこの話題に向け「オブジェクト」という用語を作り出したことを残念に思います。大きなアイデアはメッセージングです。
-- OOPの発明者、アラン・ケイ

Erlangは通常、オブジェクト指向言語とは考えられていません。しかし、おそらくErlangが唯一の主流のオブジェクト指向言語です。はい、もちろんSmalltalkは適切なOOP言語です - しかし、広く使われているわけではありません。SmalltalkとErlangはどちらも、その発明者であるアラン・ケイが当初意図した方法でOOPを利用します。

メッセージング

アラン・ケイは1960年代に「オブジェクト指向プログラミング」という用語を作りました。彼は生物学のバックグラウンドを持っていて、生きている細胞がするのと同じ方法でコンピュータプログラムを通信させることを試みていました。

アラン・ケイの大きなアイデアは、独立したプログラム(セル)が互いにメッセージを送信して通信することでした。独立したプログラムの状態は外の世界とは決して共有されません(カプセル化)。

それでおしまい。OOPは、継承、ポリモーフィズム、newキーワード、そして無数のデザインパターンといったものを持つことを意図したものではありませんでした。

純粋な形式のOOP

Erlangは最も純粋な形でOOPです。より主流の言語とは異なり、OOPの中心となる概念、つまりメッセージングに焦点を当てています。Erlangでは、オブジェクトはイミュータブルなメッセージをオブジェクト間で受け渡すことによって通信します。

イミュータブルメッセージがメソッド呼び出しに比べて優れたアプローチであるという証拠はありますか?

もちろんです! Erlangはおそらく世界で最も信頼できる言語です。これは、世界のほとんどのテレコム(そしてインターネット)基盤にパワーを供給します。Erlangで書かれたシステムの中には99.9999999%の信頼性を持っているものがあります(あなたはそのとおりに読みます - ナインナイン)。

コードの複雑さ

OOPに活用されたプログラミング言語では、コンピュータソフトウェアはより冗長になり、読みにくくなり、説明が少なくなり、変更や保守が難しくなります。
-- リチャード・マンスフィールド

ソフトウェア開発の最も重要な側面は、コードの複雑さを抑えることです。以上。コードベースを維持することが不可能になっても、手の込んだ機能はどれも重要ではありません。コードベースが複雑になり保守できなくなると、100%のテスト範囲でさえも価値がありません。

コードベースが複雑になるとはどういうことですか? 考慮すべきことはたくさんありますが、私の意見では、最大の違反者は次のとおりです。共有されたミュータブル状態、誤った抽象化、および低いSN比(多くの場合、定型コードによって引き起こされる)。それらのすべてがOOPで蔓延しています。

状態の問題

状態とは? 簡単に言えば、状態はメモリに格納された一時的データです。OOPで変数やフィールド/プロパティを考えて下さい。命令型プログラミング(OOPを含む)は、プログラム状態とその状態への変更という観点から計算を記述します。宣言型(関数型)プログラミングでは代わりに目的の結果を記述し、状態への変更を明示的に指定しないで下さい。

ミュータブル状態 - メンタル・ジャグリングの行為

ミュータブルオブジェクトの大きなオブジェクトグラフを作成すると、大規模なオブジェクト指向プログラムは複雑さを増すことに苦労すると思います。ご存知でしょうが、あなたがメソッドを呼び出すときに何が起こるのか、そして副作用が何になるのかを理解し、頭の中で心に留めようとしています。
—- Clojureの作成者であるリッチ・ヒッキー

状態自体はまったく無害です。しかし、ミュータブル状態は大きな犯罪者です。それが共有されている場合は特にそうです。ミュータブル状態とは何ですか? 変化する可能性のある状態です。OOPで変数やフィールドを考えて下さい。

現実世界の例をどうぞ!

白紙の紙があり、その上にメモを書きます。そして、同じ紙切れになってしまいます(テキスト)。あなたは、事実上、その紙切れの状態を変えました。

他の誰もその紙片を気にかけていないので、それは現実の世界では全く問題ありません。この紙がモナリザ絵画のオリジナルの絵でない限り。

人間の脳の限界

ミュータブル状態はなぜそれほど大きな問題なのでしょうか? 人間の脳は既知の宇宙で最も強力なマシンです。しかし、私たちの頭脳は、私たちのワーキングメモリーに一度に約5つのアイテムしか保持できないため、状態を扱うのが実際に苦手です。コードベースの前後でどのような変数が変わるのかではなく、あなたがコードが何をするのかについて考えるだけであれば、コードの一部について推論する方がはるかに簡単です。

ミュータブル状態を使ったプログラミングは、メンタル・ジャグリングの行為です。私はあなたのことを知りませんが、おそらく2つのボールを組み合わせることができます。私に3つ以上のボールを下さい、そして私はそれらのすべてを確実に落とします。それでは、なぜ私たちは仕事で毎日このメンタル・ジャグリングの行為を実行しようとするのですか?

残念ながら、ミュータブル状態のメンタル・ジャグリングは、OOPの中核を成すものです。オブジェクトにメソッドが存在する唯一の目的は、その同じオブジェクトを変更することです。

散乱状態

OOPは、プログラム全体に状態を分散させることによって、コード構成の問題をさらに悪化させます。散乱状態は、様々なオブジェクト間で無差別に共有されます。

現実世界の例をどうぞ!

私たち全員が大人になったことをちょっと忘れて、クールなレゴのトラックを組み立てようとしているふりをしましょう。

しかし、罠があります - すべてのトラックの部品はあなたの他のレゴのおもちゃからの部品とランダムに混在しています。そして、それらは50個の異なる箱に再びランダムに入れられました。トラックの部品をまとめることはできません。様々なトラックの部品が置かれている場所に頭の中で保管する必要があり、それらを1つずつ取り出すことしかできません。

はい、あなたは最終的にそのトラックを組み立てるつもりですが、それはどのくらい掛かりますか?

これはプログラミングとどのように関係していますか?

関数型プログラミングでは、状態は通常分離されています。 あなたはいつも何らかの状態がどこから来ているのか分かっています。状態はあなたの様々な機能に分散されることはありません。OOPでは、すべてのオブジェクトには独自の状態があり、プログラムを作成するときには、現在作業しているすべてのオブジェクトの状態に注意する必要があります。

私たちがもっと楽になるためには、コードベースのごくわずかな部分だけが状態を扱うようにするのが最善です。アプリケーションの中核部分をステートレスで純粋なものにします。これが実際にフロントエンド(別名Redux)でのFluxパターンの大成功の主な理由です。

乱雑な共有状態

ミュータブル状態が分散しているために、私たちの生活がまだ十分に困難ではない場合と同様に、OOPはさらに一歩進みます!

現実世界の例をどうぞ!

物事は非公開に保たれて共有されることはないので、現実の世界におけるミュータブル状態はほとんど問題になりません。これは「適切なカプセル化」です。 次のモナリザの絵に取り組んでいる画家を想像してみて下さい。彼は一人で絵に取り組んでいて、描き上げ、そして彼の傑作を数百万ドルで売却します。

今、彼はそのすべてのお金に飽きて、物事を少し違ったやり方でやろうと決心しました。彼は絵画パーティーをするのは良い考えだと思いました。彼は友人のエルフ、ガンダルフ、警官、そしてゾンビを助けてくれるように誘います。チームワーク! 彼ら全員が同時に同じキャンバスに絵を描き始めます。もちろん、そこから良いことは何も出て来ません - この絵は完全に大失敗!

共有されたミュータブル状態は現実の世界では意味がありません。それでも、これはまさにOOPプログラムで起こることです - 状態は無差別に様々なオブジェクトの間で共有されています。そして、彼らはそれらが適当と思う方法でそれを変化させます。その結果、コードベースが拡大し続けるにつれて、プログラムについての推論がますます難しくなります。

並行性の問題

OOPコードでミュータブル状態を無差別に共有すると、そのようなコードの並列化はほとんど不可能になります。この問題に対処するために複雑なメカニズムが発明されました。スレッドロック、ミューテックス(Mutex)、および他の多くのメカニズムが発明されました。もちろん、そのような複雑なアプローチには、デッドロック、合成性の欠如、マルチスレッドコードのデバッグが非常に困難で時間がかかるという、それぞれ独自の欠点があります。このような並行処理メカニズムを利用することによって引き起こされる複雑さの増大については話すことすらしません。

すべての状態が悪ではない

すべての状態が悪ですか? いいえ、アラン・ケイの状態は悪ではありません! 状態のミュテーションは、それが真に分離されていればおそらくおそらく問題ありません(OOPのやり方の分離ではありません)。

イミュータブルなdata-transfer-objectsを持つことも全く問題ありません。ここでの鍵は「イミュータブル」です。そのようなオブジェクトは、関数間でデータを渡すために使用されます。

しかし、そのようなオブジェクトはOOPメソッドとプロパティを完全に冗長にします。変更できない場合、オブジェクトにメソッドとプロパティを設定することで何が使用されますか?

カプセル化のトロイの木馬

カプセル化はOOPの最大の利点の1つであると言われています。オブジェクトの内部状態を外部からのアクセスから保護することになっています。これには小さな問題があります。うまくいきません。

カプセル化は、OOPのトロイの木馬です。それは安全に見えるようにすることによって共有されたミュータブル状態という考えを良いと思い込ませます。カプセル化により(推奨されています)、危険なコードがコードベースに潜入し、コードベースが内部から腐敗する可能性があります。

グローバル状態の問題

グローバル状態がすべての悪の根源だと言われました。それは絶対に避けなければなりません。私たちがこれまで一度も言われたことがないのは、カプセル化は実際には見せ掛けのグローバル状態であるということです。

コードをより効率的にするために、オブジェクトは値ではなく参照によって渡されます。これが「依存性注入(dependency injection)」が完全に失敗するところです。

説明させて下さい。OOPでオブジェクトを作成するたびに、その依存関係への参照をコンストラクタに渡します。それらの依存関係にも独自の内部状態があります。新しく作成されたオブジェクトはそれらの依存関係への参照をその内部状態で喜んで格納しています。そして、それはまたそれらの参照をそれが使用することになるかもしれない他のに渡します。

これにより、無差別に共有されたオブジェクトの複雑なグラフが作成され、それらすべてが互いの状態を変更してしまいます。これは、プログラムの状態が変化した原因を確認することがほとんど不可能になるため、大きな問題を引き起こします。そのような状態変化をデバッグしようとして、無駄な日数が掛かるかも知れません。並行性に対処する必要がない場合は、ラッキーです(これについては後で詳しく説明します)。

メソッド/プロパティ

特定のフィールドへのアクセスを提供するメソッドやプロパティは、フィールドの値を直接変更する以上のものです。派手なプロパティやメソッドを使用してオブジェクトの状態を変更するかどうかは問題ではありません - 結果は同じです: ミュータブル状態。

実世界モデリングの問題

OOPが現実の世界をモデル化しようとしていると言う人もいます。これは単純に真実ではありません - OOPは実社会とは関係ありません。プログラムをオブジェクトとしてモデル化しようとするのは、おそらくOOPの最大の間違いの1つです。

現実の世界は階層的ではありません

OOPは、すべてをオブジェクトの階層としてモデル化しようとします。残念ながら、それは現実の世界で物事がどのように機能するかではありません。現実世界のオブジェクトはメッセージを使用して互いに対話しますが、それらはほとんど互いに独立しています。

実社会での継承

OOPの継承は現実の世界をモデルにしていません。実世界の親オブジェクトは、実行時に子オブジェクトの動作を変更することはできません。あなたがあなたのDNAをあなたの両親から受け継いでも、彼らはあなたのDNAを彼らが望むように変更することができません。あなたは両親から「行動」を受け継がず、自分の行動を発達させます。そして、あなたは両親の行動を「無効にする」ことができません。

現実の世界には方法がありません

あなたが書いている紙切れには「書き込み」方法がありますか? いいえ! あなたは空の紙切れを取り、ペンを持ち上げ、そしてテキストを書きます。あなたは、個人として、「書く」方法も持っていません - あなたは、外部の出来事やあなたの内的な考えに基づいてテキストを書くという決断をします。

名詞の王国(The Kingdom of Nouns)

オブジェクトは、分割できない単位で関数とデータ構造を結び付けます。関数とデータ構造はまったく異なる世界に属しているので、これは根本的な誤りだと思います。
-- ジョー・アームストロング、Erlangの作成者

オブジェクト(または名詞)は、OOPの中核を成すものです。OOPの根本的な制限は、それがすべてを名詞にすることです。そして、すべてが名詞としてモデル化されるべきではありません。操作(機能)をオブジェクトとしてモデル化しないで下さい。2つの数を乗算する関数だけが必要なときに、なぜMultiplierクラスを作成しなければならないのでしょうか? 単に乗算機能を持ち、データをデータにし、機能を機能にしましょう。

OOP以外の言語では、データをファイルに保存するなどの簡単なことを行うのは簡単です - 普通の英語でアクションを記述する方法と非常によく似ています。

現実世界の例をどうぞ!

画家の例に戻ると、画家はPaintingFactoryを所有しています。彼は、専用のBrushManager、ColorManager、CanvasManager、およびMonaLisaProviderを採用しました。彼の親友のゾンビはBrainConsumingStrategyを利用します。これらのオブジェクトは、CreatePainting、FindBrush、PickColor、CallMonaLisa、およびConsumeBrainzの各メソッドを定義します。

もちろん、これは明らに愚かであり、現実の世界では起こり得なかったことです。絵を描くという単純な行為のために、どれほど不必要な複雑さが生まれたでしょうか?

オブジェクトとは別に存在することが許可されている場合は、機能を保持するために奇妙な概念を考案する必要はありません。

単体テスト

自動テストは開発プロセスの重要な部分であり、後退を防ぐのに非常に役立ちます(つまり、既存のコードにバグが入り込む)。単体テストは自動テストのプロセスにおいて大きな役割を果たします。

そうでない人もいるかもしれませんが、OOPコードを単体テストするのは難しいことで有名です。単体テストでは、物事を個別にテストし、メソッドを単体テスト可能にすることを前提としています。

  1. その依存関係は別のクラスに抽出する必要があります。
  2. 新しく作成したクラスのインターフェースを作成します。
  3. 新しく作成したクラスのインスタンスを保持するためにフィールドを宣言します。
  4. 依存関係をモックするため、モックフレームワークを利用します。
  5. 依存性を注入するため、依存性注入フレームワークを利用します。

コードの一部をテスト可能にするためだけに、さらに複雑な部分を作成する必要がありますか? コードをテスト可能にするためにどれだけの時間が浪費されましたか?

>PS また、私たちはシングルメソッドをテストするためにクラス全体をインスタンス化する必要があります。これにより、その親クラスすべてからのコードも取り込まれます。

OOPでは、レガシーコードのテストを書くことはさらに困難です - ほとんど不可能です。従来のOOPコードをテストする問題を中心に、全体が作成されました(TypeMock)。

定型コード

信号対雑音比に関しては、定型コードがおそらく最大の違反者となります。定型コードは、プログラムをコンパイルするために必要な「ノイズ」です。定型コードは記述に時間が掛かり、ノイズが増えるためにコードベースが読みにくくなります。

OOPでは「implementationではなくinterfaceにプログラムする」ことをお勧めしますが、全てがinterfaceになるべきではありません。テストの容易さのためだけに、コードベース全体でinterfaceを使用することに頼らなければなりません。私たちはおそらく依存性注入を利用しなければならないでしょう。そして、それはさらに不必要な複雑さをもたらします。

プライベートメソッドのテスト

プライベートメソッドはテストするべきではないと言う人もいます… 私はどちらかといえば賛成できません。単体テストは理由から「ユニット」と呼ばれます - 小さなコード単位を単独でテストします。それでも、OOPでプライベートメソッドをテストすることはほぼ不可能です。テストしやすさのためだけにプライベートメソッドを内部に作成するべきではありません。

プライベートメソッドのテスト容易性を達成するためには、それらは通常別々のオブジェクトに抽出されなければなりません。これにより、不要な複雑さと定型コードが導入されます。

リファクタリング

リファクタリングは開発者の日常業務の重要な部分です。皮肉なことに、OOPコードはリファクタリングが難しいことで有名です。リファクタリングは、コードをそれほど複雑でなく、より保守しやすくするためのものです。反対に、リファクタリングされたOOPコードはかなり複雑になります。コードをテスト可能にするには、依存性注入を利用し、リファクタリングされたクラス用のインタフェースを作成する必要があります。 それでも、OOPコードをリファクタリングすることは、Resharperのような専用のツールがなければ、現実には困難です。

// before refactoring:
public class CalculatorForm {
    private string aText, bText;
        private bool IsValidInput(string text) => true;
        private void btnAddClick(object sender, EventArgs e) {
        if ( !IsValidInput(bText) || !IsValidInput(aText) ) {
            return;
        }
    }
}

// after refactoring:
public class CalculatorForm {
    private string aText, bText;
        private readonly IInputValidator _inputValidator;
        public CalculatorForm(IInputValidator inputValidator) {
        _inputValidator = inputValidator;
    }
        private void btnAddClick(object sender, EventArgs e) {
        if ( !_inputValidator.IsValidInput(bText)
            || !_inputValidator.IsValidInput(aText) ) {
            return;
        }
    }
}

public interface IInputValidator {
    bool IsValidInput(string text);
}

public class InputValidator : IInputValidator {
    public bool IsValidInput(string text) => true;
}

public class InputValidatorFactory {
    public IInputValidator CreateInputValidator() => new InputValidator();
}

上記の簡単な例では、1つのメソッドを抽出するためだけに行数が2倍以上増えました。 そもそも複雑さを減らすためにコードがリファクタリングされているときに、なぜリファクタリングがさらに複雑さを増すのでしょうか?

これをJavaScriptの非OOPコードの同様のリファクタリングと比較して下さい。

// before refactoring:

// calculator.js:
const isValidInput = text => true;

const btnAddClick = (aText, bText) => {
  if (!isValidInput(aText) || !isValidInput(bText)) {
    return;
  }
}

// after refactoring:

// inputValidator.js:
export const isValidInput = text => true;

// calculator.js:
import { isValidInput } from './inputValidator';

const btnAddClick = (aText, bText, _isValidInput = isValidInput) => {
  if (!_isValidInput(aText) || !_isValidInput(bText)) {
    return;
  }
}

コードは文字通り同じままです。単にisValidInput関数を別のファイルに移動し、その関数をインポートするための単一行を追加しました。テストの容易さのために、_isValidInputを関数シグネチャに追加しました。

これは簡単な例ですが、実際にはコードベースが大きくなるにつれて複雑さは急激に増大します。

それだけではありません。 OOPコードのリファクタリングは非常に危険です。複雑な依存関係グラフと状態がOOPコードベース全体に散らばっているため、人間の頭ですべての潜在的な問題を考慮することは不可能です。

一時しのぎ (バンドエイド)

何かがうまくいかないとき、私たちは何をしますか? それは簡単です、私たちには2つの選択肢しかありません - それを捨てるかそれを修正してみて下さい。OOPは簡単に捨てられることができないものです、何百万もの開発者がOOPで訓練されます。そして、世界中の何百万という組織がOOPを使用しています。

あなたはおそらくOOPが実際にはうまくいかないことに気付くでしょう。それは、私たちのコードを複雑で信頼できないものにします。そしてあなたは一人じゃありません! OOPコードでよく見られる問題に対処しようとする人々は何十年もの間懸命に考えてきました。彼らは無数のデザインパターンを思い付きました。

デザインパターン

OOPは理論的に開発者がますます大規模なシステムを段階的に構築することを可能にする一連のガイドラインを提供します: SOLID原則、依存性注入、デザインパターン、その他諸々。

残念ながら、デザインパターンは一時しのぎに他なりません。それらは、OOPの欠点に対処するためだけに存在します。このトピックについては、無数の本が書かれています。私たちのコードベースに非常に複雑なものを導入することに対して責任がなかったのであれば、それらはそれほど悪くはなかったでしょう。

問題のあるFactory

実際に、保守可能な優れたオブジェクト指向コードを書くことは不可能です。

一方では、一貫性がなく、どの標準にも準拠していないようなOOPコードベースがあります。それとは反対側に、私たちは過剰に設計されたコードの塔、誤った抽象の束が互いの上に積み重なっています。デザインパターンはそのような抽象化の塔を作るのにとても役に立ちます。

すぐに、新しい機能を追加すること、そしてすべての複雑さを理解することさえ難しくなります。コードベースはSimpleBeanFactoryAwareAspectInstanceFactory、AbstractInterceptorDrivenBeanDefinitionDecorator、TransactionAwarePersistenceManagerFactoryProxyorRequestProcessorFactoryFactoryのようなものでいっぱいになります。

開発者自身が作成した抽象化の塔を理解しようとすると、貴重な頭脳を無駄にしなければなりません。構造がないことは、多くの場合、悪い構造を持つよりも優れています(あなたが私に尋ねるならば)。

さらに読む:FizzBuzzEnterpriseEdition

4つのOOP支柱の崩壊

OOPの4つの柱は、抽象化、継承、カプセル化、およびポリモーフィズムです。

実際に何があるのかを1つずつ見ていきましょう。

継承

再利用性の欠如は、関数型言語ではなく、オブジェクト指向言語にあると思います。オブジェクト指向言語の問題はそれらが暗黙のうちに持ち歩く環境をすべて持っているからです。あなたはバナナを望んでいましたが、あなたが得たのはバナナを持ったゴリラとジャングル全体でした。
— ジョー・アームストロング、Erlangの作成者

OOPの継承は現実の世界とは関係ありません。実際、継承はコードの再利用性を実現するための劣った方法です。ギャング・オブ・フォーは、継承よりも合成を好むことを明示的に推奨しています。最近のプログラミング言語の中には、継承を完全に回避するものがあります。

継承にはいくつか問題があります。

  1. クラスが必要としないほど多くのコードを取り込む(バナナとジャングルの問題)。
  2. クラスの一部を別の場所に定義していると、特に複数レベルの継承があるため、コードを推論するのが難しくなります。
  3. ほとんどのプログラミング言語では、多重継承は不可能です。これは主に継承をコード共有メカニズムとして役に立たなくします。

OOPポリモーフィズム

ポリモーフィズムは素晴らしいです、それは私たちが実行時にプログラムの振る舞いを変えることを可能にします。しかし、それはコンピュータプログラミングにおける非常に基本的な概念です。なぜOOPがポリモーフィズムに重点を置いているのか、私はよく分かりません。OOPポリモーフィズムは仕事を終わらせますが、ここでもまたmental jugglingという行為をもたらします。それはコードベースをかなり複雑にします。そして、呼び出されている具体的な方法についての推論は本当に困難になります。

一方、関数型プログラミングでは、目的の実行時動作を定義する関数を渡すだけで、はるかにエレガントな方法で同じポリモーフィズムを実現できます。それより簡単なことは何でしょうか。 複数のファイル(およびインタフェース)で、多数のオーバーロードされた抽象仮想メソッドを定義する必要はありません。

カプセル化

前述したように、カプセル化はOOPのトロイの木馬です。それは実際には賛美されたグローバルなミュータブル状態であり、安全でないコードを安全に見せます。安全でないコーディング慣行は、OOPプログラマが日常業務で頼る支柱です…

抽象化

OOPの抽象化は、プログラマから不要な詳細を隠すことによって複雑さに取り組もうとします。理論的には、隠された複雑さについて考える必要なしに、開発者がコードベースについて推論することを可能にするはずです。

私は何を言うべきかさえ分かりません… 単純な概念のための空想的な言葉。手続き型/関数型言語では、実装の詳細を隣接ファイルに単に「隠す」ことができます。この基本的な行為を「抽象化」と呼ぶ必要はありません。

OOPの支柱の崩壊についての詳細は、「さようなら、オブジェクト指向プログラミング」を読んで下さい。

なぜ、OOPが業界を支配するのか?

答えは簡単です、ヒト型爬虫類の異星人種はNSA(とロシア人)と共謀して私たちプログラマーを死ぬまで虐待しました…

しかし冗談抜きに、Javaがおそらく答えです。

Javaは、MS-DOS以降のコンピューティングで起きたことで最も苦しめています。
— オブジェクト指向プログラミングの発明者であるアラン・ケイ

Javaはシンプルだった

1995年に最初に導入されたとき、Javaは他のものと比較して非常にシンプルなプログラミング言語でした。当時、デスクトップアプリケーションを書くためのエントリの障壁は高かった。デスクトップアプリケーションの開発には、低レベルのwin32 APIをC言語で記述する必要がありました。また、開発者は手動でメモリ管理にも関心を持つ必要がありました。もう1つの選択肢はVisual Basicでしたが、多くの人はMicrosoftエコシステムに縛られることを望みませんでした。

Javaが導入されたとき、それは無料だったので多くの開発者にとって非常に簡単であり、そしてすべてのプラットフォームで使用できました。組み込みのガベージコレクション、わかりやすい名前のAPI(謎めいたwin32 APIと比較して)、適切な名前空間、おなじみのCのような構文などにより、Javaはさらに親しみやすくなりました。

GUIプログラミングも一般的になりつつあり、様々なUIコンポーネントがクラスにうまくマッピングされているように見えました。IDEでのメソッドの自動補完も、OOP APIのほうが使いやすいと人々に主張させました。

おそらく、Javaが開発者にOOPを強制しない限り、Javaはそれほど悪くはなかったでしょう。Javaに関する他のすべてはかなり良さそうでした。他の主流のプログラミング言語が欠けていたそのガベージコレクション、移植性、例外処理機能は、1995年に本当に素晴らしかったです。

次に、C#が来た

当初、MicrosoftはJavaに大きく依存していました。 事態が悪くなり始めたとき(そしてSun MicrosystemsとJavaライセンスをめぐる長い合法的な戦いの後)、Microsoftは自身のバージョンのJavaに投資することにしました。それがC# 1.0が生まれたときです。言語としてのC#は、常に「より良いJava」と考えられてきました。しかし、大きな問題が1つあります。それは、同じ欠陥のある同じOOP言語であり、わずかに改善された構文の中に隠されていたことです。

Microsoftは.NETエコシステムに多大な投資をしてきました。これには優れた開発者ツールも含まれていました。何年もの間、Visual Studioはおそらく利用可能な最高のIDEの1つでした。その結果、特に企業では.NETフレームワークが広く採用されるようになりました。

ごく最近まで、MicrosoftはTypeScriptを推進することによって、ブラウザのエコシステムに多額の投資をしてきました。TypeScriptは、純粋なJavaScriptをコンパイルでき、静的型チェックなどを追加できるという点で優れています。それについては、それほど素晴らしいことではありません。機能的な構成要素を適切にサポートしていないということです - 組み込みのイミュータブルなデータ構造、関数の合成、適切なパターンマッチングがありません。TypeScriptはOOP優先で、ほとんどがブラウザのC#です。アンダース・ヘルスバーグは、C#とTypeScriptの両方の設計を担当していました。

関数型言語

一方、関数型言語は、マイクロソフトほどの規模の大きな企業に支えられたことは一度もありません。 投資がごくわずかだったのでF#は数えません。関数型言語の開発は、主にコミュニティ主導です。これはおそらく、OOP言語とFP言語の人気の違いを説明しています。

進むべき時か?

OOPは失敗した実験であることが分かりました。次に進むべき時です。私たちは、コミュニティとして、この考えが私たちを失敗させたことを認め、そして私たちはそれをあきらめなければなりません。
-- Lawrence Krubner

根本的にプログラムを編成するのに最適ではない方法を使用しているのはなぜですか? これは無知なのでしょうか? 私はそれを疑います、ソフトウェア工学で働いている人々は愚かではありません。「デザインパターン」、「抽象化」、「カプセル化」、「ポリモーフィズム」、「インターフェースの分離」などの派手なOOP用語を使用することで、同僚を前にして「スマートに見える」ことについて、おそらくもっと心配していますか? おそらくそうではありません。

私たちが何十年も使ってきたものを使い続けるのは本当に簡単だと思います。ほとんどの人は、関数型プログラミングを実際に試したことがありません。 (私のように)使っている人はOOPコードを書くことに戻ることはできません。

ヘンリー・フォードはかつてよく知られてあるように次のように言いました - 「私が人々に彼らが欲しいものを尋ねたならば、彼らはより速い馬を言ったであろう」。ソフトウェアの世界では、ほとんどの人はおそらく「より良いOOP言語」を望んでいるでしょう。人は自分が抱えている問題を簡単に説明することができますが(コードベースの整理と複雑さの軽減)、最良の解決策ではありません。

代替案は何ですか?

ネタバレ警報: 関数型プログラミング

ファンクターやモナドのような言葉があなたを少し不安にさせるのであれば、あなただけではありません! 彼らがその概念のいくつかにもっと直感的な名前を与えたならば、関数型プログラミングはそれほど恐ろしくなかったでしょう。ファンクタ? それは単に関数で変換できるものです、list.mapを考えて下さい。モナド? 連鎖できる単純な計算!

関数型プログラミングを試してみると、より良い開発者になるでしょう。ほとんどの時間を抽象化やデザインパターンについて考える必要はなく、現実世界の問題を解決する実際のコードを書く時間ができます。

あなたはこれに気付かないかも知れませんが、あなたはすでに機能的なプログラマーです。日常業務で関数を使っていますか? はい? それで、あなたはすでに機能的なプログラマーです! これらの機能を最大限に活用する方法を学ぶ必要があります。

非常に穏やかな学習曲線を持つ2つの優れた関数型言語はElixirとElmです。それらは開発者に最も重要なことに集中させます - より伝統的な関数型言語が持っている複雑さのすべてを取り除きながら信頼できるソフトウェアを書くことです。

他の選択肢は何ですか? あなたの組織はすでにC#を使っていますか? F#を試してみて下さい。これは素晴らしい関数型言語であり、既存の.NETコードとの優れた相互運用性を提供します。Javaを使っている? それならScalaかClojureを使うことは本当に良い選択です。JavaScriptを使っている? 適切なガイダンスと構文チェックがあれば、JavaScriptは優れた関数型言語になり得ます。

OOPの擁護者

私はOOPの擁護者からある種の反応を期待しています。彼らは、この記事は不正確さに満ちていると言うでしょう。中には名前を呼んでいる人もいます。 実際のOOP経験のない「ジュニア」開発者とさえ呼ぶかも知れません。私の仮定は誤っていると言う人もいるかも知れませんし、例は役に立たないのです。する事なんでも。

彼らは自分の意見に対する権利を持っています。しかし、OOPの防衛に関する彼らの主張は通常非常に弱いものです。皮肉なことに、ほとんどの人が実際には関数型言語でプログラムしたことがない可能性があります。あなたが実際に両方を試したことがない場合、どうすれば2つのことの間の比較を誰かが引き出すことができますか? このような比較はあまり役に立ちません。

デメテルの法則はあまり有用ではありません。共有されたミュータブル状態は、その状態にアクセスしたり変更したりする方法に関わらず、依然として共有されるミュータブル状態です。それは単に敷物の下の問題を一掃します。ドメイン駆動設計? これは便利な設計方法論であり、複雑さには少々役立ちます。しかし、それでも共有ミュータブル状態の基本的な問題に対処するためには何もしません。

ツールボックス内の単なるツール...

私は、OOPはツールボックス内の他のツールに過ぎないと人々が言うのをよく耳にします。はい、それは馬と車が両方とも輸送のための道具であるのと同じくらい道具箱の中の道具です... 結局のところ、それらはすべて同じ目的を果たします。古き良き馬に乗り続けることができるのに、なぜ車を使うのでしょうか?

歴史は繰り返す

これは実際に私に何かを思い出させます。20世紀の初めに、自動車が馬に取って代わり始めました。1900年ニューヨークでは道路に車が数台しかありませんでしたが、人々は交通機関として馬を使っていました。1917年には、これ以上馬が道路に残っていませんでした。巨大な産業は、馬の輸送を中心としていました。厩肥の清掃などの事業を中心に、企業全体が生まれています。

そして人々は変化に抵抗しました。彼らは自動車を別の「流行」と呼び、やがてそれが通過します。結局のところ、馬は何世紀もの間ここにいました! 政府に介入を要求する人さえいました。

これはどのような意味がありますか? ソフトウェア業界はOOPを中心としています。何百万人もの人々がOOPの訓練を受けており、何百万もの企業がコードでOOPを利用しています。もちろん、彼らは自分の生活(bread-and-butter)を脅かすものは何でも嘘だとはねつけます。それが常識です。

私たちは、歴史がそれ自身を繰り返しているのをはっきりと見ています - 20世紀にはそれは馬対自動車でした、21世紀にはそれはオブジェクト指向vs関数型プログラミングです。

次は何ですか?

SlashdotHacker News