10

継承について読むと、ある例についていつも混乱します。

通常、以下の例のような例があります。

class Shape
{
public:
    Shape() {}
    virtual ~Shape  () {}
    virtual void Draw() = 0;
};

class Cube : public Shape
{
public:
   Cube(){}
   ~Cube(){}
   virtual void Draw();
};

Shape* newCube = new Cube();
newCube->Draw(); 

私の質問は、なぜShapeそれ自体を描画するのが の責任なのですか? 形状を描画する方法を知り、代わりに形状をレンダラーに提供するのは、レンダラー クラスの責任ではないでしょうか? 次元の変化を記録したい場合はどうすればよいでしょうか? 等?の中にこれらの異なるタスクのそれぞれに対するメソッドがあるShapeでしょうか?

このような多くの例を見ると、クラスに責任を割り当てる能力について不思議に思うことがあります。クラスが1つの責任しか持たないことについて、私が理解していないことはありますか?

4

7 に答える 7

4

基本クラスのメソッドを作成する選択Draw()は、コンテキスト (解決する特定の問題) によって異なります。ここで問題をもう少し明確にするために、OO スキルの面接中に私が定期的に使用した別の例を示します。

Document クラスと Printer クラスを想像してみてください。印刷機能はどこに行くべきですか?明らかな選択肢が 2 つあります。

document.print(Printer &p);

また

printer.print(Document &d);

正しいのはどれ? 答えは、ポリモーフィックな動作が必要な場所 (ドキュメント内またはプリンター内) によって異なります。すべてのプリンターが同一の機能を備えていると仮定すると (オペレーティング システムが促進しようとする神話)、多態的な動作は Document オブジェクトにあるはずです。ただし、すべてのドキュメントがほぼ同じ (または少なくとも重要なドキュメント) であり、プリンターが大きく異なる (以前はそうであった - プロッター、ライン プリンター、レーザー プリンター、デイジー ホイール プリンターを考えてみてください) と仮定すると、など) では、ドキュメントをレンダリングする最適な方法をプリンタに決定させる方が理にかなっています。

Print()プリンターとドキュメントの両方の組み合わせからポリモーフィックな動作が必要になる可能性があるため、どちらのオブジェクトの一部であってはならないと主張することもできます。この場合、二重発送が必要です。

于 2014-07-04T08:31:46.520 に答える
3

AShapeは、それがどのように描かれたかについて何も知らないはずです。設計するプロジェクトが大きくなるほど、この決定はより重要になります。

私にとって、それはすべて循環依存関係に帰着します。

Model View Controllerの基本的な原則は、ユーザーが行うこと(動詞、または「ビュー」) が、操作または分析されるもの(名詞、または「コントローラー」)から明確に分離されているということです。 プレゼンテーションとロジックの分離. 「モデル」は中間者です。

それは単一責任の原則でもあります。「...すべてのクラスは単一の責任を持つべきであり、その責任はクラスによって完全にカプセル化されるべきです」

その背後にある理由は次のとおりです。循環依存とは、何かへの変更がすべてに影響することを意味します

単一責任の原則からのの (簡潔にするために編集された) 引用: 、したがって、別々のクラスまたはモジュールにする必要があります。異なる時期に異なる理由で変更される2つのものを結合するのは悪い設計です。」(私のものを強調)

最後に、関心の分離の概念: 「目標は、機能を他の機能とは独立して最適化できるようにシステムを設計することであり、1 つの機能の障害が他の機能の障害を引き起こさないようにし、一般的に理解しやすくすることです。 、複雑な相互依存システムを設計および管理します。」(私のものを強調)


これはプログラミング設計の問題だけではありません。

Web サイトの開発を考えてみてください。「コンテンツ」チームは、スクリプト (「開発」チームによって作成されたもの) の周りに非常に穏やかに言葉や書式設定、色、写真を配置する必要があります。そうしないと、すべてが壊れてしまいます。コンテンツ チームは、スクリプトがまったくないことを望んでいます。言葉を変更したり、画像を微調整したりするためだけに、プログラミングの方法を学ぶ必要はありません。また、開発チームは、コーディングの仕方を知らない人が行ったマイナーな視覚的変更のすべてが、自分たちのものを壊す可能性があることを心配する必要はありません.

自分のプロジェクトに取り組むとき、私は毎日このコンセプトについて考えています。2 つのソース ファイルを相互にインポートする場合、いずれかを変更すると、両方を同時に再コンパイルする必要があります。大規模なプロジェクトでは、些細な変更で数百または数千のクラスの再コンパイルが必要になる可能性があります。私が現在関わっている 3 つの主要なプロジェクトには、約 1000 の異なるソース コード ファイルがあり、この種の循環依存関係は1 つだけです。

ビジネスのチームでも、ソース コード ファイルでも、プログラミング オブジェクトの設計でも、絶対に必要でない限り、循環依存関係は避けることをお勧めします。


したがって、少なくとも、私は draw 関数を に入れませんShape。設計されているプロジェクトのタイプとサイズに大きく依存しますが、レンダリングはRenderingUtils、作業の大部分を行う public static 関数だけを含むクラスによって行うことができます。

プロジェクトが適度に大きいものであれば、さらに進んでRenderable、モデル レイヤーとしてインターフェイスを作成します。Aは をShape実装Renderableするため、それがどのように描画されるかを認識したり気にしたりしません。そして、図面がShape.

これにより、 に影響を与える (または再コンパイルする必要がない!) ことなく、レンダリングの実行方法を完全に変更できる柔軟性が得られます。また、描画コードを変更することなく、 とはShape大きく異なるものをレンダリングすることもできます。Shape

于 2014-07-03T04:50:46.960 に答える
1

上記の答えは、私には完全に複雑すぎるようです。

形状と円の例の目的は、インターフェイス (外の世界とどのように対話することが期待されるか) と実装 (どのように動作することが期待されるか) の違いを明確にすることです。

あなたが与えた例の問題は、それが切り捨てられていることです。関係する形状が多いほど、より理にかなっています。

円、三角形、長方形がある場合を考えてみましょう。では、Shape はどのように自分自身を描画するのでしょうか? それがどの種類であるか、またはそのため、何をすべきかを知りません。

次に、形状のコンテナーを考えてみましょう。それらにはすべて draw メソッドがあります。親クラスはそれを強制します。したがって、さまざまな描画方法の実装が本質的に無関係であっても、同種の形状のコンテナーを持つことができます。

なぜ円は形ではなく、自分自身を描くのですか? 方法を知っているからです。

于 2014-07-05T01:12:35.907 に答える
1

自分自身を描く方法を真に知っているのはオブジェクトだけです。

錠前屋を想像してみてください... 彼は 1000 種類の異なる錠前を選ぶことができます。私は店に行ってどんな錠でも買って彼に渡すことができます。

今、私が発明家で、デザインがユニークで革新的な独自のロックを作り始めたと想像してみてください。彼はそれらを開くことができるでしょうか?おそらく、しかしおそらくそうではない...それは私がロックの中で何をしたかによって異なります..私は彼/彼女が知っている技術を使用していますか?

あなたの形状オブジェクトは同じです...内部でどのように実装されているかによって、汎用レンダリングエンジンでレンダリングできるかどうかが決まります。各オブジェクトにそれ自体を描画するように要求する場合、これについて心配する必要はありません。

于 2014-07-04T22:30:19.117 に答える
0

純粋仮想関数は、アルゴリズムの動作がセットに対して明確に定義されていないが、アルゴリズムの存在がセットに対して明確に定義されている場合に使用さます。

これが消化するのに多すぎないことを願っていますが、おそらく機能分析からの教訓が関連しています. 仮想関数の一連の理論的な意味については、脱線します。

集合Aの族がプロパティ {x : P(x)}
を持つとする A を族A
の要素 とする A' も族Aの要素とする

A および A' は、次の 3 つのカテゴリのいずれかに該当する可能性があります。
(1) A と A' は等価
である A のすべての a 要素について、a は A' の要素であり、
かつ ~A のすべての b 要素について、b は ~A' の要素である

(2) A と A' が交差する a
が A' の要素である A の
a 要素が存在し、b が ~A' の要素である A の b 個の要素も存在する

(3) A と A' は
素である A' の要素でもある A の要素 a は存在しない

WHERE ~X は集合 X の要素ではないすべての x を参照する

(1) の場合、U が族 A の要素であることが、族Aの要素であるすべての U に対して u = P(U) となる単一の値 u の存在を意味する場合、非抽象的動作を定義します

(2) の場合、U がファミリAの要素であることが、u = P(U') となる単一の値 u の存在を意味する場合、U' は U のサブセットである場合、仮想動作を定義します。

そして (3) の場合、純粋な仮想動作を定義します。なぜなら、 A と A' は両方とも家族A のメンバーであるという点でのみ似ているため、 A と A'の交点は空集合であり、存在することを意味します。 A と A' の共通要素は存在しない

論理的な定義の観点から構文が何を意味するかを考えてみると、次のように答えることができます。

(1) 関数は抽象的である必要がありますか? (ケース 1 の場合はいいえ、ケース 2 と 3 の場合ははい) (2) 関数は純粋仮想である必要がありますか? (ケース 1 と 2 ではいいえ、ケース 3 でははい)

ケース 2 では、動作に必要な情報が保持されている場所 (基本クラスまたは派生クラス) によっても異なります。

必ずしも SHAPE の定義の一部ではない情報を DISPLAY が検索しない限り、DISPLAY から SHAPE をレンダリングすることはできません。DISPLAY は、SHAPE に定義されているものを超えて、SHAPE から派生した型の定義を調べることができないためです。したがって、派生型に含まれる情報に依存する機能は、派生クラス内の抽象化された関数に対して定義する必要があります。

于 2014-07-04T20:45:03.787 に答える