web-dev-qa-db-ja.com

「安全な」コードと「過剰に設計された」コードの間にどこに線を引くかを知る方法は?

画面に表示されるTextureに渡されるクラスRendererがあるとします。 1つの可能な設計は次のとおりです。

class Texture
{
    public:
    Texture(unsigned w, unsigned h) : w_ {w}, h_ {h}, buf_(w*h) {}

    void set_pixel(size_t i, Color c) { ... }
    std::vector<Color> image_data() const { ... }
    unsigned width() const { ... }
    unsigned height() const { ... }

    private:
    unsigned w_, h_;
    std::vector<Color> buf_;
};

このデザインは「安全」です。 buf_ベクトルは初期化されず、実装の詳細は隠されます。一方、このデザイン:

struct Texture
{
    std::vector<Color> buf;
    unsigned w {}, h {};
};

より簡単です。特に私の場合、1度か2度しか使用していない場合、2番目の場合のように単純にしておくべきか、1番目の場合のように「適切な」コードを書くべきかどうかを判断するのは困難です。

4
David Tran

クラス/構造体にTextureという名前を付けた理由を考えましたか?キーワードは、意味コンセプト意図...

あなたのstruct Texturerazorbladeになるように設計すると、非常にうまくカットされます(コンパイルのオーバーヘッド、初期化のオーバーヘッドがなく、完全に制御できます)。それらの多くを「ジャグリング」しようとする場合、(特に、初期化されていないメンバーからのnullポインター例外またはガベージ値によって)自分を傷つけるリスクが高まります。 Texture構造には、たとえば、0.05%の時間のバグ生成リスク係数があります。また、それは自明ではないので、一見見栄えの良いかみそりです。目にしたとき、それを処理しようとすると、それがどれほど危険であるかがわかりません。それをいじるほど、あなたは非常に速く切り上げられるでしょう。最後に、あなたはその潜在的な有用性があなたにとって何であるかもしれないかを非常に迅速に本当に理解していません。

class Textureデザインもカットし(問題の中心になり、それ以上ではありません)、うまくカットされます。これは包丁のようなもので、ハンドル付きです。バグ生成のリスクファクターは非常に小さく、たとえば0.001%の時間です。これは、あなたが唯一の問題なので、クラスユーザーはハンドルからナイフに触れるだけです(そのパブリックAPI、宣言されたパブリックメソッド/メンバー)のみ)、舞台裏で、すべての秘密でブレード(そのプライベートメンバー)を自由に研ぐことができます。さらに、その方法を通じて、その意図された意味/機能性がより明確になるので、それは光沢のあるsilver包丁であり、それを見ると1つわかります(ナイフブレードはほとんどの場合シルバーです。ほとんどの画像テクスチャはピクセルで作られていますが、これもまた別の議論事項です)。深刻なバグが発生する前に、もっと長くプレイすることができます。

つまり、設計された機能を最大限に活用しない場合、ソリューションは過剰に設計されています。時間とともに感じ、仕事に適したツールを選択する方法を習得する必要がありますが、多くの場合、仕事に依存します 。だから、メスを使ってパンをスライスしたり、外科医が包丁を使って手術をしたりしないのはそのためです。

ところで

2番目のケースのように単純にしておくべきか、最初のケースのように「適切な」コードを書くべきかどうかを決めるのは難しいです。

2番目のケースは単純ではありません、それは非常に単純です。複雑性のはしごを移動するときにしきい値があり、それを超えると設計が悪くなります。 シンプルに保つ は、基本要素の削除を含むことを意図したものではなく、オブジェクトの優れたコンストラクターは確かに1つです。

9
Vector Zita

これは客観的に答えることはできません。

自分でコードを使用するだけの場合は、単純なアプローチで十分です。それでも、正しく初期化するのを忘れて、これが見つけるのに数時間または数日かかる微妙なバグにつながる場合は、それについて考えを変えるかもしれません。

チームで作業している場合、構造体を正しく使用するために他のすべてのチームメンバーに依存できますか?それを本当に信頼しますか?そうでない場合は、安全なアプローチを採用することをお勧めします。

それが提供するAPIの一部であり、知らない人も使用するAPIの場合は、間違いなく安全なアプローチを採用する必要があります。

あなたの場合、それはまたパフォーマンスとメモリ消費についての質問です。この特定のケースではなく、テクスチャと3Dグラフィックス処理のようなサウンドのレンダリングで、メモリの消費量と速度は関係ありません。安全性は開発者にとってのみ有益であるため、3D APIまたはエンジンは多くの場合、パフォーマンスの安全性を扱います。エンドユーザーは、コードが正しく機能し、高速で、リソースをほとんど使用しない限り、コードが安全であるかどうかを気にしません。安全でない場合、コードを正しくすることは開発者にとってより多くの作業ですが、これも製品の価格に影響しない限り、エンドユーザーは気にしません。

個人的には、パフォーマンスが危険な方法を使用するように強制しない限り、私は安全なアプローチを好みます。どうして?シンプル:私はこの面で自分を信用していません。私が書くコードの量を考えると、2年後には詳細を覚えていないので、単純なインターフェースを処理するだけで、 "Oh、and you xを設定した場合は、最初にyを初期化します。それ以外の場合は、z "に値を設定する必要があります。代わりに、コンストラクターを呼び出します。コードを書いたときに私はこの知識を持っていたので、将来再び使用するまで失われる可能性があるので、私にとっては。すべてを文書化したり、大量のコメントを書き込んだりすることができますが、安全にするより作業が少ないのはなぜですか。

3
Mecki

このようなほとんどの状況で問題になるのは、可能なクラスに維持できる重要な不変式があるかどうかであり、追加する作業によってそれらの不変式が維持されます。

少なくとも私の経験では、維持できる不変条件がある場合、そうするためのコードを書くことは、非常に些細な場合を除いてすべて正当化される可能性がかなり高いです。

同時に、疑似クラスと疑似クラスは数十年前から知られていますが、今日でも広く見られ続けています。この論文はこの例を示しています:

class Thing {
private:
    long value;
public:
    Thing(const long x = 0) : value(x) {}
    Thing(const Thing& t) : value{t.value) {}

    ~Thing() {}

    Thing& operator=(const Thing& rs)
    {
        value = rs.value;
        return *this;
    } 

    long getValue() const
    {
        return value;
    }

    void setValue(const long x))
    {
        value = x;
    }
};

この場合、たくさんのコードが見られます-そしてそれらはすべてかなり無害ですが、最終的には無意味です。ゲッターは値をそのまま取得します。セッターは、ユーザーが指定した値を設定するだけです。 Thing a;を定義しても、long a;が同じように(そしておそらくはそれよりも)向上しないことはありません。それでも、多くの人はこれを何らかの形で「よりオブジェクト指向」であり、オブジェクト指向の正義(またはその順序の何か)への道筋を理解しています。

したがって、究極の問題は、ユーザーがより複雑なコードから何を得るかです。あなたが本当に彼らの生活をより単純にしているなら、それはおそらく正当化されるでしょう。そして、はい、場合によっては、彼らの生活をよりシンプルにすることは、何か新しいことを加えるのではなく、何らかの方法をとることを意味します。

たとえば、クラスを定義して利用可能な操作を簡単に推測できる小さなセットに減らすことは完全に合理的であり、その操作を使用してクライアントコードが失敗しないことをより確実にすることができますかもしれませんが動作しますが、本当に確実にすることは困難です(または不可能ですらあります)。

参照

  1. 疑似クラスと疑似クラスはオブジェクト指向プログラミングを混乱させる
2
Jerry Coffin