web-dev-qa-db-ja.com

非OOP言語の利点と優れた用途

私はC#開発者ですが、Java、JavaScript、XSLT、CとPerlの一部、および私が忘れていた可能性のあるその他のものも知っています。それでも、私が最もよく知っているパラダイムはOOPです。

私は常にOOPは手続き型プログラミングの自然な進化であると思っていましたが、OOPはそれで完璧でしょう。Web上のいくつかの記事を読んだ後、ここで質問、私は多くの人がこれに同意していないことを発見し、一部はOOPは悪い選択肢であると言います.

開発中は、ラムダ、LINQ、匿名型の使用を高く評価しており、プロトタイピングと動的な性質を持つJavaScriptを本当に楽しんでいます。

しかし、それでも、OOPがオプションではないシナリオ、または他のパラダイムが適しているシナリオは考えられません。考えられる唯一のことは、OOPは、クラスを宣言したり、他のクラスをインポートしたり、メソッドを宣言したり、パラメーターを指定したり、型や名前を返したりして、コンソール画面に「Hello、World!しかし、それでも、実際のプログラムでは、それはそのコストを補うもののようです。

他のパラダイムはOOPよりも適していますか? OOPと比較した場合の利点と、OOPが役立つ代わりに状況を悪化させる場合はどこにありますか?特に、手続き型プログラミングと関数型プログラミングの利点と利点優れている?

6
Raphael

[...] OOPは、助けるのではなく事態を悪化させますか?[...]そして、手続き型プログラミングはどうですか?他のパラダイムと比較した場合の利点は何ですか?

私は長年にわたってCで手続き型またはフェザー級のオブジェクト指向コードを書くことを好むようになりました。私は最初から完全に一周するようにしています(多くの同僚の目で後ろ向きに作業しています)。 m恐竜になります)。その理由とそれを行うことで得られるメリットについて説明します。そして、メリットは明確なものではありません。これらは、私が過去に観察したばかりの人間の傾向に関連しています。

繰り返しますが、これらは私が個人的に観察した傾向にすぎず、OOPはこのようにする必要があると言うルールはありませんが、OOP私はまだそれをより高いレベルのコードに多く使用していますが)OOP=愛好家がカップリングの量を増やし、より単純なデータ型(おそらくintのようなプリミティブ型)から依存関係をリダイレクトする傾向から来ました)ますます複雑なユーザー定義型に。

余談ですが、私の主観を理解するのを助けるために、過去の私の専門は画像とメッシュ処理に関連していることは注目に値します。ピクセル、頂点、エッジ、三角形、およびポリゴンのループ処理に関連する多くのコードを、それらを処理および表現するためのデータ構造とともに記述しました(通常、メッシュ関連のデータ構造とアルゴリズムは最も複雑です)。これらはまた、パフォーマンスが重要な領域でもあり、私の入力サイズは、数百万のピクセル、数百万の頂点、数百万のポリゴンにまたがることがよくあります。ユーザーは、ここでパフォーマンスの違いに非常に気づきます。生産モデルと画像/テクスチャが高解像度であるためです。ただし、パフォーマンスはCへの完全な円とは関係ありません(私は、私が一般的に書いたコードは、多くの場合、高レベルのユーザーエンドの概念やユーザーインターフェイスなどから切り離されています。

My Lovely Procedural Image Library

したがって、これは孤立したものではありませんが、私の見解を理解するのに役立つ可能性のある、過去に発生したインシデントについて説明します。これが、Cでより多くの手続き型コードを再度作成することにしたのは、この1つの出来事とは異なります。しかし、私はずっと前にオブジェクト指向の愛好家のチームと協力していて(少なくとも「sorta」も含めて自分自身も数えました)、Cで記述した手続き型イメージライブラリを持ち、それは完全に外界から切り離されていました。その機能のほとんどは、RGBAピクセル形式の32ビット画像を処理するこのフォームに似ています。

void image_filter(unsigned char pixels[], int w, int h, int stride);

もちろん、私には著者の偏見がありますが、私に尋ねると、それは完全に細かくて素敵なCコードです。私が書いた、十分にテストされた、長年使用してきた、より優れた、より長持ちするものの1つです。幅広いピクセル形式のサポートを提供したり、新しいハードウェアに対するパフォーマンスを改善したり(並列ループとSIMDを使用)する以外は、これを変更する理由はありません。このような最適化を適用してテストしたい場合は歓迎します。徹底的に)。

また、完全に外界から切り離されました(非常に安定していて、個人的に維持する必要のないC標準ライブラリを数えない限り)。依存関係は次のようになりました。

enter image description here

着信(求心性)依存関係はあるが発信(遠心性)依存関係がないため、瞬く間にビルドできるようになり、必要なプロジェクトにデプロイするのが非常に簡単になりました(そして、私は多くの個人的なプロジェクトで使用していました)。非常に安定させました。それは他に何にも依存していないので、何か他のものが変わった結果として変わる理由はありませんでした。

しかし、OOP=愛好家のチームと協力しており、これはレガシーCコードベースから離れようとしている状況であり、ピアコードレビューがあり、彼らはそれを好まなかった。彼らはそれはあまりにも原始的でCに似ていると思いました。また、lerpsやクランプ、バイトで動作する最小/最大関数など、数学リポジトリ内のいくつかの数学関数を複製していると誰かが指摘しました。中心的な数学関数を使用するようにコードを変更します。これは、OOP DRYとはあまり関係がありませんでしたが、私はそれが嫌いだったので、次のようにする必要があります。

enter image description here

私の数学の「リポジトリ」(私は「ライブラリ」と呼ぶことを拒否します)が素敵でミニマリストの数学ライブラリを作成する方法を理解しようとする時間を捧げる人々によって設計されなかったため、私の画像ライブラリを非常に醜くしたと私の意見ではその責任のほとんどは事前に予想されました。それは、プログラマが韻と理由のない何かを必要とするたびに追加された関数とクラスの折衷的な混乱であり、それは絶えず変化していたので、今では私のイメージライブラリの構築に時間がかかり、これに応じて常に再構築する必要があります不安定な数学リポジトリ。

しかし、それだけではありませんでした。最終的に抽象的なImageインターフェースを導入したので、次の繰り返しは、関数がこれらの画像インターフェースで生のピクセルではなく動作するようにすることでした:

void image_filter(Image& image);

その時点で、依存関係は次のようになりました。

enter image description here

また、その画像インターフェイスも不安定で、ガウスぼかしに仮想メソッドを追加する人々が画像をぼかすように、私は本当に馬鹿げていると思う方法で変更されがちでした。このようなメソッドは、画像で実行したいすべてのことを画像インターフェイスのメンバー関数にすることなく、クラスの外部で定義および実装できる場合、インターフェイスに属している必要はありません*。

  • これは別のポイントにつながります。オブジェクトがそのクラスの直接メソッドとして必要なすべての関数を提供しようとするとは思わない(そのような関数がクラスの外部に実装されている場合、カプセル化を改善して緩める)カップリングを上げ、インターフェース設計をより安定した状態に保ちます。しかし、私はここで少数派になる傾向があり、私が一緒に働いたほとんどの人は、クラスにメソッドとして必要なすべてを追加して、オートコンプリート/インテリセンスを取得し、必要なすべてを得ることがはるかに望ましいと信じていますクラスのすぐそこにいたい。

その後、私のライブラリは手続き型で "リファクタリング"(私の見解では "解体"のようなもの)されるようになりましたが、ライブラリが提供する関数は、上記の抽象イメージインターフェイスを実装する具象クラスのメンバーになりました。

そして、それは私たちに何をもたらしましたか?多分構文上の砂糖かもしれませんが、元のコードが何にも依存しないことから複数に依存するようになったのと引き換えに、そして私の目には、設計の不十分なインターフェースと安定性のポイントを決して達成しなかった機能の「リポジトリ」、同じコードを何度も再構築する必要がある間、常に変更、更新、追加を受け取ります。そして、すべての可動部分と醜いインターフェースがあるため、次のように、コードが最初は単純なC形式であった方法を逃しました。

void image_filter(unsigned char pixels[], int w, int h, int stride);

...外界に依存していません。そして、私がCに戻る道を歩み始め、外部の世界から切り離され、分離され、変更に影響されず、一般的には非常に安定しているコードを生成する傾向を評価して、これをますます認識するようになりました。 t OO OOP化しようとしている愛好家のチーム。

だからこそ、必要に応じて、Cでの単純な古い手続き型プログラミングが好きです。私は「ライブラリ構築」の考え方でそれを使用する傾向があり、速いビルド時間、コードの分離された性質、複雑で不安定なユーザー定義型の代わりに、より単純でより安定したデータ型への依存、そして多分そのすべての美学でさえ。画像ライブラリのように低レベルでシンプルなコードであっても、安定した状態を保ち、可動部品の技術的な世界で変更する必要がないコードが好きです。不安定な世界に対する安定性の小さなスライスが好きです。オブジェクト指向のアーキテクチャとフレームワークは、多くの場合、変化する傾向のある繊細なデザインアイデアに対するカップリングの量が増えるため、はるかに不安定になる傾向があります。時間。

確かにそれは完全に主観的な答えであり、多くの偏見があり、個人的な経験によって形作られているものですが、手続き型プログラミングはこのように魅力的だと思います。

質問

他のパラダイムはOOPよりもどのシナリオに適していますか? OOPと比較した場合の利点は何ですか。また、OOPを使用すると、状況が悪化することがあります。

指で触れるのは難しいですが、「データの変換」だけに重点を置いている場合は、通常、手続き型プログラミングが適しています。上記の画像ライブラリの例は、当初はピクセルの配列を「データ」として変換するという考えに基づいて動作していました。また、ピクセルまたはそれらのコレクションを「データ」よりも複雑なものとしてモデル化することには、特にピクセルをループして変換するだけでよい関数のコンテキストでは、それほどメリットはないと思います。

そして、人々がピクセルまたはピクセルのコレクションの概念をデータ以上のものとして抽象化しようとすると、基礎となるデータがすべて同じであり、直面することはなかったとしても、抽象化は扱いにくく、不安定で、時間の経過とともに陳腐化するリスクがあります。陳腐化する傾向。その上、この場合、Imageインターフェースをモデル化してピクセルのコレクションの概念を抽象化しようとした場合、そのインターフェースを実装するオブジェクトは、相互運用する機能があるため、必ずしもピクセルのカプセル化と情報隠蔽の恩恵を受けるとは限りません。他の画像処理コードを操作するには、生のピクセルデータを公開する必要があります。このようなオブジェクトは、人類に知られているすべての画像関連の操作を提供したり、get_pixelこことset_pixelで、既存の画像処理コードに対して直接機能する能力を犠牲にします。

したがって、この場合のシナリオは、手続き型コードでピクセル「データ」を直接操作するか、機能を備えたピクセル/画像「オブジェクト」を操作しますが、実際にはデータを外部にリークする必要があるオブジェクトです。オブジェクトは不安定である可能性が高く(変更が必要になる傾向があります)、かさばっており、適切に設計することが困難です。概念の最も効果的で安定したモデルは、ユーザー定義のオブジェクトやインターフェイスではなく、単なる「生データ」のままにして、はるかに複雑な方法で対話する場合があります。

依存関係は安定性に向かって流れる必要があり(将来の変更を必要とする可能性が低いもの-マーティンが使用する定義と同じではなく、変更を必要とする可能性が最も低いコードのみ)、生データがより安定していて問題が発生しない場合があります機能の抽象的なインターフェースよりも。そのような場合、データの変換に焦点を当てた手続き型プログラミングが頻繁に発生し、保守が容易であり、通常はそもそも保守がほとんど不要なコードが生成されることがよくあります。

エンティティコンポーネントシステム

おそらく同様の理由で、Cが再び好きになり、エンティティコンポーネントシステムも好きになりました。それらの依存関係は、抽象化ではなく「生データ」に向かって均一に流れます。私の場合、依存関係が少なくて済み、オブジェクトとオブジェクトの相互作用が少ないなどの点で、抽象化よりもデータの安定化が簡単であることがよくあります。

それは私の脳にもよく似ていますか?つまり、大規模なコードベースを見るとき、私を圧倒するのは個々の関数の複雑さではありません。それは、制御フローと、ある関数から次の関数への相互作用の数です。非常に単純な1000のオブジェクトと単純な抽象化を含むコードベースをすべて個別に理解しやすく見せることができますが、オブジェクトと関数の間の間で何が起こっているのかにまだ圧倒されます。すべてと、結果として生じる依存関係の複雑なグラフの間の相互作用の数は、心を圧倒します。

そして、私が説明している種類の手続き型コードは、単に「生データ」に依存するため、これらの相互作用を最小限に抑え、コールスタックをより浅く保ち、コードベースの依存関係をグラフ化して結果に圧倒されないようにします。多くの人が関数内またはオブジェクト内レベルでクリーンなコードに焦点を当てているようですが、システムを維持および理解する私の能力につながる最終的な要因は、一般に関数間およびオブジェクト間レベルに関するものです。特定の関数またはオブジェクトを理解しやすくするよりも、コード間の相互作用と複雑な制御フローを最小限に抑える方がはるかに有益であり、生データを変換するループのみを中心に展開するより手続き的な考え方を採用すると、その効果が得られる傾向があります。

enter image description here

また、ECSはオブジェクト指向であることをまったく信じていませんが、ECSではオブジェクトであることから概念的にメリットがあるためです。システムは一般的に切り離されており、相互に通信しないため、メリットはありません。コンポーネントは生データをモデル化するため、メリットがありません。エンティティはコンポーネントを含んでいるだけなので含まれません。 OOP=言語とモデルを使用しているとしても、いくつかの小さな便宜のためにシステムをオブジェクトとしてモデル化したとしても、多くのゲームエンジンで使用されているエンティティコンポーネントシステムは、最終的に非常に手続き型の考え方をモデル化します。私は、OOPよりも手続き型プログラミングの方が適切で柔軟な例として、エンティティコンポーネントシステムの恩恵を受けるゲームエンジンを使用します。

関数型プログラミング

[...]そして関数型プログラミング?

これは私がずっと前にLISPと少し恋愛関係があったが、それにスケールの何かを書いたことがなかったので、私は答えるのが不適当です。しかし、それが不変性を助長し、副作用を阻止する方法は、非常に素晴らしく、とても美しく、このような重い再帰ロジックを頭に入れれば維持がはるかに簡単なものです。

以前のLISPの冒険は、同じことを書くのに常に10倍以上の時間を費やさなければならなかったものでした。これを再帰的に表現し、副作用なしに考えなければならなかったためです。これで)関数を返す関数を入力する関数と一緒に、コードの正確さについて簡単に推論することが容易である場所にフレークされないものを思い付きました。それは古くからありますが、考えられるEdgeのケースを調べてそれをばらばらにしようとしてもできなかったのを覚えています。また、コードの連鎖的な破損を心配することなく簡単に変更できました。

副作用を回避するためにできる限り多くの関数を記述しようとするCコードを記述しているときでも、このレッスンはまだ価値があることに気づきました。副作用がないように関数を書くコストを安くするために、あちこちで使用している不変のCデータ構造の小さなライブラリーさえ持っています。私はまだそのような関数内のデータを変更し、不変の構造は変更可能な「ビルダー」または「トランジェント」を中心に展開します。これにより、脳にとってより簡単になりますが、データは関数に対してローカルです。

ですから、関数型プログラミングは理論的には多くの場合理想的だと思います。必要以上に多くの関数が本番環境で副作用を引き起こす傾向があり、ラムダとクロージャと述語などを中心に構築された表現力のある言語がある場合、より多くの状況で適用できるアルゴリズムがはるかに多くなります。唯一の問題は、結果を維持するのがはるかに簡単だとしても、脳が関数型コードを最初から書くだけで苦労することです。そのため、関数型言語から多くの教訓を得て、実践的にC、C++、およびLuaに適用しようとします(私が最近最もよく使用する3つの言語)、最大のものは、副作用がないようにできるだけ多くの関数を記述することです(ただし、これを達成するためにすべてのデータ型を不変にする必要はありませんが、それらを値で関数にコピーして、新しい結果を返すことができます)。

まとめ

とにかく、私の混乱の中、私は要約をするように求められました。考え出してみます。

手続き型プログラミングは次の理由で便利です。

  1. それは間違いなく3つの中で最も簡単な傾向があります。
  2. それは私が良いことと悪いことの両方を考える複雑なユーザー定義型を思いとどまらせます(適切な場合は良い、そうでない場合は悪い)。これにより、最小限の結合と、データの最小公分母で動作するコードが促進される傾向があります。
  3. それは、おそらくそれがより単純でより制限されているという理由だけで、関数の生データを変換するだけのように、Excelになりがちな領域で使用される場合、古く感じられず、2年後に不自然に感じないコードを思い付くのを容易にします本来は。できないことで失敗することはありません。
  4. 何かを設計するための最も安定した方法は、単にそれをデータとして残すことです。人類がこれまでに構築した無数の画像ライブラリの中で、ピクセル形式はほんの一握りしかありませんでした。ピクセルまたはイメージの抽象的なインターフェイスではなく、未加工のピクセルデータに対して手続き的に多くのコードが記述された場合、おそらく、記述が必要なコードがはるかに少なく、保守や書き換えが必要なコードよりもはるかに少ないコードになるでしょう。

関数型プログラミングは便利だと思いますが、ここでは経験が最も限られています。

  1. 複雑なコードベースで推論するのが最も難しいのは、コードの相互作用と複雑なグラフのような制御フローの間で発生する副作用です。複雑な副作用があるため、コードの正確さについて、「何をするか」という理由だけではもはや推論できません。また、「いつ」「どこで」すべてが発生することを心配する必要があり、正確さを指数関数的に推論することが困難になります。関数型プログラミングは、純粋な関数を奨励する関数プログラミングであり、これらの副作用を最小限に抑えます。副作用の原因となる場所を最小限に抑えると、最も複雑な再帰的な制御フローでさえ、正確さの点から考えるとはるかに簡単になります。副作用を引き起こさない一連の関数は、特定のスレッドによって、特定の時間に任意の順序で呼び出される可能性があります。
  2. コンパレーター自体がラムダ関数を呼び出す関数である可能性があるのに対し、小なりコンパレーターを使用して要素の範囲をソートするなど、機能的な方法で適用できるアルゴリズムには豊富な表現力があります。関数型プログラミングを使用すると、制御フローを非常に豪華で再帰的かつ非常に柔軟にすることができます。これは、あらゆる種類の副作用が途中で発生した場合にデバッグするのは通常悪夢ですが、最初にそれらの副作用を回避することで問題を回避します場所。

OOPが役立つ理由:

  1. これはdoesにより、複雑で豊富なユーザー定義型を作成できます。生データは、生のデータ、原始的すぎるデータ、露出しすぎて、やりたいことを効果的に表現したり、不変条件を効果的に維持したりできない場合があります。
  2. オブジェクトや抽象インターフェースの概念に美しく対応するものもあります。
  3. 抽象インターフェースは、データ表現よりも安定している場合があります。例は、前方反復子または列挙可能の例です。さまざまな方法でデータを表す無数のデータ構造が作成されており、それらの多くは価値があり、さまざまな分野でExcelを使用しています。ただし、それらが表す要素は何であれ、それらに含まれるすべての要素を反復処理する前方反復子の概念は、はるかに安定した概念です。前方反復子に対して書かれたコードは、それを提供するあらゆるデータ構造を反復できます。そのような概念は、機能的な方法で、またはコルーチンまたは他の手段を通じて表現される可能性がありますが、イテレータオブジェクトは、それを設計する最も簡単な方法である可能性があります。そのような場合、順方向反復子オブジェクトによって実装された抽象順方向反復子インターフェースに対して作成されたカウント関数は、すべてが準拠する反復子オブジェクトを提供する限り、これまでに作成されたすべてのデータ構造ごとに書き換える必要はありません。

ただし、#3から続くと、標準ライブラリを超えて長い間テストを続けてきた、広く適用可能な多くの抽象インターフェースを考えるのは実際には困難です(ジェネリックや手続き型関数やいくつかの軽量関数などの他の概念を使用する傾向もあります)プログラミング)、そしてその安定性と幅広い適用性の欠如は、私がOOPについて見つける最も難しいことの1つです。

私は、データ表現がそれらを隠す意図された抽象化よりも安定している多くのケースをより簡単に思いつくことができます。生の形で長方形を表すデータと、たとえば抽象的な形状インターフェースを実装する具体的な長方形オブジェクトとを比較してください。前者には非常に少数の種類があり、唯一の注目すべき違いは、おそらく整数を使用するか浮動小数点を使用するかです。後者は無限であり、毎年すべてが行き来し、ファッショントレンドのように変化します。多くの場合、機能の標準化はデータ表現の標準化よりもはるかに困難です。多くのオブジェクト指向コードを安定させ、最小限のメンテナンス作業で済むようにするには、少なくとも特定のチーム内で機能(インターフェース設計)を標準化する必要があります。そして、特にスキルセットとデザインの感性が混在している、ゆるやかに調整された人々のチームでは、これは、ほんの一握りのことを除いて、ほとんど不可能にすることができます。

コードの再利用を最大化しようとするOO愛好家の場合、検討する価値があるのは、チームが考案した抽象的なImageインターフェイスを使用するなどして、コードがより再利用可能になり、長期にわたって広く適用できるようになるかどうかです。または、サードパーティのライブラリまたは元々持っていた、ピクセルデータを直接操作する上記の手続き型フォームから。何年も続く最も再利用可能なソリューションは、データ表現が抽象化よりも安定している場合は、データを直接操作することもあります。このような場合、抽象インターフェースを介して間接的にデータと対話するのではなく、データを直接操作することで、より広く適用可能で再利用可能な安定したコードを見つけることができます。場合によっては、抽象インターフェースが実際により安定していますが、codの設計方法を選択する際に、データを直接操作する単純な手続き型関数の代替アプローチを検討する価値もあります。 e。

9
user204677

たとえば、関数型プログラミング言語は、証明可能な正しいプログラムに関心がある場合に非常に役立ちます。これらには、常に同じ値に評価される式を操作するプロパティがあります。プログラムの「状態」の概念はありません(おそらく一部のブートストラップコードを除く)。これにより、数学的に厳密な方法で正確性を証明することが容易になります。

5
Dino