ソフトウェアに抽象化が多すぎてデザインパターンが多すぎる、またはその逆のことを伝えるにはどうすればよいですか?
私が協力している開発者は、これらの点に関してプログラミングが異なっています。
小さな機能をすべて抽象化し、可能な限り設計パターンを使用して、いかなる犠牲を払っても冗長性を回避する人もいます。
私を含む他の人たちは、より実用的になるように努め、すべてのデザインパターンに完全に適合するわけではありませんが、適用される抽象化が少ないため、理解がはるかに速くなります。
これはトレードオフです。プロジェクトに十分な抽象化が行われたことを確認するには、どうすればよいですか?
例:汎用キャッシュレイヤーがMemcacheを使用して記述されている場合。本当に必要なのは、Memcache
、MemcacheAdapter
、MemcacheInterface
、AbstractCache
、CacheFactory
、CacheConnector
、...またはこれらのクラスの半分だけを使用する場合、これは維持しやすく、それでも優れたコードですか?
Twitterでこれを見つけました:
食事に必要な材料はいくつですか?車両を構築するために必要な部品はいくつありますか?
implementationを少し変更すると、コード全体で一連の変更が発生する場合、抽象化が少なすぎることがわかります。適切な抽象化は、変更が必要なコードの部分を分離するのに役立ちます。
少しのinterfaceの変更により、コード全体でさまざまなレベルで一連の変更が発生すると、抽象化が多すぎることがわかります。 2つのクラス間のインターフェースを変更する代わりに、プロパティを追加したり、メソッド引数のタイプを変更したりするためだけに、何十ものクラスやインターフェースを変更することがわかります。
それはさておき、数字を与えることによって質問に答える方法は本当にありません。抽象化の数は、プロジェクトごと、言語ごと、さらには開発者ごとに同じではありません。
デザインパターンの問題は、「ハンマーを握っているとき、すべてが釘のように見える」ということわざに要約できます。 適用設計パターンの行為は、プログラムをまったく改善しません。実際、デザインパターンを追加する場合、より複雑なプログラムを作成していると私は主張します。問題は、デザインパターンを上手く活用しているかどうかという問題であり、これが「いつ抽象化しすぎているのか」という問題の核心です。
単一の実装用のインターフェースと抽象スーパークラスを作成している場合、不要で不要な2つの追加コンポーネントがプロジェクトに追加されています。インターフェイスを提供することのポイントは、それがどのように機能するかを知らなくても、プログラム全体で同等に処理できるようにすることです。抽象スーパークラスのポイントは、実装の基本的な動作を提供することです。 one実装しかない場合は、複雑なインターフェイスとabstactクラスが提供するすべての複雑なインターフェイスとnoneの利点が得られます。
同様に、Factoryパターンを使用していて、スーパークラスでのみ使用可能な機能を使用するためにクラスをキャストしている場合、Factoryパターンはコードに利点をもたらしません。回避できたはずのクラスをプロジェクトに追加しただけです。
TL; DR私の要点は、抽象化の目的はそれ自体は抽象的ではないということです。これは、プログラムで非常に実用的の目的に役立ちます。デザインパターンを使用するか、インターフェイスを作成するかを決める前に、そうすることで、追加のプログラムにもかかわらずプログラムが理解しやすくなるかどうかを自問する必要があります。複雑さまたはプログラムは、複雑さが増す(できれば両方)にもかかわらず、より堅牢です。答えが「いいえ」または「多分」の場合、なぜそれをしたかったのか、おそらくコードに抽象化を追加する必要なしに、より良い方法で実行できるかどうかを検討するのに数分かかります。
それより下では少なすぎる、または多すぎる場合には、「必要な」数の抽象化レベルがあるとは思いません。 グラフィックデザインの場合と同様、良いOOPデザインは目に見えないものであり、当然のことと見なされるべきです。悪いデザインは常に痛い親指のように飛び出します。
ほとんどの場合、構築している抽象化のレベルの数は決してわかりません。
抽象化のほとんどのレベルは私たちには見えません。
その推論が私をこの結論に導きます:
抽象化の主な目的の1つは、プログラマーがシステムのすべての動作を常に念頭に置く必要性をなくすことです。 設計があなたに余計な知識を強いる場合何かを追加するためにシステムについては、おそらく抽象化が少なすぎます。悪い抽象化(貧弱なデザイン、貧弱なデザイン、または過剰なエンジニアリング)も、何かを追加するために、多くのことを知っておく必要があると思います。ある極端な例では、神のクラスまたは一連のDTOに基づく設計があり、もう1つの極端な例では、無数のフープを飛び越えてHello Worldを実現するためのOR /永続フレームワークがあります。どちらの場合も、あなたはあまりにも多くのことを知っています。
悪い抽象化は、いったんスイートスポットを過ぎると邪魔し始めるという事実において、ガウスベルに固執します。一方、優れた抽象化は目に見えず、そこにあることに気付かないため、あまり多くすることはできません。 API、ネットワークプロトコル、ライブラリ、OSライブラリ、ファイルシステム、ハードウェアレイヤーなどのレイヤーの数を考えてみてください。
抽象化の他の主な目的はコンパートメント化であり、エラーが特定の領域を超えて浸透することはありません。二重の船体や個別のタンクとは異なり、船体の一部に穴がある場合に船が完全に浸水するのを防ぎます。 コードへの変更が一見無関係な領域にバグを作成することになる場合次に、抽象化が少なすぎる可能性があります。
デザインパターンは、問題に対する一般的な解決策です。設計パターンを理解することは重要ですが、それらは適切に設計されたコードの兆候にすぎません(優れたコードは 4つのギャング の設計パターンのセットを無効にすることができます)。原因ではありません。
抽象化はフェンスのようなものです。これらは、プログラムの領域をテスト可能で交換可能なチャンクに分割するのに役立ちます(脆弱でない非固定コードを作成するための要件)。そしてフェンスのように:
サイズを最小限に抑えるために、自然なインターフェイスポイントで抽象化する必要があります。
それらを変更したくありません。
あなたは彼らが独立することができるものを分離することを望みます。
間違った場所にあることは、それがないことよりも悪いことです。
大きな leaks は使用しないでください。
今まで一度も「リファクタリング」という言葉が出てきませんでした。だから、ここに行きます:
新しい機能をできるだけ直接実装してください。単純なクラスが1つしかない場合は、インターフェース、スーパークラス、ファクトリーなどは必要ありません。
クラスが太りすぎるようにクラスを拡張していることに気付いた場合は、クラスを分解するときです。そのとき、-方法について考えることは非常に理にかなっています。
パターン、より具体的には4人組による「デザインパターン」は、開発者が考えて話し合うための言語を構築するため、他の理由の中でも特に優れています。「オブザーバー」、「ファクトリー」、または「ファサード」と誰もが知っている正確にすぐにそれが何を意味するか。
だから私の意見は、すべての開発者は、少なくとも基本的なことを説明する必要なくOOの概念について話すことができるようにするために、少なくとも元の本のパターンについての知識を持っているべきだと思います。あなたは実際にse可能性が現れるたびにそのパターンは?ほとんどの場合そうではありません。
ライブラリーは、パターンベースの選択肢が少なすぎるのではなく多すぎるという理由で誤りを犯す可能性がある1つの領域である可能性があります。 「fat」クラスから、より多くのパターンから派生したもの(通常は、より多くのより小さなクラスを意味する)に変更すると、インターフェースが根本的に変更されます。これは、ライブラリで通常変更したくない1つのことです。これは、ライブラリのユーザーにとって本当に興味深い唯一のことだからです。彼らは内部での機能の扱い方についてはそれほど気にしませんが、新しいAPIで新しいリリースを行うときにプログラムを常に変更しなければならない場合は、非常に気にします。
抽象化のポイントは、何よりもまず、抽象化のコンシューマ、つまり抽象化のクライアント、他のプログラマ、そして多くの場合自分にもたらされる価値であるべきです。
抽象化を使用しているクライアントとして、プログラミング作業を実行するために多くの異なる抽象化を組み合わせて組み合わせる必要がある場合、抽象化が多すぎる可能性があります。
理想的には、階層化は、いくつかの下位の抽象化をまとめ、それらの基礎となる抽象化を処理する必要なく、コンシューマーが使用できる単純で高レベルの抽象化に置き換える必要があります。基礎となる抽象化に対処する必要がある場合、レイヤーは(不完全であることにより)リークしています。消費者が非常に多くの異なる抽象化に対処しなければならない場合、階層化はおそらく欠落しています。
使用するプログラマーの抽象化の価値を検討した後、DRYの実装などの実装を評価および検討することに移ります。
はい、それはすべてメンテナンスを容易にすることですが、私たちはまず品質の抽象化とレイヤーを提供することによって消費者のメンテナンスの窮状を検討し、次に冗長性の回避などの実装面の観点から独自のメンテナンスを緩和することを検討する必要があります。
例:汎用キャッシュレイヤーがMemcacheを使用して記述されている場合。 Memcache、MemcacheAdapter、MemcacheInterface、AbstractCache、CacheFactory、CacheConnectorなどが本当に必要ですか?それとも、これらのクラスの半分だけを使用する場合、これは維持が簡単で、それでも優れたコードですか?
私たちはクライアントの視点に目を向ける必要があり、クライアントの生活が簡単になればそれは良いことです。彼らの生活がもっと複雑なら、それは悪いことです。ただし、これらの要素を単純なものにまとめる欠落したレイヤーがある可能性もあります。内部的には、これらは実装のメンテナンスをよりよくするかもしれません。しかし、あなたが疑っているように、それは単に過度に設計されている可能性もあります。
抽象化は、コードを理解しやすくするように設計されています。抽象化の層が物事をより混乱させることになるなら-それをしないでください。
目的は、正しい数の抽象化とインターフェースを使用して以下を行うことです。
これは物議を醸すメタアンサーかもしれないと思うし、私はパーティーに少し遅れるが、ここで言及することは非常に重要だと思う。
デザインパターンの使用方法に関する問題は、デザインパターンが教えられると、次のようなケースが発生することです。
この特定のシナリオがあります。この方法でコードを整理します。これは見た目はスマートですが、多少不自然な例です。
問題は、実際のエンジニアリングを始めたとき、物事がこのカットアンドドライではないということです。あなたが読んだデザインパターンはあなたが解決しようとしている問題に完全には適合しません。言うまでもなく、使用しているライブラリは、それらのパターンを説明するテキストに記載されているすべてに、それぞれ独自の方法で完全に違反しています。その結果、あなたが書くコードは「間違っている」と感じ、このような質問をします。
これに加えて、ソフトウェアエンジニアリングについて話すとき、Andrei Alexandrescuを引用します。
ソフトウェアエンジニアリングは、おそらく他のどのエンジニアリング分野よりも優れており、豊富な多様性を示します。同じことを非常に多くの正しい方法で行うことができ、正誤の間に無限のニュアンスがあります。
おそらくこれは少し大げさかもしれませんが、これは、コードに自信が持てなくなったもう1つの理由を完全に説明していると思います。
不眠症でゲームエンジンをリードするマイクアクトンの予言的な声が私の頭の中で叫ぶのは、このような時です。
データを知る
彼はあなたのプログラムへの入力と望ましい出力について話している。そして、神話の男月間からのこのフレッド・ブルックスの宝石があります:
あなたのフローチャートを見せて、あなたのテーブルを隠してください、そして私は謎に包まれ続けるでしょう。テーブルを見せてください。通常、フローチャートは必要ありません。それらは明白になります。
だから私があなたなら、私の典型的な入力ケースに基づいて私の問題を推論し、それが望ましい正しい出力を達成するかどうかを考えます。そして、このような質問をします:
これを行うと、「抽象化またはデザインパターンのレイヤーがいくつ必要になるか」という質問の答えがはるかに簡単になります。何層の抽象化が必要ですか?これらの目標を達成するために必要な数だけ。 「デザインパターンについてはどうですか?私は何も使用していません!」まあ、パターンを直接適用せずに上記の目標が達成されれば、それで問題ありません。それを機能させ、次の問題に進みます。コードからではなく、データから始めます。
すべてのソフトウェアレイヤーで、自分(または同僚)が次の上位レイヤーソリューションを表現したい言語を作成します(したがって、私の投稿ではいくつかの自然言語の類似物を投入します)。ユーザーは、その言語の読み書き方法を何年も学びたくありません。
このビューは、アーキテクチャの問題を決定するときに役立ちます。
その言語は簡単に理解できるはずです(次の層のコードを読みやすくします)。コードは書かれたよりもはるかに頻繁に読み取られます。
1つの概念は1つのWordで表現する必要があります。1つのクラスまたはインターフェイスで概念を公開する必要があります。 (スラヴ語の言語は通常、1つの英語の動詞に対して2つの異なる単語を持っているため、語彙を2回学習する必要があります。すべての自然言語は、複数の概念に対して1つの単語を使用します)。
あなたが公開する概念は驚きを含んではいけません。これは主にgetメソッドやsetメソッドなどの命名規則です。デザインパターンは標準のソリューションパターンを提供し、読者が「OK、ファクトリからオブジェクトを取得する」と見て、それが何を意味するかを知っているので役立ちます。しかし、具体的なクラスをインスタンス化するだけで十分な場合は、そうしたほうがよいでしょう。
言語は使いやすいものにする必要があります(「正しい文」を簡単に作成できるようにします)。
これらすべてのMemCacheクラス/インターフェースが次のレイヤーに表示されるようになると、キャッシュの単一の概念にこれらの単語のどれをいつどこで使用するかをユーザーが理解するまで、ユーザーは急な学習曲線を作成します。
必要なクラス/メソッドのみを公開すると、ユーザーが必要なものを見つけやすくなります(Antoine de Saint-ExuperyのDocBrowns引用を参照)。実装クラスの代わりにインターフェースを公開すると、それが簡単になります。
確立されたデザインパターンを適用できる機能を公開する場合は、別の何かを発明するよりも、そのデザインパターンに従う方が適切です。ユーザーは、完全に異なる概念よりも簡単に、設計パターンに従うAPIを理解します(イタリア語を知っている場合、中国語よりスペイン語の方が簡単です)。
抽象化を導入すると、使用が簡単になります(抽象化と実装の両方を維持するオーバーヘッドの価値があります)。
コードに(重要な)サブタスクがある場合は、「予想される方法」で解決します。つまり、別のタイプのホイールを再発明するのではなく、適切な設計パターンに従ってください。
考慮すべき重要なことは、ビジネスロジックを実際に処理する消費コードが、これらのキャッシュ関連クラスについてどの程度知る必要があるかです。理想的には、コードは、作成するキャッシュオブジェクトと、コンストラクターメソッドでは不十分な場合にそのオブジェクトを作成するファクトリーのみを考慮する必要があります。
使用されるパターンの数や継承のレベルは、各レベルを他の開発者に正当化できる限り、それほど重要ではありません。レベルを追加するごとに正当化が難しくなるため、これは非公式の制限となります。より重要な部分は、機能要件またはビジネス要件の変更によって影響を受ける抽象化のレベルの数です。単一の要件に対して1つのレベルのみに変更を加えることができる場合、抽象化されすぎたり、抽象化が不十分であったりすることはありません。複数の無関係な変更に対して同じレベルを変更した場合、抽象化されている可能性が高く、懸念事項をさらに分離する必要があります。