web-dev-qa-db-ja.com

OOP-私の設計のジレンマのゼロ動作オブジェクト

OOP=の背後にある基本的な考え方は、データと動作(そのデータに対する)は不可分であり、それらはクラスのオブジェクトという概念によって結合されているということです。オブジェクトには、それと連携するデータとメソッドがあります( OOPの原則から明らかなように、単なるデータであるオブジェクト(C構造体など)はアンチパターンと見なされます。

ここまでは順調ですね。

問題は、私のコードが最近このアンチパターンの方向にますます進んでいるように見えることに気づいたことです。クラスと疎結合設計の間で情報を隠そうとするほど、クラスは純粋なデータのない動作クラスとすべての動作のないデータクラスの混合になります。

私は通常、他のクラスの存在への認識を最小限に抑え、他のクラスのインターフェースの知識を最小限に抑える方法でクラスを設計します。私は特にこれをトップダウン方式で実施します。低レベルのクラスは高レベルのクラスを知りません。例えば。:

一般的なカードゲームAPIがあるとします。クラスCardがあります。このCardクラスは、プレーヤーへの可視性を決定する必要があります。

1つの方法は、Cardクラスでboolean isVisible(Player p)を使用することです。

もう1つは、Playerクラスにboolean isVisible(Card c)を含めることです。

特に、最初のアプローチは、高レベルのPlayerクラスに関する知識を低レベルのCardクラスに与えるため、嫌いです。

代わりに、Viewportとカードのリストを指定して、どのカードが表示されるかを決定するPlayerクラスがある3番目のオプションを選択しました。

ただし、このアプローチでは、可能なメンバー関数のCardおよびPlayerクラスの両方が奪われます。カードの可視性以外のものに対してこれを行うと、すべての機能が他のクラスで実装されるため、純粋にデータを含むCardおよびPlayerクラスが残ります。 、上記のViewportのようなメソッドのみ。

これは明らかにOOPの主要な考え方に反しています。

どちらが正しい方法ですか?クラスの相互依存性を最小限に抑え、想定される知識と結合を最小限に抑えるタスクをどのように実行すればよいでしょうか。誰もが問題全体を回避するクラス設計についての3番目の解決策または見通しを持っていますか?

追伸次に別の例を示します。

不変のクラスDocumentIdがあり、単一の_BigDecimal id_メンバーとこのメンバーのゲッターのみがあるとします。ここで、DocumentIdを指定するとデータベースからこのIDのDocumentを返すメソッドがどこかに必要です。

あなたは:

  • Document getDocument(SqlSession)メソッドをDocumentIdクラスに追加し、永続性(_"we're using a database and this query is used to retrieve document by id"_)、DBへのアクセスに使用されるAPIなどに関する知識を突然紹介します。また、このクラスでは、コンパイルするためだけに永続性JARファイルが必要になりました。
  • メソッドDocument getDocument(DocumentId id)を使用して他のいくつかのクラスを追加し、DocumentIdクラスを無効、動作なし、構造体のようなクラスのままにします。
96
RokL

あなたが説明するものは、貧血ドメインモデルとして知られています。多くのOOP設計原理(デメテルの法則など)と同様に)、ルールを満たすためだけに後方に曲げる価値はありません。

ランドスケープ全体を乱雑にせず、他のオブジェクトに依存してハウスキーピングを実行しない限り、自分で行うことができる限り、値のバッグを持つことに問題はありません。

Cardのプロパティを変更するためだけの個別のクラスがある場合は、コードのにおいになるはずです-それ自体で適切に処理することが合理的に期待できる場合。

しかし、どのCardが可視であるかを知るのは、実際にはPlayerの仕事ですか?

そして、なぜCard.isVisibleTo(Player p)ではなくPlayer.isVisibleTo(Card c)を実装するのですか?またはその逆?

はい、あなたはあなたがそうしたようにそのためのある種のルールを思い付くように試みることができます-PlayerCardよりも高いレベルである(?)メソッドを見つけるための1つの場所。

時間が経つと、isVisibleTobothCardPlayerの両方に実装するという設計上の腐敗につながる可能性があります。なんでそうなの? player1.isVisibleTo(card1)card1.isVisibleTo(player1).とは異なる値を返すという恥ずべき日をすでに想像しているので、私は-それは主観的です-これは設計によって不可能にする必要があります

カードとプレーヤーの相互の可視性は、ViewportDealGameなどのコンテキストオブジェクトによって管理する必要があります。

グローバル機能を持​​つことと同じではありません。結局のところ、多くの同時ゲームがあるかもしれません。同じカードを多くのテーブルで同時に使用できることに注意してください。スペードのエースごとに多くのCardインスタンスを作成しませんか?

isVisibleToCardを実装することもできますが、それにコンテキストオブジェクトを渡して、Cardにクエリを委任します。高結合を回避するためにインターフェースするプログラム。

2番目の例と同様に、ドキュメントIDがBigDecimalのみで構成されている場合、なぜそのラッパークラスを作成するのでしょうか。

必要なのはDocumentRepository.getDocument(BigDecimal documentID);だけです

ちなみに、Javaはありませんが、C#にはstructsがあります。

見る

参考のために。それは非常にオブジェクト指向の言語ですが、誰もそれから大したことはしません。

42
Konrad Morawski

OOP=の背後にある基本的な考え方は、データと動作(そのデータに対する)は不可分であり、それらはクラスのオブジェクトという考え方によって結合されるということです。

あなたは、クラスがOOPのfundamentalの概念であると仮定することの一般的な誤りを犯しています。クラスは、カプセル化を実現するための特に一般的な方法の1つにすぎません。しかし、それをスライドさせることができます。

一般的なカードゲームAPIがあるとします。あなたはクラスカードを持っています。ここで、このCardクラスはプレーヤーへの可視性を決定する必要があります。

グッドヘブンNO。 Bridgeをプレイしているときに、 7つのハートに質問しますかダミーの手を、秘密だけが知っている秘密から全員が知っている秘密に変更するときが来ましたか?もちろん違います。これはカードの問題ではありません。

1つの方法は、CardクラスにisVisible(Player p)をブール値にすることです。もう1つは、PlayerクラスにisVisible(Card c)をブール値にすることです。

どちらも恐ろしいです。どちらもしないでください。playercardもBridgeのルールを実装する責任はありません!

代わりに、3番目のオプションを選択しました。ここでは、ビューポートクラスがあり、プレーヤーとカードのリストを指定すると、表示されるカードが決まります。

これまで「ビューポート」でカードをプレイしたことがないので、このクラスが何をカプセル化することになっているのかわかりません。私はhaveいくつかのデッキのカード、一部のプレイヤー、テーブル、そしてHoyleのコピーでカードをプレイしました。ビューポートが表すものはどれですか。

ただし、このアプローチでは、可能なメンバー関数のカードクラスとプレーヤークラスの両方が奪われます。

良い!

カードの可視性以外のものに対してこれを行うと、すべての機能が他のクラスに実装されるため、純粋なデータを含むCardクラスとPlayerクラスが残ります。これらのクラスは、ほとんどがデータのないクラスであり、上記のビューポートのようなメソッドのみです。これは明らかにOOPの主要な考え方に反しています。

番号; OOPの基本的な考え方は、オブジェクトがその懸念事項をカプセル化することです。あなたのシステムでは、カードはそれほど気にかけられていません。どちらもプレイヤーではありません。これは、世界を正確にモデル化しているためです。現実の世界では、プロパティはゲームに関連するカードであり、非常にシンプルです。カードの写真を1からの数字に置き換えることができますゲームのプレイをほとんど変更せずに、52人まで52人です。ゲームのプレイをほとんど変更せずに、4人をNorth、South、East、Westというラベルのマネキンに置き換えることができます。プレーヤーとカードは、カードゲームの世界。ルールは複雑なものなので、ルールを表すクラスは、複雑さがあるべき場所です。

さて、プレーヤーの1人がAIである場合、その内部状態は非常に複雑になる可能性があります。しかし、そのAIはカードを見ることができるかどうかを決定しません。 ルールはそれを決定します

これが私があなたのシステムを設計する方法です。

まず、複数のデッキを持つゲームがある場合、カードは驚くほど複雑です。あなたは質問を考慮する必要があります:プレイヤーは同じランクの2枚のカードを区別できますか?プレーヤー1が7つのハートの1つをプレイしてから何かが発生し、その後、プレーヤー2が7つのハートの1つをプレイした場合、プレーヤー3はそれがsameセブンハートであったと判断できます?これを注意深く検討してください。しかし、その懸念は別として、カードは非常にシンプルでなければなりません。それらは単なるデータです。

次に、プレーヤーの性質は何ですか?プレーヤーconsumes一連のvisible actionsおよびproducesanaction

ルールオブジェクトは、これらすべてを調整するものです。ルールは一連の可視アクションを生成し、プレーヤーに通知します。

  • プレーヤー1、10個のハートがプレーヤー3からあなたに渡されました。
  • プレイヤー2、カードはプレイヤー3からプレイヤー1に渡されました。

そして、プレーヤーにアクションを要求します。

  • プレイヤー1、あなたは何をしたいですか?
  • プレーヤー1は言う:frompを高音化。
  • プレーヤー1、3倍のfrompが防御できないギャンビットを生成するため、これは違法行為です。
  • プレイヤー1、あなたは何をしたいですか?
  • プレイヤー1は言う:スペードのクイーンを捨てる。
  • プレーヤー2、プレーヤー1がスペードのクイーンを捨てました。

等々。

メカニズムをポリシーから分離する。ゲームのポリシーは、 cards ではなく、 policy object にカプセル化する必要があります。カードは単なるメカニズムです。

150
Eric Lippert

データと動作の結合がOOPの中心的な考え方であることは正しいですが、それだけではありません。たとえば、encapsulation:OOP /モジュール式プログラミングでは、パブリックインターフェイスを実装の詳細から分離できます。 OOPでは、これはデータが公にアクセス可能であってはならず、アクセサを介してのみ使用されるべきであることを意味します。この定義により、メソッドのないオブジェクトは実際に役に立たないことになります。

アクセサー以外のメソッドを提供しないクラスは、本質的に複雑すぎる構造体です。 OOPを使用すると、構造体ではできない内部の詳細を柔軟に変更できるため、これは悪くありません。たとえば、メンバーフィールドに値を格納する代わりに、再計算できますまたは、バッキングアルゴリズムが変更され、それを追跡する必要がある状態。

OOPには明確な利点があります(特に、単純な手続き型プログラミングよりも優れています)が、「純粋な」OOPを目指して努力するのは初心者です。オブジェクト指向のアプローチにうまく対応できず、解決される問題もあります。他のパラダイムによってより簡単に。そのような問題に遭遇したとき、劣ったアプローチを主張しないでください。

  • フィボナッチ数列を計算することを検討してくださいオブジェクト指向の方法で。私はそれを行うための健全な方法を考えることができません。単純な構造化プログラミングは、この問題に対する最良の解決策を提供します。

  • あなたのisVisibleリレーションは両方のクラスに属しているか、どちらにも属していないか、実際にはcontextに属しています。振る舞いのないレコードは、関数型または手続き型プログラミングアプローチの典型であり、問​​題に最も適しているようです。何も悪いことはありません

    static boolean isVisible(Card c, Player p);
    

    Cardrankアクセサ以外のメソッドがないsuitに問題はありません。

29
amon

OOP=の背後にある基本的な考え方は、データと動作(そのデータに対する)は不可分であり、それらはクラスのオブジェクトという概念によって結合されているということです。オブジェクトには、それと連携するデータとメソッドがあります(明らかにOOPの原則により、単なるデータであるオブジェクト(C構造体など)はアンチパターンと見なされます(...)これは明らかにOOPの主要な考え方に反しています。

これはかなりの数の誤った前提に基づいているため、難しい質問です。

  1. OOPがコードを記述する唯一の有効な方法であるという考え。
  2. OOPは明確に定義された概念です。非常に流行語になり、OOPが何であるかについて同意できる2人を見つけるのは難しいほどです。
  3. OOPであるという考えは、データと動作をバンドルすることに関するものです。
  4. すべてが抽象である/すべきであるという考え。

#1-3についてはあまり触れません。なぜなら、それぞれが独自の答えを生み出す可能性があるためであり、意見に基づいた多くの議論を招くからです。しかし、私は「OOPはデータと動作の結合についてである」という考えが特に厄介であることに気付きます。これは、#4につながるだけでなく、すべてがメソッドであるべきだという考えにもつながります。

タイプを定義する操作と、そのタイプを使用する方法には違いがあります。 ith要素を取得できることは、配列の概念に不可欠ですが、並べ替えは、配列に対して実行できる多くのことの1つにすぎません。並べ替えは、「偶数の要素のみを含む新しい配列を作成する」以上の方法である必要はありません。

OOPはオブジェクトの使用に関するものです。 オブジェクトは抽象化を実現するための1つの方法にすぎません 。抽象化は、コード内の不要な結合を回避する手段であり、それ自体がすべてではありません。カードの概念がそのスイートとランクの値によってのみ定義されている場合は、単純なタプルまたはレコードとして実装しても問題ありません。コードの他の部分が依存関係を形成する可能性のある非本質的な詳細はありません。時々あなたは隠すものを何も持っていないだけです。

isVisibleCardタイプのメソッドにしないでください。半透明または不透明にできる非常に特殊なカードがない限り、カードを表示することはカードの概念に不可欠ではないためです。 。)。 Playerタイプのメソッドである必要がありますか?まあ、それはおそらくプレーヤーの決定的な品質でもありません。いくつかのViewportタイプの一部である必要がありますか?これも、ビューポートの定義と、カードの可視性をチェックする概念がビューポートの定義に不可欠であるかどうかによって異なります。

isVisibleはフリー関数である可能性が非常に高いです。

19
Doval

明らかにOOPの原則により、単なるデータであるオブジェクト(C構造体など)はアンチパターンと見なされます。

いいえ、そうではありません。 Plain-Old-Dataオブジェクトは完全に有効なパターンであり、プログラムの異なる領域間で永続化または通信する必要があるデータを処理するすべてのプログラムでそれらを期待します。

データレイヤーmightPlayerテーブルから読み取ったときに完全なPlayersクラスをスプールしますが、代わりに、テーブルのフィールドを持つPOD。これは、プレーヤーのPODを具体的なPlayerクラスに変換するプログラムの別の領域に渡されます。

型付きまたは型なしのデータオブジェクトの使用は、プログラムでは意味をなさない場合がありますが、それがアンチパターンになるわけではありません。それらが理にかなっている場合はそれらを使用し、そうでない場合は使用しないでください。

9
DougM

個人的には、ドメイン駆動設計がこの問題を明確にするのに役立つと思います。私が尋ねる質問は、カードゲームを人間にどのように説明するかです。言い換えれば、私は何をモデリングしていますか?私がモデリングしているものにWordの「ビューポート」とその動作に一致する概念が本当に含まれている場合は、ビューポートオブジェクトを作成し、論理的に必要なことを実行させます。

ただし、ゲームにビューポートの概念がない場合、それが必要だと思います。そうしないと、コードが「間違っている」と感じます。私はそれを私のドメインモデルに追加することについて二度考えます。

Wordモデルは、何かの表現を構築していることを意味します。私は、あなたが表現しているものを超えて、抽象的なものを表現するクラスを配置しないように注意します。

ディスプレイとインターフェイスする必要がある場合は、コードの別の部分でビューポートの概念が必要になる可能性があることを編集して追加します。しかし、DDD用語では、これはインフラストラクチャの問題であり、ドメインモデルの外部に存在します。

8
RibaldEddie

私は通常、自己宣伝はしませんが、実際にはOOPに関する設計上の問題 私のブログ について多くのことを書いています。複数のページを要約すると、クラスからデザインを開始しないインターフェースまたはAPIから始めて、そこからコードを作成すると、意味のある抽象化を提供し、仕様を適合させ、再利用できないコードで具象クラスが肥大化するのを防ぐことができます。

これがCard-Playerの問題にどのように適用されるか:ViewPortCardを2つと考える場合、Player抽象概念を作成することは理にかなっています独立したライブラリ(PlayerCardなしで使用されることもあります)。しかし、私はPlayerCardsを保持し、Collection<Card> getVisibleCards ()アクセサーを提供するべきだと考える傾向があります。これらの両方のソリューション(ViewPortとmine)は、理解しやすいコード関係を作成するという点で、isVisibleまたはCardのメソッドとしてPlayerを提供するよりも優れています。

DocumentIdには、クラス外のソリューションがはるかに適しています。 (基本的には整数)を複雑なデータベースライブラリに依存させる動機はほとんどありません。

5
Arthur Havlicek

目の前の質問が適切なレベルで回答されているかどうかはわかりません。私はフォーラムの賢明な人に、ここで質問の核心について積極的に考えるよう要請しました。

U Madは、OOP=の理解に基づいてプログラミングすると、通常、多くのリーフノードがデータホルダーになるが、上位レベルのAPIは動作。

このトピックは、isVisibleがカードとプレーヤーのどちらで定義されるかについて、少し正接していたと思います。ナイーブではありますが、これは単なる例証です。

私はここで経験を積んで、目の前の問題を調べました。 U Madが強く求めた良い質問があると思います。ルールと関連ロジックを独自のオブジェクトにプッシュすることを理解しています。しかし、私が理解しているように、質問は

  1. 本当に多くの機能を提供しない単純なデータホルダーコンストラクト(クラス/構造体。この質問のようにモデル化されているものは気にしません)があっても大丈夫ですか?
  2. はいの場合、それらをモデル化するための最良または推奨される方法は何ですか?
  3. いいえの場合、このデータカウンターパーツをより高いAPIクラス(動作を含む)にどのように組み込むか

私の見解:

オブジェクト指向プログラミングで正しく理解するのが難しい粒度について質問していると思います。私の小さな経験では、それ自体では動作を含まないエンティティをモデルに含めません。必要に応じて、データと動作をカプセル化するという考えを持つクラスとは異なり、そのような抽象化を保持するように設計された構造体をおそらく使用していました。

3
Harsha

OOPにおける一般的な混乱の原因の1つは、多くのオブジェクトが状態の2つの側面をカプセル化しているという事実にあります。それらは、オブジェクトについて知っていることと、オブジェクトについて知っていることです。オブジェクトの状態の議論では、しばしば無視されます後者の側面。オブジェクト参照が無差別であるフレームワークでは、参照が外部の世界に公開されたオブジェクトについて何が知っているかを判断する一般的な方法がないためです。

カードのこれらの側面を個別のコンポーネントにカプセル化するCardEntityオブジェクトを用意すると役立つと思います。 1つのコンポーネントはカードのマーキングに関連します(例:「ダイヤモンドキング」または「ラバブラスト;プレイヤーはAC-3で回避するか、2D6のダメージを与える」)。位置などの状態のユニークな側面に関連している可能性があります(たとえば、デッキにあるか、ジョーの手にあるか、またはラリーの前のテーブルにある)。 3分の1は、それを見ることができます(おそらく誰も、おそらく1人のプレーヤー、またはおそらく多くのプレーヤー)。すべてが同期されていることを確実にするために、カードが含まれる可能性のある場所は、単純なフィールドとしてではなく、CardSpaceオブジェクトとしてカプセル化されます。カードをスペースに移動するには、適切なCardSpaceオブジェクトへの参照をカードに与えます。その後、古いスペースから自分自身を削除し、新しいスペースに入れます。

「Xについて知っている」と「Xについて知っている」を明示的にカプセル化すると、多くの混乱を避けることができます。特に多対多の関連付けでは、メモリリークを回避するために注意が必要な場合があります(たとえば、新しいカードが存在し、古いカードが消えた場合、破棄する必要のあるカードが永続的なオブジェクトに永続的に接続されないようにする必要があります。 )しかし、オブジェクトへの参照の存在がその状態の関連部分を形成する場合、オブジェクト自体がそのような情報を明示的にカプセル化することは完全に適切です(たとえ他のクラスに実際に管理する作業を委任したとしても)。

2
supercat

ただし、このアプローチでは、可能なメンバー関数のカードクラスとプレーヤークラスの両方が奪われます。

そして、それはどのように悪い/不適切なアドバイスですか?

カードの例と同様の例を使用するには、CarDriverを検討し、DriverCarを駆動できるかどうかを判断する必要があります。

OK、つまり、Carに適切な車のキーがあるかどうかをDriverに知らせたくないと決めました。また、何らかの理由でDriverCarクラスを知らせたくないと決めました(元の質問でもこれを完全に具体化しないでください)。したがって、上記の質問のUtils値を返すためにビジネスルールを含むメソッドを含むbooleanクラスの行に沿った中間クラスがあります。

これで結構です。中間クラスは、今では車のキーを確認するだけでよいかもしれませんが、ドライバーがアルコールの影響下で有効な運転免許を持っているかどうか、またはディストピア時代の将来は、DNAバイオメトリクスを確認するようにリファクタリングできます。カプセル化することで、これらの3つのクラスが共存しても大きな問題はありません。

0
h.j.k.