プライベートデータを公開せずにクラスを分割する良い方法を見つけるのに苦労しています。 SRPについて私が読んだほとんどの記事は、分離された責任を引き受ける新しいクラスが、元のクラスに対して以前はプライベートであったデータにアクセスする方法を無視しているようです。
たとえば、グラフィカルコンピュータシミュレーションでロボットグリッパーを表すGripper
クラスを考えます。このクラスは、グリッパーのロジック、アイテムのピックアップ、回転、別の位置への配置などを処理します。グリッパー自体をGUIに描画することもできます。
Gripper
クラスを変更する理由は2つあります。グリッパーの動作に関するロジックへの変更と、グリッパーの描画方法に関する変更です。ただし、Gripper
には、ロジックと描画部分の両方で使用されるプライベートデータメンバーがいくつかあります。一部の(const)ゲッターを介してこれらのメンバーを公開するだけでは、後退したように感じられます。私は実装の詳細を公開し、この新しい「インターフェース」をサポートすることに専念していますが、それはまったく間違っているようです。
だから私はこれを思いつきました:
class Renderer
{
public:
/* Takes the data needed to draw a gripper and does so. */
void
DrawGripper(const Foo& foo, const Qux& qux);
/* Additional methods to draw other things. */
};
class Gripper
{
public:
void
Draw(Renderer& renderer) const
{
renderer.DrawGripper(mFoo, mQux);
}
private:
Foo mFoo;
Bar mBar;
Qux mQux;
};
プロの:
Draw
関数を除いて、すべての描画コードはGripper
からなくなりました。Renderer
は抽象的なインターフェイスであり、さまざまな実装を簡単に許可できます。DrawGripper
に渡すことができますが、単純なメンバー関数はすべてのメンバーに完全にアクセスできます。短所:
Gripper
には、まだDraw
関数があり、Renderer
について知っています。詐欺は扱いにくいと思います。結局、Gripper
が存在する理由の1つは、最終的には画面に描画されるためです。そのため、Draw
関数がまだあるという事実は、それほど悪くないようです。おそらくこれは、2つの悪の小さい方を選択する必要がある場合でしょうか。個人データを公開する代わりの方法は、はるかに悪いことです。
私はここで正しい軌道に乗っていますか?これは、このような場合に展開できる優れたシステムですか?問題またはより良い方法?
私はこれらの線に沿って何かに行くことに決めました:
_class GripperRenderer
{
public:
virtual
void Draw(const Foo& foo, const Qux& qux) = 0;
};
class Gripper
{
public:
void
AcceptRenderer(GripperRenderer& renderer) const
{
renderer.Draw(mFoo, mQux);
}
private:
Foo mFoo;
Bar mBar;
Qux mQux;
};
_
@Emerson Cardosoの回答に従ってゲッターを使用することは、まったく受け入れられません。描画の必要性がなければ、誰もビジネスを行っていない実装の詳細を公開します。たとえば、GetPosition()
メソッドは、デカルト座標系を公開し、それをサポートすることに永久に結び付けます。クライアントは、本来の目的でGetPosition()
メソッドを使用することができます(使用する予定ですが)。今後、内部座標系を変更する場合、GetPosition()
メソッドを機能させるために変換をサポートする必要があります。これは面倒で、場合によっては不可能であり、確実に保守性を向上させることはできません。
これは有名な記事の背後にある考え方/動機です "getterとsetterの方法がなぜ悪いのか) アレン・ホルブによると私は信じています。
はい、Gripper
にはAcceptRenderer()
メソッドがあるため、GripperRenderer
を認識しています。 Gripper
のプライベートメンバーにアクセスする唯一の方法は、トラフ/ Gripper
自体のアクセス許可があるためです。 Gripper
の描画のためだけにプライベートへのアクセスを明示的に許可する方法と見なすことができます。これはfriend
(描画に必要なメンバーのみが渡される)を使用するよりもきめ細かく、実装の詳細をすべての人に公開するよりもはるかに優れています。責任はまだ分割されています。
単純にデータを受け入れ、それを公開するGripperRenderer
実装を記述できますか?もちろん。しかし、意図的な悪/愚かさから保護することはできません。そうすることは、適切なプログラマーにとってはコードの匂いになるはずですが、パブリックゲッターを使用すると(もしあれば)、それは完全に合理的なことです。
IMO、新しいアプローチから、Renderer
クラスとGripper
クラスの両方がSRPを壊します。
Renderer
は、レンダリングされる各アイテムのすべての詳細の知識を必要とします。この問題の神のクラスになる可能性があります(次のように考えてください:このクラスには複数のソースまたは変更する理由があります); Gripper
クラスは、あなたが言ったように、まだdrawメソッドを持っています。提案
Renderer
)を表すインターフェイス(またはcppの抽象クラス)を作成します。GripperRenderer
-この具象クラスに必要なすべての特定のものは、コンストラクターに渡す必要があります。そうしないと、Draw
メソッドシグネチャを標準化できません);Gripper
クラスは、その動作/ロジックを処理する責任を負ってください。他のクラスからアクセスする必要があるすべての情報は、getterメソッドで公開する必要があります。Gripper
をインスタンス化するときは、そのGripperRenderer
もインスタンス化し、レンダリング操作に必要なすべての情報をレンダリングに渡す必要があります。サンプルソース
上記の私の提案の簡単な図を以下に示します(コーディングエラーがある場合はお詫び申し上げます)。
class Renderer
{
public:
// Only has a draw method, according to this class' responsibility
virtual void Draw() = 0;
};
class GripperRenderer : public Renderer
{
public:
GripperRenderer(const Foo& foo, const Qux& qux) {
// Receives any data, or pointer to data, needed
// for the rendering
}
void Draw() {
// draws the gripper, according to class' responsibility
}
};
class Gripper
{
public:
// Methods that handle only gripper logic, according to class' responsibility
...
// Getters for stuff used by external objects
Foo& getFoo();
Qux& getQux();
private:
Foo mFoo;
Bar mBar;
Qux mQux;
};
IMO、これは理解しやすく、簡単に保守できます。
これについてさらに支援が必要な場合は、おそらく、問題のより複雑で現実的な例を示す必要があります。
Gripper
はRenderer
を保持しますか? Gripper
が不変のデータGesture
を提供し、Renderer
がGesture
を要求するようにすることをお勧めします。上記の操作は、画面の再編集のためのオブジェクトによって行われます。
Gesture
をインターフェースとして公開することにより、Gripper
を変更する人は、公開データが変更されないことを認識できます。
Foo
をリファクタリングしてもRenderer
は壊れません。
また、関連しないオブジェクトBar
が、Renderer
で使用される可能性を削減します。
SRPについて読んで、SRPのためにクラスを分割する必要があると考え、その結果、公開したくないプライベートメンバーを公開するという問題が発生した場合は、後戻りして検討する必要があります。この:
SRPはあなたの人生をより簡単にするためにあります。 SRPをフォローすると生活が困難になる場合は、SRPを誤解している可能性が高いです。 SRPはおそらくそれらすべての中で最も誤解されているガイドラインです。そして、たとえそれが正しかったとしても、それがあなたの人生をより簡単にするのではなくより困難にするならば、それからそれをしないでください。
あなたの場合:ロボットアームの動作方法を変更すると、グリッパーの動作コードとレンダリング方法のコードの両方を変更する必要が生じる可能性が高くなります。
もちろん、OpenGLを使用してレンダリングするレンダリングコードを記述していて、Metalを使用してレンダリングするように変更したい場合があります。さて、その場合、あなたの問題は完全に別の場所にあります。レンダリングコードは、レンダリングするアイテムの説明を生成するだけで、使用するテクノロジーとは無関係である必要があります。
多くの場合、複雑な状態を含むオブジェクトには、他のオブジェクトに物事を委任するという高レベルの責任があり、その状態の知識を使用して構成します。
レンダラーオブジェクトは、グリッパーの現在の状態のグラフィック表現を作成する役割を果たし、グラバーの状態から派生した情報を使用してグラバーによって構成されます。
この他のオブジェクトがグラバーと同じモジュールの一部である場合、それらは内部状態の要素の表現のための非公開コードを共有できます。
しかし、ごく単純な場合を除いて、関連する責任を持つ2つの関連オブジェクトが状態表現全体を共有することはまれです。
レンダラーは、グリッパーによって提供される構成に関する知識があるため、可能なことのみを実行します。使用中の描画APIと互換性のある形式で幾何学的表現を作成する。