非同期関数を公開するインターフェイスは、リークの多い抽象化ですか?


12

私は「依存性注入の原則、実践、およびパターン」という本を読んでおり、この本で十分に説明されているリークのある抽象化の概念について読んでいます。

最近では、依存関係の注入を使用してC#コードベースをリファクタリングし、非同期呼び出しをブロックの代わりに使用しています。そうすることで、コードベースの抽象化を表し、非同期呼び出しを使用できるように再設計する必要があるいくつかのインターフェイスを検討しています。

例として、アプリケーションユーザーのリポジトリを表す次のインターフェースについて考えてみます。

public interface IUserRepository 
{
  Task<IEnumerable<User>> GetAllAsync();
}

本の定義によれば、リークの多い抽象化は特定の実装を念頭に置いて設計された抽象化であり、一部の実装は抽象化自体を通じて「リーク」します。

私の質問は次のとおりです。IUserRepositoryなどの非同期を念頭に置いて設計されたインターフェイスを、漏れやすい抽象化の例として検討できますか?

もちろん、可能なすべての実装が非同期と関係があるわけではありません。アウトプロセス実装(SQL実装など)だけが必要ですが、インメモリリポジトリは非同期を必要としません(実際にインメモリバージョンのインターフェイスを実装する方がおそらくインターフェースが非同期メソッドを公開している場合は困難です。たとえば、メソッドの実装でTask.CompletedTaskまたはTask.FromResult(users)のようなものを返す必要がある場合があります。

あれについてどう思う ?


@ニール私はおそらくポイントを得ました。TaskまたはTask <T>を返すメソッドを公開するインターフェースは、それ自体がリークの多い抽象化ではなく、単にタスクを含む署名を持つコントラクトです。TaskまたはTask <T>を返すメソッドは、非同期実装があることを意味しません(たとえば、Task.CompletedTaskを使用して完了したタスクを作成する場合、非同期実装はしていません)。逆に、C#の非同期実装では、非同期メソッドの戻り値の型がTaskまたはTask <T>である必要があります。別の言い方をすると、私のインターフェースの唯一の「漏れやすい」側面は、名前の非同期サフィックスです
Enrico Massone

@Neilには、実際にはすべての非同期メソッドの名前が「Async」で終わる必要があることを示す命名ガイドラインがあります。ただし、これは、非同期呼び出しを使用しないで実装できるため、TaskまたはTask <T>を返すメソッドにAsyncサフィックスを付けて名前を付ける必要があることを意味しません。
Enrico Massone

6
メソッドの「非同期性」は、それがを返すという事実によって示されると私は主張しますTask。非同期メソッドの末尾にasyncという単語を付けるためのガイドラインは、その他の点では同一のAPI呼び出しを区別することでした(C#は戻り値のタイプに基づいてディスパッチできません)。私たちの会社では、それをすべてまとめました。
リッチジラ

メソッドの非同期性が抽象化の一部である理由を説明する多数の回答とコメントがあります。より興味深い質問は、言語またはプログラミングAPIがメソッドの機能を実行方法からどのように分離し、タスクの戻り値や非同期マーカーが不要になるかです。関数型プログラミングの人々はこれをよりよく理解しているようです。非同期メソッドがF#およびその他の言語でどのように定義されているかを検討します。
フランクHileman

2
:-)->「関数型プログラミングの人々」ヘクタール。Asyncは同期よりもリークが多いわけではありません。デフォルトで同期コードを作成することに慣れているため、そのように見えます。デフォルトですべて非同期でコーディングした場合、同期関数はリークしているように見えることがあります。
StarTrekRedneck

回答:


8

もちろん、リークのある抽象化法則を適用することもできますが、すべての抽象化はリークの多いものであると考えられているため、特に興味深いことではありません。その推測に反対する人もいますが、抽象化とは何か、リークとはどういう意味かを理解していなければ役に立たないのです。したがって、まず、これらの各用語の見方を詳しく説明します。

抽象化

抽象化の私のお気に入りの定義は、Robert C. MartinのAPPPから派生しています

「抽象化とは、本質的なものを増幅し、無関係なものを排除することです。」

したがって、インターフェイス自体は抽象化ではありません。それらは、重要なことを表面にもたらし、残りを隠す場合にのみ、抽象化です。

漏れやすい

本「依存性注入の原則、パターン、および実践」では、依存性注入(DI)のコンテキストで漏出性抽象化という用語を定義しています。ポリモーフィズムとSOLIDの原則は、このコンテキストで大きな役割を果たします。

依存関係逆転の原則:(DIP)のことが、再びAPPPを引用し、次の次の

"クライアントは[...]抽象インターフェースを所有している"

これが意味することは、クライアント(コードを呼び出す)が必要とする抽象化を定義し、次にその抽象化を実装することです。

漏れやすい抽象化は、私の見解では、何らかの形でクライアントがないことを、いくつかの機能を含むことにより、DIPに違反する抽象化である必要があります

同期依存関係

ビジネスロジックを実装するクライアントは、通常、DIを使用して、一般にデータベースなどの特定の実装の詳細から切り離します。

レストラン予約のリクエストを処理するドメインオブジェクトについて考えてみましょう。

public class MaîtreD : IMaîtreD
{
    public MaîtreD(int capacity, IReservationsRepository repository)
    {
        Capacity = capacity;
        Repository = repository;
    }

    public int Capacity { get; }
    public IReservationsRepository Repository { get; }

    public int? TryAccept(Reservation reservation)
    {
        var reservations = Repository.ReadReservations(reservation.Date);
        int reservedSeats = reservations.Sum(r => r.Quantity);

        if (Capacity < reservedSeats + reservation.Quantity)
            return null;

        reservation.IsAccepted = true;
        return Repository.Create(reservation);
    }
}

ここでは、IReservationsRepository依存関係はクライアントであるクラスによってのみ決定れますMaîtreD

public interface IReservationsRepository
{
    Reservation[] ReadReservations(DateTimeOffset date);
    int Create(Reservation reservation);
}

このインターフェースは、MaîtreDクラスが非同期である必要がないため、完全に同期です。

非同期の依存関係

インターフェースを非同期に簡単に変更できます。

public interface IReservationsRepository
{
    Task<Reservation[]> ReadReservations(DateTimeOffset date);
    Task<int> Create(Reservation reservation);
}

MaîtreDクラスは、しかし、しない必要があるので、今DIPが侵害され、これらのメソッドは非同期に。実装の詳細によりクライアントが強制的に変更されるため、これは漏洩しやすい抽象化と見なします。TryAcceptこの方法は、今も、非同期になることがあります。

public async Task<int?> TryAccept(Reservation reservation)
{
    var reservations =
        await Repository.ReadReservations(reservation.Date);
    int reservedSeats = reservations.Sum(r => r.Quantity);

    if (Capacity < reservedSeats + reservation.Quantity)
        return null;

    reservation.IsAccepted = true;
    return await Repository.Create(reservation);
}

ドメインロジックが非同期であるという固有の根拠はありませんが、実装の非同期をサポートするために、これが必要になりました。

より良いオプション

NDC Sydney 2018 でこのトピックについて講演しました。その中で、リークしない代替案についても概説します。私もこの講演を2019年のいくつかの会議で行う予定ですが、今は非同期インジェクションという新しいタイトルに変更されています。

講演に伴うブログ記事も公開する予定です。これらの記事はすでに書かれていて、私の記事のキューに入れられており、発行されるのを待っているので、しばらくお待ちください。


私の考えでは、これは意図の問題です。私の抽象化が一方向に動作するように見えるが、詳細または制約が提示された抽象化を壊す場合、それは漏れやすい抽象化です。しかし、この場合は、操作が非同期であることを明示的に示しています。これは、私が抽象化しようとしていることではありません。これは、SQLデータベースがあり、接続文字列を公開しているという事実を(賢明かどうかにかかわらず)抽象化しようとしている例とは、私の心の中で明確です。多分それは意味論/パースペクティブの問題です。
Ant P

したがって、抽象化は「それ自体」では決して漏洩しないと言えます。代わりに、特定の実装の一部の詳細が公開されたメンバーからリークし、コンシューマーにその実装の変更を制限して抽象化の形状を満たす場合、それは漏洩です。 。
Enrico Massone

2
興味深いことに、説明で強調した点は、依存関係注入のストーリー全体で最も誤解されている点の1つです。開発者は、依存関係の逆転の原則を忘れて、最初に抽象化を設計しようと試み、次に抽象化自体に対処するためにコンシューマの設計を適応させることがあります。代わりに、プロセスは逆の順序で実行する必要があります。
Enrico Massone

11

それはまったく漏れやすい抽象化ではありません。

非同期であることは、関数の定義に対する根本的な変更です。これは、呼び出しが戻ったときにタスクが完了していないことを意味しますが、プログラムのフローがほとんど遅延せずにほとんどすぐに続行されることも意味します。同じタスクを実行する非同期関数と同期関数は、本質的に異なる関数です。非同期であることは実装の詳細ではありません。関数の定義の一部です。

関数が関数を非同期にする方法を公開した場合、リークが発生します。あなたは(それをする必要はありません/すべきではありません)それがどのように実装されているかを気にします。


5

asyncメソッドの属性は、特定の注意と処理が必要であることを示すタグです。そのため、世界に漏れ出る必要があります。非同期操作を適切に構成することは非常に難しいため、APIユーザーにヘッドアップを提供することが重要です。

代わりに、ライブラリがそれ自体のすべての非同期アクティビティを適切に管理asyncしている場合は、APIから「リーク」させないようにする余裕があります。

ソフトウェアの難しさには、データ、制御、空間、時間の4つの側面があります。非同期操作は4つの次元すべてに及ぶため、最も注意が必要です。


私はあなたの意見に同意しますが、「リーク」は何か悪いことを意味します。これは、「漏れやすい抽象化」という用語の意図です-抽象化では望ましくないものです。非同期と同期の場合、何も漏れていません。
StarTrekRedneck

2

漏れやすい抽象化とは、特定の実装を念頭に置いて設計された抽象化であり、一部の実装は抽象化自体を通じて「リーク」します。

結構です。抽象化は、より複雑な具体的な事柄や問題のいくつかの要素を無視する概念的なものです(事物/問題をより単純に、扱いやすくするため、またはその他の利点のために)。そのため、それは実際の問題とは必ずしも異なるため、一部のケースではリークが発生します(つまり、すべての抽象化がリークしますが、唯一の問題は、どの程度までの意味ですか?私たちにとって有用です、その適用範囲は何ですか)。

そうは言っても、ソフトウェアの抽象化に関しては、無視することを選択した詳細(時には十分かもしれません)は、私たちにとって重要なソフトウェアの一部の側面(パフォーマンス、保守性など)に影響を与えるため、実際には無視できません。 。したがって、リークの多い抽象化は、特定の詳細を無視するように設計された抽象です(可能であり、有用であるという前提の下で)が、これらの詳細の一部は実際には重要であることが判明しました(無視できないため、 "漏れ出す")。

そのため、実装の詳細を公開するインターフェース自体は漏洩しません(または、単独で表示されるインターフェースは、それ自体が漏洩の抽象化ではありません)。代わりに、リークは、インターフェースを実装するコード(インターフェースによって表される抽象化を実際にサポートできるかどうか)、およびクライアントコードによって行われる仮定(これは、インターフェースですが、それ自体をコードで表現することはできません(たとえば、言語の機能は十分に表現力がないため、ドキュメントなどで説明する場合があります)。


2

次の例を検討してください。

これは、戻る前に名前を設定するメソッドです。

public void SetName(string name)
{
    _dataLayer.SetName(name);
}

名前を設定するメソッドです。呼び出し元は、返されたタスクが完了するまで(IsCompleted= true)、名前が設定されているとは想定できません。

public Task SetName(string name)
{
    return _dataLayer.SetNameAsync(name);
}

名前を設定するメソッドです。呼び出し元は、返されたタスクが完了するまで(IsCompleted= true)、名前が設定されているとは想定できません。

public async Task SetName(string name)
{
    await _dataLayer.SetNameAsync(name);
}

Q:どちらが他の2つに属していませんか?

A:非同期メソッドは、スタンドアロンのメソッドではありません。独立しているのは、voidを返すメソッドです。

私にとって、ここでの「リーク」はasyncキーワードではありません。メソッドがタスクを返すのはこのためです。そして、それは漏れではありません。それはプロトタイプの一部であり、抽象化の一部でもあります。タスクを返す非同期メソッドは、タスクを返す同期メソッドによって行われたのとまったく同じ約束をします。

だから、いや、asyncそれ自体の導入は漏れやすい抽象化だとは思わない。しかし、インターフェイス(抽象化)を変更することで「リーク」するタスクを返すには、プロトタイプを変更する必要がある場合があります。そして、それは抽象化の一部であるため、本質的にはリークではありません。


0

これは、すべての実装クラスで非同期呼び出しを作成するつもりがない場合にのみ、リークの多い抽象化です。たとえば、サポートするデータベースタイプごとに1つなど、複数の実装を作成できます。これは、プログラム全体で使用されている正確な実装を知る必要がないと仮定すると、まったく問題ありません。

また、非同期実装を厳密に実施することはできませんが、その名前はそうである必要があることを示しています。状況が変化し、何らかの理由で同期呼び出しになる可能性がある場合は、名前の変更を検討する必要がある可能性があるため、これが可能性が高いと思われない場合にのみ、これを行うことをお勧めします。未来。


0

これは反対の視点です。

だけではなく代わりにが必要になり始めたためFoo、戻るTask<Foo>ことから戻ることはしませんでしTaskFoo。確かに、ときどき対話しますTaskが、ほとんどの実際のコードではそれを無視してを使用しFooます。

さらに、実装が非同期である場合とそうでない場合でさえ、非同期動作をサポートするインターフェースを定義することがよくあります。

実際、aを返すインターフェースTask<Foo>は、気にしてもしなくても、実際に非同期であるかどうかにかかわらず、実装が非同期である可能性があることを通知します。抽象化によって、その実装について知る必要がある以上のことがわかった場合、それは漏洩しやすいものです。

実装が非同期でない場合は、非同期に変更してから、抽象化とそれを使用するすべてのものを変更する必要があります。これは非常に漏れやすい抽象化です。

それは判断ではありません。他の人が指摘したように、すべての抽象化は漏れます。コードの最後のどこかに実際には非同期のものが存在する可能性があるため、コード全体で非同期/待機の波及効果を必要とするため、これはより大きな影響を及ぼします。

それは不満のように聞こえますか?それは私の意図ではありませんが、正確な観察だと思います。

関連する点は、「インターフェースは抽象概念ではない」という主張です。マーク・シーマンが簡潔に述べたことは少し乱用されました。

「抽象化」の定義は、.NETであっても「インターフェース」ではありません。抽象化は他の多くの形を取ることができます。インターフェースは不十分な抽象化であるか、その実装を非常に厳密にミラー化できるため、ある意味でそれは抽象化ではありません。

しかし、抽象化を作成するために絶対にインターフェースを使用します。質問がインターフェースと抽象化に言及しているので、「インターフェースは抽象化ではない」と明言することは賢明ではありません。


-2

GetAllAsync()、実際に非同期?名前に「非同期」が含まれていることを確認しますが、これは削除できます。だから私はもう一度尋ねます... Task<IEnumerable<User>>同期的に解決されるを返す関数を実装することは不可能ですか?

.NetのTaskタイプの詳細はわかりませんが、関数を同期的に実装することが不可能な場合は、(このように)リークのある抽象化であることを確認してください。私はないそれがあったことを知っている場合IObservableではなく、タスク、それは可能性が機能外何も知らないので、それはその特定の事実を漏洩していないので、同期または非同期のいずれかに実装します。


Task<T> 非同期を意味します。タスクオブジェクトをすぐに取得しますが、ユーザーのシーケンスを待つ必要がある場合があります
Caleth

それは非同期必ずしもだという意味ではありません待たなければなりません。シャル非同期意味するだろう待ちます。おそらく、基になるタスクが既に実行されている場合、待つ必要はありません
ダニエルT.
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.