すべての主要なOOP機能を失うことなく、OOPで不変性を本当に使用できますか?


11

私のプログラムでオブジェクトを不変にすることの利点を理解しています。アプリケーションの良い設計について本当に深く考えているとき、私は多くの場合、オブジェクトの多くが不変であることに自然に到達します。多くの場合、すべてのオブジェクトを不変にしたいところになります。

この質問は同じ考えを扱っていますが、不変性に対する優れたアプローチと、それを実際に使用するタイミングを示唆する答えはありません。いくつかの良い不変のデザインパターンがありますか?一般的な考え方は、「オブジェクトを変更する必要が絶対にない限り、オブジェクトを不変にする」ことであるように見えますが、実際には役に立ちません。

私の経験では、不変性はコードをますます機能的なパラダイムに追い込み、この進行は常に起こります。

  1. リスト、マップなどの永続的な(機能的な意味での)データ構造が必要になります。
  2. 相互参照(たとえば、子が親を参照している間、子を参照しているツリーノード)で作業することは非常に不便です。
  3. 継承は意味をなさないために停止し、代わりに構成を使用し始めます。
  4. カプセル化のようなOOPの基本的な考え方全体がバラバラになり始め、私のオブジェクトは関数のように見え始めます。

この時点で、私は実際にはOOPパラダイムの何も使用しておらず、純粋に機能的な言語に切り替えることができます。したがって、私の質問:良い不変のOOP設計への一貫したアプローチがありますか、または不変のアイデアを最大限に活用すると、常にOOPの世界から何も必要としない関数型言語でプログラミングすることになりますか?OOPがバラバラにならないように、どのクラスを不変にし、どのクラスを不変にするかを決定するための良いガイドラインはありますか?

便宜上、例を示します。さんが持ってみましょうChessBoard不変のチェスの駒の不変なコレクションとして(抽象クラスを拡張しますPiece)。OOPの観点から見ると、ピースはボード上の位置から有効な動きを生成する役割を果たします。しかし、動きを生成するには、ピースがそのボードへの参照を必要とする一方で、ボードはそのピースへの参照を必要とします。さて、あなたのOOP言語に応じてこれらの不変の相互参照を作成するためのいくつかのトリックがありますが、それらは管理するのが苦痛で、ボードを参照する部分を持たない方が良いです。しかし、ボードの状態がわからないため、ピースは動きを生成できません。すると、ピースはピースタイプとその位置を保持する単なるデータ構造になります。その後、ポリモーフィック関数を使用して、あらゆる種類のピースの動きを生成できます。これは関数型プログラミングでは完全に達成できますが、ランタイム型チェックやその他の不適切なOOPプラクティスがないOOPではほとんど不可能です...


3
基本的な質問は好きですが、詳細に苦労しています。例えば、不変性を最大限にしたときに継承が意味をなさないのはなぜですか?
マーティンBa

1
オブジェクトにはメソッドがありませんか?
モニカの害


4
「OOPの観点から見ると、ピースはボード上のその位置から有効な動きを生成する役割を果たします。」-間違いなく、そのような作品を設計することはおそらくSRPを傷つけるでしょう。
ドックブラウン

1
@lishaakあなたは「継承に関する問題を簡単な言葉で説明するのに苦労しています」、それは問題ではないからです。貧弱なデザインが問題です。継承は、OOPの本質であり、必要に応じて正弦波です。いわゆる「継承の問題」の多くは、明示的なオーバーライド構文を持たない言語の問題であり、より適切に設計された言語ではそれほど問題ではありません。仮想メソッドとポリモーフィズムがなければ、OOPはありません。面白いオブジェクト構文を使用した手続き型プログラミングがあります。したがって、オブジェクト指向を避けている場合、オブジェクト指向の利点が見られないのも不思議ではありません。
メイソンウィーラー

回答:


24

すべての主要なOOP機能を失うことなく、OOPで不変性を本当に使用できますか?

なぜだか分からない。とにかくJava 8がすべて機能するようになるまで、何年もやっています。ストリングスを聞いたことがありますか?最初から素敵で不変。

  1. リスト、マップなどの永続的な(機能的な意味での)データ構造が必要になります。

同様にそれらを必要としている。私がそれを読んでいる間にあなたがコレクションを変更したので私のイテレータを無効にすることはただ失礼です。

  1. 相互参照(たとえば、子が親を参照している間、子を参照しているツリーノード)で作業することは非常に不便です。

循環参照は特別な種類の地獄です。不変性はそれからあなたを救うことはありません。

  1. 継承は意味をなさないために停止し、代わりに構成を使用し始めます。

さて、ここで私はあなたと一緒にいますが、それが不変性とどう関係するかわかりません。合成が好きな理由は、動的な戦略パターンが好きだからではなく、抽象化のレベルを変えることができるからです。

  1. カプセル化のようなOOPの基本的な考え方全体がバラバラになり始め、私のオブジェクトは関数のように見え始めます。

「OOPのようなカプセル化」のあなたの考えが何であるかを考えるとぞっとする。ゲッターとセッターが関係する場合は、そうではないため、そのカプセル化の呼び出しを停止してください。それはなかった。マニュアルのアスペクト指向プログラミングです。検証する機会とブレークポイントを置く場所はいいですが、カプセル化ではありません。カプセル化は、内部で何が起こっているかを知らないか気にしない私の権利を保持しています。

オブジェクトは関数のように見えるはずです。それらは機能のバッグです。それらは一緒に動き、一緒に再定義する機能のバッグです。

関数型プログラミングは現在流行しており、人々はOOPについてのいくつかの誤解を捨てています。これがOOPの終わりだと誤解させないでください。機能的およびOOPは非常にうまく共存できます。

  • 関数型プログラミングは、割り当てについて正式なものです。

  • OOPは、関数ポインターについて正式です。

本当にそれ。ダイクストラは私たちgotoに有害だと言ったので、それについて正式になり、構造化プログラミングを作成しました。そのように、これらの2つのパラダイムは、これらの面倒なことをさりげなく行うことから生じる落とし穴を避けながら、物事を成し遂げる方法を見つけることに関するものです。

何かお見せしましょう:

f n(x)

それは機能です。これは実際には一連の機能です。

f 1(x)
f 2(x)
...
f n(x)

OOP言語でそれをどのように表現していると思いますか?

n.f(x)

少しnそこの実装fが使用されるものを選択し、その関数で使用される定数のいくつかが何であるかを決定します(率直に言って同じことを意味します)。例えば:

f 1(x)= x + 1
f 2(x)= x + 2

それはクロージャーが提供するものと同じです。クロージャがそれを囲むスコープを参照する場合、オブジェクトメソッドはインスタンス状態を参照します。オブジェクトはクロージャーをより良くすることができます。クロージャは、別の関数から返される単一の関数です。コンストラクターは、関数のバッグ全体への参照を返します。

g 1(x)= x 2 + 1
g 2(x)= x 2 + 2

うん、あなたはそれを推測した:

n.g(x)

fとgは一緒に変化し、一緒に動き回る関数です。そのため、同じ袋に入れます。これがオブジェクトの本当の姿です。n定数(不変)を保持するということは、それらを呼び出すときにこれらが何をするかを予測するのが簡単であることを意味します。

これが単なる構造です。私がOOPについて考える方法は、他の小さなことと話す小さなことの集まりです。うまくいけば、ささいなことの小さな選択グループに。私がコーディングするとき、私は自分自身をオブジェクトとして想像します。オブジェクトの観点から物事を見る。そして、私は怠zyになろうとするので、オブジェクトをやり過ぎないようにします。私は簡単なメッセージを受け取り、それらに少し手を加え、私の親友だけに簡単なメッセージを送ります。そのオブジェクトを使い終わったら、別のオブジェクトに飛び乗り、その観点から物事を見ていきます。

そのように考えることを私に最初に教えたのは、クラス責任カードです。当時私はそれらについて混乱していましたが、今日もまだ関係がない場合は気になります。

ChessBoardを不変のチェスのピースの不変のコレクション(拡張抽象クラスのピース)とします。OOPの観点から見ると、ピースはボード上の位置から有効な動きを生成する役割を果たします。しかし、動きを生成するためには、ピースにはボードへの参照が必要ですが、ボードにはピースへの参照が必要です。

あら!繰り返しますが、不要な循環参照があります。

方法:A ChessBoardDataStructureはxyコードをピース参照に変換します。これらのピースには、x、y、および特定の値を取り、ChessBoardDataStructureそれを新しいspankingのブランドのコレクションに変えるメソッドがありますChessBoardDataStructure。次に、それを最適な動きを選択できるものに押し込みます。これChessBoardDataStructureで不変になり、ピースも不変になります。このようにして、メモリに白のポーンが1つしかありません。ちょうど適切なxyの場所にいくつかの参照があります。オブジェクト指向、機能的、不変。

待って、私たちはすでにチェスについて話していませんでしたか?


6
みんなこれを読んでください。そして、データの抽象化の理解についてウィリアムR.クックによる再訪をお読みください。そして、これをもう一度読んでください。そして、クックの「オブジェクト」と「オブジェクト指向」の簡略化されたモダンな定義提案を読んでください。アランケイの「大きなアイデアは「メッセージング」です」、彼のオブジェクト指向の定義
ヨルグWミットタグ

1
最後になりましたが、オーランド条約
ヨルグWミットタグ

1
@Euphoric:「関数型プログラミング」、「命令型プログラミング」、「論理プログラミング」などの標準定義に一致するかなり標準的な定式化であると思います。それ以外の場合、Cは関数型言語です。ロジック言語、ロジックプログラミング、ダイナミック言語、ダイナミックタイピングをエンコードできるため、アクター言語、アクターシステムをエンコードできるためです。また、Haskellは、副作用に実際に名前を付け、変数に保存し、…として渡すことができるため、世界最大の命令型言語です。
JörgW Mittag

1
言語表現力に関するマティアス・フェライセンの天才論文のアイデアと同様のアイデアを使用できると思います。たとえば、JavaからC♯へのOOプログラムの移動は、ローカル変換のみで実行できますが、Cに移動するには、グローバルな再構築が必要です(基本的に、メッセージディスパッチメカニズムを導入し、すべての関数呼び出しをリダイレクトする必要があります)。 JavaとC♯はOOを表現でき、Cはそれを「のみ」エンコードできます。
ヨルグWミッターグ

1
@lishaakさまざまなものに指定しているからです。これにより、抽象化の1つのレベルが維持され、一貫性のない位置情報ストレージの複製が防止されます。余分なタイピングを単に再送する場合は、メソッドに貼り付けて、一度だけ入力するようにします。ピースは、それがどこにあるかを伝えれば、それがどこにあるかを覚える必要はありません。ピースには、常に真で不変の色、動き、画像​​/略語などの情報のみが保存されるようになりました。
candied_orange

2

私の考えでは、OOPによってメインストリームに導入された最も有用な概念は次のとおりです。

  • 適切なモジュール化。
  • データのカプセル化。
  • プライベートインターフェイスとパブリックインターフェイスの明確な分離。
  • コードの拡張性のための明確なメカニズム。

これらの利点はすべて、継承やクラスなどの従来の実装の詳細がなくても実現できます。「オブジェクト指向システム」というアランケイの当初のアイデアは、「メソッド」の代わりに「メッセージ」を使用し、たとえばC ++よりもErlangに近かった。多くの従来のOOP実装の詳細を排除するGoを見てください。

不変オブジェクトを使用する場合でも、インターフェイス、動的ディスパッチ、カプセル化など、従来のOOPの利点のほとんどを使用できます。あなたはセッターを必要とせず、そしてしばしばより単純なオブジェクトのためにゲッターさえ必要としません。また、不変性の利点を享受することもできます。その間にオブジェクトが変更されず、防御的なコピーもデータ競合もないことを常に確信しており、メソッドを純粋にするのは簡単です。

Scalaが不変性とFPアプローチをOOPと組み合わせようとする方法を見てください。確かに、それは最もシンプルでエレガントな言語ではありません。しかし、それは合理的に実質的に成功しています。また、同様のミックスのための多くのツールとアプローチを提供するKotlinを見てください。

N年前に言語作成者が念頭に置いていたものとは異なるアプローチを試みることに関する通常の問題は、標準ライブラリとの「インピーダンスの不一致」です。OTOH Javaと.NETの両方のエコシステムは、現在、不変のデータ構造に対する合理的な標準ライブラリサポートを備えています。もちろん、サードパーティのライブラリも存在します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.