デザインについて。 パート2.実際の例

前回説明したように、設計は単純ではありません。 あらゆる種類のオプションを常に念頭に置き、エレガントなソリューションを引き裂くさまざまな要件の中から妥協点を見つけようとする必要があります。 一方では、ソリューションを維持しやすく、拡張性が高く、高性能であり、作成者だけでなく、少なくとも1人の他の人にも明確にする必要があります。 ソリューションはメモリをほとんど消費せず、OOPの100 500原則のいずれにも違反しないようにします。最も重要なことは、少なくとも1年前に終了することです。ただし、マネージャーは1か月前に準備ができていなければならないと常に主張しています。



これらの基準の多くは互いにあまり互換性がないため、遅かれ早かれ、良いデザインはそのようなものであるという結論に達します。これらの要件の重みも時間とともに変化する可能性があること。





その曖昧さのために、2つの異なるチームが異なる基準を好む多くの例を見つけることができます。 あるグループはセキュリティの決定をより重要な基準と見なすかもしれませんが、別のグループは有効性を好むかもしれません。 このようなあいまいさは、異なるプロジェクトがテクノロジーの動物園全体を使用するという事実につながり、それらのすべては実装の点で互いに非常に異なる可能性があります(しかし、.Net Framework内でも同じ問題の異なるソリューションを見つけるのは非常に簡単です)。



しかし、哲学化するのに十分な、いくつかの多かれ少なかれ具体的な例を見てみましょう。



効率と保守性


ほとんどの開発者が直面する最も一般的な妥協点の1つは、ソリューションの効率性(生産性)とその保守性の妥協点だと思います。



そのような例を見てみましょう。



internal class SomeType { private readonly int _i = 42; private readonly string _s = "42"; private readonly double _d; public SomeType() { } public SomeType(double d) { _d = d; } }
      
      







これは、フィールドの宣言時に初期化されるデフォルト値を含むクラスがある非常に典型的な例です。 ただし、この例では、C#言語コンパイラが次のようなコードに変換するため、コードが多少大きくなります。



 public SomeType() { _i = 42; _s = "42"; // System.Object::ctor(); } public SomeType(double d) { _i = 42; _s = "42"; // System.Object::ctor(); _d = d; }
      
      







したがって、宣言中に初期化されたすべてのフィールドは、基本クラスコンストラクターが呼び出される前でも値を取得します。これにより、基本クラスコンストラクターから呼び出された仮想メソッドからもアクセスでき、すべての読み取り専用フィールドは必ず初期化されます(しかし、いずれにしても、このトリックを使用しないでください!)。 しかし、各コンストラクターで複製されるコードの「膨張」により、この動作が発生します。



Jeffrey Richterの素晴らしい本「C#を介したC#」では、次のアドバイスを提供しています。



明らかに、ここでは読みやすさと保守性対有効性の古典的な妥協に直面しています。 一般に、リヒターのアドバイスは根拠があり、盲目的にそれに従う必要はありません。 このジレンマを解決するには、 読みやすさ (結局、デフォルト値を見るために毎回正しいコンストラクターを探す必要がある)と保守性 (誰かが新しいコンストラクターを追加してデフォルトコンストラクターを呼び出すのを忘れた場合)を減らす価値があるかどうかを明確に理解する必要があります。生産性の向上は得られますか? ほとんどの場合、答えは「いいえ、価値はありません!」ですが、このクラスがライブラリクラスであるか、単に何百万回もインスタンス化されている場合、私の答えはそれほど明確ではありません。





私がジェフリー・リヒターの意見に異議を唱えるとは思わないでください。リヒターは私たちのほとんどとは異なる「生地から盲目にされている」ことを明確に理解する必要があります。 彼は、ミリ秒ごとにカウントされる下位レベルの問題の解決に慣れていますが、これはほとんどのアプリケーション開発者にとってそれほど重要ではありません。



安全性と有効性


いくつかのソリューションの設計におけるもう1つの非常に一般的な妥協点は、構造とクラス(重要な型と参照型の間)の選択です。 一方では、特定のサイズ(x86プラットフォームでは24バイトのオーダー)までの構造体は、マネージヒープにメモリが割り当てられていないため、パフォーマンスを大幅に向上させることができます。 しかし、一方で、可変の重要な型の場合、多くの開発者が示唆するように振る舞いは同じではないため、多くの非常に重要な問題が発生する可能性があります。





多くの人々は、可変の重要なタイプを私たちの時代の最大の悪と考えています。 それが何であるか理解していないか、単にこの意見に同意しない場合は、 「可変の重要なタイプの危険性について」という記事をご覧ください。その後、おそらくあなたの意見が変わります;)



より具体的な例を見てみましょう。 コレクションの列挙子を実装するとき、その作成者は、この列挙子の実装方法(クラスの形式または構造体の形式)を決定する必要があります。 最初のケースでは、はるかに安全なソリューションが得られます(結局、列挙子は「可変」タイプです)、2番目のケースでは、より効果的です。



したがって、たとえば、 List < T >クラスの列挙子は構造体です。つまり、次のコードフラグメントでは、ほとんどの同僚にとって予期しない動作が発生します。



 var x = new { Items = new List<int> { 1, 2, 3 }.GetEnumerator() }; while (x.Items.MoveNext()) { Console.WriteLine(x.Items.Current); }
      
      







この振る舞いを見るほとんどの開発者は、貧しい兄弟プログラマーをあざけることを明確に決めたレドモンドの同志の愚かさにかなり合理的にresしています。 ただし、すべてがそれほど単純ではありません。



どんなコレクションでも、遅かれ早かれ、誰かがその内部コンテンツを見たいと思う瞬間が生じます。 場合によっては(たとえば、配列やリストの場合)インデクサーをこれらの目的に使用できますが、ほとんどの場合、コレクションはforeachループを使用して(直接的または間接的に)列挙されます。 私たちの多くにとって、各サイクルのヒープ上のメモリの追加割り当ては些細なことのように思えますが、.NET環境は非常に普遍的であり、ループは現代のプログラミング言語の最も一般的な構成の1つです。 そして、これらすべてが4コアプロセッサではなく、モバイルデバイスで発生する場合、BCL開発者によるそのような決定は、もはやそれほど狂気に思えません。



クラスと構造の選択(特に構造が可変の場合)は非常に重大な決定であり、設計者は、ある場合に何が得られ、別の場合に何が失われるかを正確に理解する必要があります。



シンプルさ 汎用性


上位レベルの設計になると、そのような選択の問題がしばしば発生します。ソリューションはどれくらい普遍的である必要がありますか?



多くのプログラマーおよびアーキテクトは、普遍性が変化する要件に対処する最良の方法であると直感的に信じています。 今日、顧客がつまようじのみを必要としている場合、念のため、つまようじの機能を備えたスイスナイフをすぐに作りましょう。突然要件が変わります!



実際、私たちもお客様も、それ自体で普遍的なソリューションを必要としません。 必要なのは、ソリューションに特定の柔軟性提供することですこれにより、既存の要件を変更した後、ソリューションを適応させることができます 。 同時に、概して、この柔軟性がどのように保証されるかについて誰も気にしません。単純さまたは普遍性のため。 構成ファイルを変更する必要があるか、いくつかのクラスを追加または変更する必要があるかどうか-これはそれほど重要ではありません。 唯一重要なことは、チームが新しい機会にどれだけの労力を費やす必要があるか、そしてこの決定がどのような結果をもたらすかです(そのような変更の後、システム全体が崩壊します)。



そのような選択になると、最初から「スイスナイフ」を作るかどうかを決めるとき、私は妥協案に傾倒しています。 再利用に関するメモすでに書いたように、この問題に対する最も効果的な解決策は、最初から単純な解決策であり、必要に応じて後続の反復のいずれかに一般化されます。



独創的な建築構造の使用は、独創的な言語構造の不合理な使用と同じ時期尚早な最適化です。 ほとんどの場合、普遍性は追加の複雑さを意味し、拡張ポイントが変化の風が吹く場所に向けられていない場合、過度に複雑で役に立たないソリューションになります。



クラスとメソッドを設計するときは、次のルールを使用します。モジュール、クラス、またはメソッドは、最小限の情報を「公開」する必要があります。 つまり、デフォルトでは、すべてのクラスとメソッドは、可能な限り最小のスコープである必要があります:クラス-内部(内部)、メソッド-プライベート(プライベート)。 これは有名な船長の声明のように聞こえますが、非常に頻繁に「よく、ここに別の方法があります、それは悪化しません」を出します。 最初の解決策は可能な限り単純でなければなりません。 実装に対する顧客の依存度が低いほど、これらの顧客が住みやすくなり、クラスを変更しやすくなります。 カプセル化は、クラスやモジュールによって実装の詳細を隠すだけでなく、不必要な詳細から顧客を保護することも忘れないでください。



ライブラリとユーザビリティ


特定の種類のタスクがありますが、その解決策は誰にも委ねません。 いいえ、重要なのは、このタスクを自分以外に任せないということではありません。1人の人がレベルに関係なく解決するのが不十分な特定のタスクがあるだけです。 多くのタスクは共同で解決する方がはるかに優れていますが、「別の意見」が必要なタスクのタイプが1つあります。ライブラリとフレームワークの開発について話しているところです。



すばらしい本「Framework Design Guidelines」をご覧になると、最初のページから、ライブラリ開発者の優先順位がアプリケーションアプリケーションの開発者に比べて大きくシフトしていることが明らかになります。 アプリケーション開発者が、単純さ、コードの維持、および市場投入までの時間の短縮の主な基準を持っている場合、ライブラリ開発者は自分のことを自分のメインクライアントであるライブラリユーザーよりも考える必要があります。



ライブラリ開発者は、ライブラリの主な原則であるシンプルさと使いやすさを矛盾する場合、OOPのすべての原則を採点できます。 ライブラリは、追加するすべてのソリューションを変更できない(またはほとんど変更できない)ため、保守が非常に困難になる可能性があります。



アプリケーションの設計において、間違いを犯し、数十個のオープンインターフェイスを変更する余裕がある場合、クラスに数十人の外部ユーザーがいると、すべてがより複雑になります。 Martin Fowlerには、 Published vs Public Interfacesというタイトルのすばらしい記事があり、この2つを明確に区別しています。 「公開された」インターフェースを変更するコストは劇的に増加します。つまり、ライブラリの最初のバージョンの開発中に犯した間違いは、その開発者を長年にわたって悩ませる可能性があります(ここに、Eric Lippertが最近述べた「Foolish Consistency is Foolish」という素晴らしい例があります)。 このため、Microsoftは.NET Frameworkから非常に有用なクラスを数千とまではいかなくても数百と公表することを急いでいません。新しいパブリッククラスが追加されるたびにメンテナンスコストが大幅に増加するからです。



上記のすべての妥協案に対する解決策は、アプリケーションアプリケーションからライブラリに移行する際に劇的に異なります。 微小最適化、拡張性、ダーティハック、「重大な変更」の問題、一貫性(他の多くの重要な要因を損なうこともあります)、これらすべてはライブラリで常に見られます。 そのため、ソフトウェア開発に関連するほとんどのヒントについては、これが特殊なライブラリではなく、アプリケーション開発に適用される可能性が最も高いことを明確に理解する必要があります。



おわりに


私たちが直面するトレードオフのほとんどは、いくつかのカテゴリに分類できます。 まず、フレームワーク(または広く使用されている再利用可能なライブラリ)かアプリケーションかを明確に認識する必要があります。 ここでは、2つの妥協ソリューションを選択する際に、これら2つの世界はまったく異なり、非常にクールなシフト優先順位であることを理解する必要があります。



1つまたは別のソリューションを選択する際のもう1つの非常に重要な基準は、長期的および短期的な利益(長期的利益と短期的利益)を理解するのに役立ちます。 1つのソリューションは今日のタスクには適しているかもしれませんが、将来的には多くの問題が確実に追加されます。 「技術的義務」を忘れないでください。また、このような比phorは同僚だけでなく、顧客にこのまたはその決定を行う際の「長期的な見通し」の重要性を納得させることができます。



そして最後に、プログラミングはそれ自体が目的ではなく、応用分野であることを忘れないでください。したがって、ほとんどの問題を解決するには、経験、プラグマティズム、常識が3つの非常に有用なツールです。



All Articles