イテレータパターン-内部表現を公開しないことが重要なのはなぜですか?


24

C#Design Pattern Essentialsを読んでいます。現在、イテレータパターンについて読んでいます。

私は実装方法を完全に理解していますが、重要性を理解していないか、ユースケースを見ていません。この本では、誰かがオブジェクトのリストを取得する必要がある例を示しています。IList<T>またはなどのパブリックプロパティを公開することでこれを行うことができますArray

本は書いている

これに伴う問題は、これら両方のクラスの内部表現が外部プロジェクトに公開されていることです。

内部表現とは何ですか?実際にはそれがありますarrayIList<T>?消費者(これを呼び出しているプログラマー)がこれを知るのが悪い理由が本当にわかりません...

この本は、このパターンが機能を公開することでGetEnumerator機能するため、GetEnumerator()この方法で「リスト」を呼び出して公開できると述べています。

特定の状況では、このパターンには(すべてのように)場所があると思いますが、どこで、いつ見るかわかりません。



4
なぜなら、実装は、配列の使用から、コンシューマクラスの変更を必要としないリンクリストの使用に変更したい場合があるためです。消費者が返された正確なクラスを知っている場合、これは不可能です。
MTilsted

11
消費者が知ることは悪いことではなく、消費者が信頼することは悪いことです。情報が電話の内部のようにプライベートではなく、パスワードのように「プライベート」であると信じるようになるため、情報隠蔽という用語は好きではありません。内部コンポーネントは、何らかの秘密であるためではなく、ユーザーとは無関係であるため非表示になっています。知っておく必要があるのは、ここでダイヤルし、ここで話し、ここで聞いてください。
キャプテンマン

、およびのF知識(メソッド)を提供する素敵な「インターフェース」があるとします。すべてが順調で、さまざまなものがありますが、それは私にとってだけです。このメソッドは、「制約」またはクラスが実行をコミットする契約の条項と考えてください。必要だからといって追加するとします。これにより追加の制約が追加され、これを行うたびにsにますます強いられます。最終的に(最悪の場合)できるのは1つだけなので、それを持たないこともあります。そして、それを行うための方法は1つしかありません。abcFFdFFF
アレックティール

@captainman、なんて素晴らしいコメントだ。はい、なぜ多くのことを「抽象化」するのかわかりますが、知ることと頼ることについてのねじれは...率直に言って…。そして、あなたの投稿を読むまで考慮しなかった重要な違い。
MyDaftQuestions

回答:


56

ソフトウェアは、約束と特権のゲームです。あなたが届けることができる以上、またはあなたのコラボレーターが必要とする以上のものを約束することは決して良い考えではありません。

これは特に型に適用されます。反復可能なコレクションを書くことのポイントは、ユーザーがそれを反復できることです-これ以上でもそれ以下でもありません。通常、具象型を公開すると、Array多くの追加の約束が作成されます。たとえば、通常の方法でArrayはおそらく共同編集者が内部に格納されているデータを変更できるという事実は言うまでもなく、自分で選択した関数でコレクションを並べ替えることができます

これが良いことだと思っても(「レンダラーが新しいエクスポートオプションが欠落していることに気付いたら、すぐにパッチを適用できます!」)、全体的にこれはコードベースの一貫性を低下させ、理由-コードを推論しやすくすることがソフトウェア工学の第一の目標です。

現在、コラボレーターが多くのものにアクセスする必要がある場合、それらのいずれも見逃さないことが保証されるようにするには、Iterableインターフェースを実装し、このインターフェースが宣言するメソッドのみを公開します。そうすれば、来年、標準ライブラリに非常に優れた、より効率的なデータ構造が現れると、クライアントコードをどこでも修正することなく、基礎となるコードを切り替えて、その恩恵を受けることができます。必要以上のものを約束しないことには他の利点もありますが、これだけでも非常に大きいため、実際には他の必要はありません。


12
Exposing the concrete type Array usually creates many additional promises, e.g. that you can sort the collection by a function of your own choosing...-素晴らしい。考えもしなかった。はい、それは「反復」と、唯一の反復をもたらします!
MyDaftQuestions

18

実装を非表示にすることは、OOPの基本原則であり、すべてのパラダイムで優れたアイデアですが、遅延イテレーションをサポートする言語のイテレーター(または特定の言語で呼び出されるもの)にとって特に重要です。

具体的なタイプのイテラブルを公開する際の問題、またはインターフェイスなどもIList<T>、それらを公開するオブジェクトではなく、それらを使用するメソッドにあります。たとえば、Foosのリストを印刷する関数があるとします。

void PrintFoos(IList<Foo> foos)
{
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
    }
}

この関数はfooのリストを印刷するためにのみ使用できます-ただし、それらが実装している場合のみ IList<Foo>

IList<Foo> foos = //.....
PrintFoos(foos);

しかし、リストのすべての偶数インデックス項目を印刷したい場合はどうでしょうか?新しいリストを作成する必要があります。

IList<Foo> everySecondFoo = new List<T>();
bool isIndexEven = true;
foreach (foo; foos)
{
    if (isIndexEven)
    {
        everySecondFoo.Add(foo);
    }
    isIndexEven = !isIndexEven;
}
PrintFoos(everySecondFoo);

これは非常に長いですが、LINQを使用すると、実際には読みやすいワンライナーで実行できます。

PrintFoos(foos.Where((foo, i) => i % 2 == 0).ToList());

さて、.ToList()最後に気づきましたか?これにより、遅延クエリがリストに変換されるため、に渡すことができPrintFoosます。これには、2番目のリストの割り当てと、アイテムの2つのパス(2番目のリストを作成するための最初のリスト上のパスと、それを印刷するための2番目のリスト上のパス)が必要です。また、これがある場合:

void Print6Foos(IList<Foo> foos)
{
    int counter = 0;
    foreach (foo in foos)
    {
        Console.WriteLine(foo);
        ++ counter;
        if (6 < counter)
        {
            return;
        }
    }
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0).ToList());

foos千ものエントリがある場合はどうなりますか?それらすべてを調べて、そのうち6つを印刷するためだけに膨大なリストを割り当てる必要があります。

Enter Enumerators-C#のThe Iterator Patternのバージョン。関数がリストを受け入れる代わりに、リストを受け入れEnumerableます:

void Print6Foos(Enumerable<Foo> foos)
{
    // everything else stays the same
}

// ........

Print6Foos(foos.Where((foo, i) => i % 2 == 0));

これPrint6Foosで、リストの最初の6項目を遅延して繰り返すことができます。残りの項目を変更する必要はありません。

ここでは、内部表現を公開しないことが重要です。ときにPrint6Foos何かそのサポートランダムアクセスを- -リストを受け入れ、我々はそれにリストを与えなければならなかったし、署名はそれだけでそれを反復処理するだろうという保証はないので、そのため私たちは、リストを割り当てなければなりませんでした。実装を非表示にするEnumerableことで、関数が実際に必要とするもののみをサポートするより効率的なオブジェクトを簡単に作成できます。


6

内部表現を公開することは、事実上良い考えではありませ。コードを推論するのが難しくなるだけでなく、保守も難しくなります。あなたが内部表現を選択したと想像してくださいIList<T>-と言うことができます-そして、この内部を公開しました。コードを使用する人は誰でもリストにアクセスでき、コードはリストである内部表現に依存する場合があります。

何らかの理由で、内部表現をIAmWhatever<T>後のある時点に変更することにしました。代わりに、クラスの内部を単に変更するだけで、タイプの内部表現に依存するコードとメソッドのすべての行を変更する必要がありますIList<T>。クラスを使用しているのがあなただけである場合、これは面倒で、せいぜいエラーになりやすいですが、クラスを使用している他の人のコードを壊す可能性があります。内部を公開せずにパブリックメソッドのセットを公開した場合、クラス外のコード行に注意を払わずに内部表現を変更し、何も変更されていないかのように動作させることができます。

これが、カプセル化が重要なソフトウェア設計の最も重要な側面の1つである理由です。


6

あなたが言うことが少ないほど、あなたが言い続ける必要が少なくなります。

あなたが尋ねるほど、あなたは言われる必要が少なくなります。

あなたのコードが公開するだけならIEnumerable<T>、それだけをサポートするのは、サポートGetEnumrator()できる他のコードに置き換えることができますIEnumerable<T>。これにより、柔軟性が高まります。

コードでのみ使用IEnumerable<T>する場合は、を実装するコードでサポートできますIEnumerable<T>。繰り返しますが、追加の柔軟性があります。

たとえば、linq-to-objectsはすべてにのみ依存していIEnumerable<T>ます。いくつかの特定の実装を高速パスIEnumerable<T>しますが、テストと使用の方法でこれを行い、常に使用GetEnumerator()IEnumerator<T>戻りの実装にフォールバックできます。これにより、配列やリストの上に構築する場合よりもはるかに柔軟性が高まります。


良い明確な答え。短くして、最初の文のアドバイスに従ってください。ブラボー!
ダニエルホリンレイク

0

利用鏡に同様の文言I @CaptainManさんのコメント:『それは、消費者が内部の詳細を知っている、それが顧客のために悪いの罰金である必要性。内部の詳細を知っています』

顧客がコードを入力するarray必要がある場合、IList<T>それらのタイプが内部の詳細であった場合、消費者それらを正しく取得する必要があります。それらが間違っていると、消費者はコンパイルできなかったり、間違った結果を得たりすることさえあります。これにより、「実装の詳細は常に公開しないでください」という他の回答と同じ動作が得られますが、公開しないことの利点を掘り始めます。実装の詳細を公開しないと、顧客はそれを使用して自分自身をバインドすることはできません!

私はこの観点から考えるのが好きです。なぜなら、それは、厳格な「常に実装の詳細を隠す」のではなく、実装を意味の陰に隠す概念を開くからです。実装を公開することが完全に有効な状況はたくさんあります。リアルタイムデバイスドライバーの場合を考えてみましょう。過去の抽象化レイヤーをスキップし、バイトを適切な場所に挿入する機能は、タイミングを作成するかどうかの違いになります。または、「良いAPI」を作成する時間がなく、すぐに使用する必要がある開発環境を検討してください。多くの場合、このような環境で基礎となるAPIを公開することは、期限を設定するかどうかの違いになります。

ただし、これらの特別なケースを残してより一般的なケースを見ると、実装を隠すことがますます重要になり始めます。 実装の詳細を公開することはロープのようなものです。適切に使用すれば、背の高い山のスケーリングなど、あらゆる種類のことを実行できます。不適切に使用すると、縄で縛られる可能性があります。製品の使用方法がわからないほど、注意する必要があります。

私が何度も見るケーススタディは、開発者が実装の詳細を隠すことにあまり注意を払わないAPIを作成するケースです。2番目の開発者は、そのライブラリを使用して、APIに必要ないくつかの主要な機能が欠けていることに気付きます。機能リクエストを作成するのではなく(所要時間は数か月になる可能性があります)、彼らは代わりに隠された実装の詳細へのアクセスを悪用することに決めます(これには数秒かかります)。これ自体は悪いことではありませんが、多くの場合、API開発者はそれをAPIの一部として計画していませんでした。その後、APIを改善し、その基礎となる詳細を変更すると、誰かがそれをAPIの一部として使用したため、古い実装にチェーンされていることに気付きます。最悪のバージョンは、誰かがあなたのAPIを使って顧客中心の機能を提供し、今ではあなたの会社が s給料はこの欠陥のあるAPIに関連付けられています!顧客について十分に知らなかったときに実装の詳細を公開するだけで、APIに縄を結び付けるのに十分なロープであることがわかりました!


1
Re:「または、「良いAPI」を作成する時間がなく、すぐに使用する必要がある開発環境を検討してください」またはあなたがそうするかどうかわからない)、私は本「死の行進:「ミッション・インポッシブル」プロジェクトを生き抜くための完全なソフトウェア開発者ガイド」をお勧めします。主題に関する優れたアドバイスがあります。(また、辞めないことについて考えを変えることをお勧めします。)
ruakh

@ruakh良いAPIを作成する時間がない環境で作業する方法を理解することは、常に重要であることがわかりました。率直に言って、象牙の塔のアプローチが大好きなのと同様に、実際のコードは実際に請求書を支払うものです。認めることができるほど早く、バランスのとれたソフトウェアへのアプローチを開始し、正確な環境に合わせてAPIの品質と生産的なコードのバランスを取ります。多くの若い開発者が、理想の混乱にとらわれていて、彼らがそれを柔軟にする必要があることに気づいていないのを見ました。(私もその若い開発者でした.. 5年間の "良いAPI"デザインからまだ回復中です)
Cort Ammon-Reinstate Monica

1
はい、本当のコードが請求書を支払います。優れたAPIデザインは、実際のコードを引き続き生成できるようにする方法です。プロジェクトがメンテナンス不能になり、新しいバグを作成せずにバグを修正することが不可能になると、Crappyコードはそれ自体に対する支払いを停止します。(とにかく、これは誤ったジレンマです。良いコードに必要なのはバックボーンほど時間ではありません。)
ruakh

1
@ruakh私が見つけたのは、「優れたAPI」なしで優れたコードを作成できることです。機会があれば、優れたAPIは優れていますが、必要なものとして扱うと、価値がある以上の争いが発生します。考えてみてください:人体のAPIは恐ろしいですが、それでもエンジニアリングのちょっとした偉業だと思います=)
Cort Ammon-Reinstate Monica

私があげるもう一つの例は、タイミングを作ることについての議論のインスピレーションでした。私が協力しているグループの1つには、ハードリアルタイムの要件を備えた、開発に必要なデバイスドライバーが含まれていました。彼らには2つのAPIがありました。1つは良かったが、1つはそうではなかった。優れたAPIは実装を隠したため、タイミングをとることができませんでした。少ないAPIで実装が公開されたため、プロセッサ固有の手法を使用して実用的な製品を作成できました。優れたAPIは丁寧に棚に置かれ、タイミング要件を満たし続けました。
コートアンモン-復帰モニカ

0

データの内部表現が何であるかを必ずしも知る必要はありません-または、将来的になる可能性があります。

配列として表すデータソースがあると仮定して、通常のforループでそれを反復するかもしれません。

ここで、リファクタリングしたいと言います。上司は、データソースがデータベースオブジェクトであることを要求しました。さらに、彼は、API、ハッシュマップ、リンクリスト、回転する磁気ドラム、マイク、または外モンゴルで見つかったヤクの削りくずのリアルタイムカウントからデータをプルするオプションが必要です。

forループが1つしかない場合は、コードを簡単に変更して、データオブジェクトへのアクセス方法を簡単に変更できます。

データオブジェクトにアクセスしようとするクラスが1000個ある場合、今では大きなリファクタリングの問題があります。

イテレータは、リストベースのデータソースにアクセスする標準的な方法です。これは一般的なインターフェイスです。これを使用すると、コードのデータソースに依存しなくなります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.