単一責任とカスタムデータタイプ


10

過去数か月、私はここSEや他のサイトの人々に、私のコードに関して建設的な批判をしてくれるように頼みました。ほぼ毎回飛び出し続けていることが1つあります。それでも、その推奨事項には同意しません。:P私はそれについてここで議論したいと思います、そして多分物事は私にとってより明確になるでしょう。

それは単一責任原則(SRP)に関するものです。基本的に、私はデータクラスを持っています。これは、データFontを操作するための関数だけでなく、データをロードするための関数も保持しています。ロードファンクションはファクトリクラス内に配置する必要があると、2つは分離する必要があると言われています。これはSRPの誤解だと思います...

私のフォントクラスの一部

class Font
{
  public:
    bool isLoaded() const;
    void loadFromFile(const std::string& file);
    void loadFromMemory(const void* buffer, std::size_t size);
    void free();

    void some();
    void another();
};

提案されたデザイン

class Font
{
  public:
    void some();
    void another();
};


class FontFactory
{
  public:
    virtual std::unique_ptr<Font> createFromFile(...) = 0;
    virtual std::unique_ptr<Font> createFromMemory(...) = 0;
};

提案された設計はSRPに従うと思われますが、私は同意しません—行き過ぎだと思います。Fontクラスは、長い自給自足(それは工場なしで無用である)ではない、そしてFontFactoryおそらく友情や更の実装を公開し、パブリックゲッター、を介して行われたリソースの実装の詳細を知っている必要がありますFont。これはむしろ責任分断されているケースだと思います。

これが私のアプローチの方が良いと思う理由です:

  • Fontは自給自足型—自給自足型であるため、理解と保守が容易です。また、他に何も含まなくてもクラスを使用できます。ただし、リソース(ファクトリ)のより複雑な管理が必要な場合は、簡単に行うこともできます(後で、自分のファクトリについて説明しますResourceManager<Font>)。

  • 標準ライブラリに従います—ユーザー定義型は、それぞれの言語で標準型の動作をコピーするために、可能な限り試行する必要があると思います。std::fstream自己十分であり、それは次のような機能を提供openしてclose。標準ライブラリに従うと、さらに別の方法で学習に労力を費やす必要がなくなります。その上、一般的に言えば、C ++標準委員会はおそらくここの誰よりもデザインについて知っているので、疑わしい場合は、彼らが行っていることをコピーします。

  • テスト容易性—問題が発生した場合、どこに問題があるのでしょうか?— Fontデータを処理する方法FontFactoryですか、それともデータをロードする方法ですか?あなたは本当に知りません。クラスを自己完結型にすることで、この問題が軽減されますFont。独立してテストできます。その後、ファクトリをテストするFont必要があり、正常に機能していることがわかっている場合は、問題が発生するたびにファクトリ内にある必要があることもわかります。

  • それは文脈にとらわれない—(これは私の最初の点と少し交差します。)Fontそのことを行い、それをどのように使用するかについての仮定はありません。好きな方法で使用できます。ユーザーにファクトリの使用を強制すると、クラス間の結合が増加します。

私にも工場があります

(のデザインFontが私を可能にするので。)

あるいは、単なるファクトリでFontはなく、マネージャの方が自給自足なので、マネージャは、どのようにビルドするを知る必要はありません。代わりに、マネージャは同じファイルまたはバッファがメモリに2回以上ロードされないようにします。工場でも同じことができると言えるかもしれませんが、それはSRPを壊しませんか?ファクトリはオブジェクトを構築するだけでなく、それらを管理する必要もあります。

template<class T>
class ResourceManager
{
  public:
    ResourcePtr<T> acquire(const std::string& file);
    ResourcePtr<T> acquire(const void* buffer, std::size_t size);
};

以下は、マネージャの使用方法のデモです。基本的にはファクトリーとまったく同じように使用されることに注意してください。

void test(ResourceManager<Font>* rm)
{
    // The same file isn't loaded twice into memory.
    // I can still have as many Fonts using that file as I want, though.
    ResourcePtr<Font> font1 = rm->acquire("fonts/arial.ttf");
    ResourcePtr<Font> font2 = rm->acquire("fonts/arial.ttf");

    // Print something with the two fonts...
}

結論...

(ここにtl; drを入れたいのですが、思いつきません
。あなたが持っている反論や提案されたデザインが私自身のデザインよりも優れていると思われる利点があれば投稿してください。基本的に、私が間違っていることを見せてください。:)


2
Martin FowlerのActiveRecordDataMapperを思い出します。
ユーザー

最外部のユーザー向けインターフェイスで利便性(現在のデザイン)を提供します。将来の実装変更を容易にするために、SRPを内部で使用します。イタリックとボールドをスキップするフォントローダーの装飾について考えることができます。Unicode BMPなどのみをロードします
rwong 2011


@rwong私はそのプレゼンテーションを知っています。それにブックマークを付けました(ビデオ)。:)しかし、私はあなたが他のコメントで何を言っているのか理解できません...
ポール

1
@rwongもうワンライナーじゃないですか?Fontを直接ロードするか、ResourceManagerを介してロードするかに関係なく、必要なのは1行だけです。また、ユーザーから不満が出た場合に、RMを再実装できないのはなぜですか。
ポール、

回答:


7

私の意見ではそのコードには何の問題もありません。それはあなたがそれを必要とするものを賢明で合理的に維持しやすい方法で実行します。

ただし、このコードに伴う問題は、他のことを実行したい場合は、すべてを変更する必要があることです

SRPのポイントは、アルゴリズムA()を実行する単一のコンポーネント 'CompA'があり、アルゴリズムA()を変更する必要がある場合、 'CompB'も変更する必要がないということです。

私のC ++スキルは、フォント管理ソリューションを変更する必要があるまともなシナリオを提案するにはあまりにも錆びていますが、私が行う通常のケースは、キャッシングレイヤーをスライドさせるという考えです。理想的には、ロードするものにそれがどこから来たのかを知らせたくないし、ロードされたものがどこから来たのか気にしない方がいいです。変更を加えるのが簡単だからです。それはすべて保守性についてです。

1つの例として、3番目のソース(たとえば、文字スプライトイメージ)からフォントをロードする場合があります。これを実現するには、ローダー(最初の2つが失敗した場合に3番目のメソッドを呼び出す)とFontクラス自体を変更して、この3番目の呼び出しを実装する必要があります。理想的には、別のファクトリー(SpriteFontFactoryなど)を作成し、同じloadFont(...)メソッドを実装して、フォントのロードに使用できるファクトリーのリストに貼り付けます。


1
ああ、わかりました。フォントをロードする方法をもう1つ追加する場合、マネージャーに取得関数をもう1つ、リソースにロード関数をもう1つ追加する必要があります。実際、これは1つの欠点です。ただし、この新しいソースが何であるかに応じて、データを異なる方法で処理する必要があります(TTFとフォントスプライトは別のものです)。そのため、特定のデザインがどれほど柔軟になるかを実際に予測することはできません。でもあなたの言いたいことはわかります。
ポール

ええ、私が言ったように、私のC ++スキルはかなり錆びているので、問題の実行可能なデモンストレーションを思いつくのに苦労しました、私は柔軟性について同意します。私が言ったように、それはあなたがあなたのコードで何をしようとしているのかに本当に依存します、私はあなたの元のコードが問題を完全に合理的に解決すると思います。
エドジェームズ

素晴らしい質問と素晴らしい答え、そして最良のことは、複数の開発者がそこから学ぶことができるということです。これが私がここでぶらぶらするのが好きな理由です:)。ああ、だから私のコメントは完全に冗長ではありません。SRPは少し難しいかもしれません。 YAGNIの哲学。白黒の答えは決してありません!
Martijn Verburg

0

あなたのクラスについて私を悩ませる1つのことは、あなたが持っていることloadFromMemoryloadFromFileメソッドです。理想的には、loadFromMemoryメソッドだけが必要です。フォントは、メモリ内のデータがどのようになったかを気にする必要はありません。もう1つは、ロードやfreeメソッドの代わりにコンストラクター/デストラクターを使用する必要があることです。このように、loadFromMemoryとなるFont(const void *buf, int len)free()なります~Font()


ロード関数は2つのコンストラクターからアクセス可能であり、デストラクタでfreeが呼び出されます-ここではそれを示しませんでした。最初にファイルを開いてデータをバッファに書き込み、それをFontに渡すのではなく、ファイルから直接フォントをロードできると便利です。ただし、バッファからロードする必要がある場合もあります。そのため、両方の方法を用意しています。
ポール
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.