web-dev-qa-db-ja.com

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

私は本を​​読んでいます Dependency Injection Principles、Practices、and Patterns と本でよく説明されているリークのある抽象化の概念について読んでいます。

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

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

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

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

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

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

あれについてどう思う ?

13
Enrico Massone

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

抽象化

私のお気に入りのabstractionsの定義は、Robert C. Martinの [〜#〜] appp [〜#〜] から派生しています:

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

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

漏れやすい

Dependency Injection Principles、Patterns、and Practicesは、依存性インジェクション(DI)のコンテキストでleaky abstractionという用語を定義しています。ポリモーフィズムと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クラスは非同期であるため、このインターフェースは完全に同期ですneed.

非同期の依存関係

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

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

ただし、MaîtreDクラスは非同期ではありませんneedこれらのメソッドは非同期であるため、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年のいくつかの会議でも行う予定ですが、今回は非同期注入という新しいタイトルに変更しました。

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

8
Mark Seemann

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

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

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

11
gnasher729

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

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

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

5
BobDalgleish

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

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

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の導入自体が漏れのある抽象化を形成するとは思いません。しかし、インターフェイス(抽象化)を変更することで「リーク」するタスクを返すには、プロトタイプを変更する必要がある場合があります。そして、それは抽象化の一部であるため、本質的にはリークではありません。

2
John Wu

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

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

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

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

2

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

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

0
Neil

これは反対の視点です。

FooだけでなくTaskが必要になったため、FooからTask<Foo>に戻ることはしませんでした。もちろん、Taskとやり取りすることもありますが、実際のコードではほとんど無視してFooを使用します。

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

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

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

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

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

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

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

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

0
Scott Hannen