Является ли интерфейс, предоставляющий асинхронные функции, утечкой абстракции?

13

Я читаю книгу « Принципы, практики и модели внедрения зависимостей» и читаю концепцию утечки абстракций, которая хорошо описана в книге.

В эти дни я делаю рефакторинг базы кода C #, используя внедрение зависимостей, так что вместо блокирующих используются асинхронные вызовы. Поступая так, я рассматриваю некоторые интерфейсы, которые представляют абстракции в моей базе кода и которые должны быть переработаны, чтобы можно было использовать асинхронные вызовы.

В качестве примера рассмотрим следующий интерфейс, представляющий хранилище для пользователей приложения:

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

Согласно определению книги, утечка-абстракция - это абстракция, разработанная с учетом конкретной реализации, так что некоторые детали реализации «просачиваются» через саму абстракцию.

Мой вопрос заключается в следующем: можем ли мы рассматривать интерфейс, разработанный с учетом асинхронности, такой как IUserRepository, в качестве примера Leaky Abstraction?

Конечно, не все возможные реализации имеют какое-то отношение к асинхронности: только реализации вне процесса (например, реализация SQL) делают, но репозиторий в памяти не требует асинхронности (фактически реализация версии интерфейса в памяти, вероятно, более трудно, если интерфейс предоставляет асинхронные методы, например, вам, вероятно, придется возвращать что-то вроде Task.CompletedTask или Task.FromResult (users) в реализациях метода).

Что ты об этом думаешь ?

Энрико Массоне
источник
@ Нил Я, наверное, понял. Интерфейс, представляющий методы, возвращающие Task или Task <T>, сам по себе не является утечкой абстракции, это просто контракт с подписью, включающей задачи. Метод, возвращающий Task или Task <T>, не подразумевает асинхронную реализацию (например, если я создаю завершенную задачу с помощью Task.CompletedTask, я не делаю асинхронную реализацию). И наоборот, асинхронная реализация в C # требует, чтобы тип возвращаемого значения асинхронного метода был типа Task или Task <T>. Другими словами, единственным «утечочным» аспектом моего интерфейса является асинхронный суффикс в именах
Энрико
@Neil на самом деле есть руководство по именованию, которое гласит, что все асинхронные методы должны иметь имя, оканчивающееся на «Async». Но это не означает, что метод, возвращающий Task или Task <T>, должен иметь имя с суффиксом Async, поскольку его можно реализовать без использования асинхронных вызовов.
Энрико
6
Я бы сказал, что «асинхронность» метода указывается тем фактом, что он возвращает a Task. Принципы суффиксации асинхронных методов к слову асинхронные заключались в том, чтобы различать идентичные вызовы API, в противном случае (отправка C # не может быть основана на типе возвращаемого значения). В нашей компании мы бросили все это вместе.
Ричзилла
Существует ряд ответов и комментариев, объясняющих, почему асинхронная природа метода является частью абстракции. Более интересный вопрос заключается в том, как язык или API программирования могут отделить функциональность метода от способа его выполнения до такой степени, что нам больше не нужны возвращаемые значения Task или асинхронные маркеры? Люди из функционального программирования, кажется, поняли это лучше. Рассмотрим, как асинхронные методы определены в F # и других языках.
Фрэнк Хилман
2
:-) -> У "людей функционального программирования" ха. Async не более негерметичный, чем синхронный, просто так кажется, потому что мы привыкли писать код синхронизации по умолчанию. Если мы все по умолчанию закодируем async, синхронная функция может показаться утечкой.
StarTrekRedneck

Ответы:

8

Можно, конечно, ссылаться на закон об вытекающих абстракциях , но это не особенно интересно, поскольку в нем утверждается, что все абстракции являются вытекающими. Можно спорить за и против этой гипотезы, но это не поможет, если мы не разделяем понимание того, что мы подразумеваем под абстракцией , и что мы подразумеваем под утечкой . Поэтому сначала я попытаюсь описать, как я рассматриваю каждое из этих терминов:

Абстракции

Мое любимое определение абстракций взято из APPP Роберта Мартина :

«Абстракция - это усиление существенного и устранение несущественного».

Таким образом, интерфейсы сами по себе не являются абстракциями . Они только абстракции, если они выводят на поверхность то, что имеет значение, и скрывают все остальное.

неплотный

В книге « Принципы, модели и практики внедрения зависимостей» термин « вытекающая абстракция» используется в контексте внедрения зависимостей (DI). Полиморфизм и принципы SOLID играют большую роль в этом контексте.

Из принципа инверсии зависимостей (DIP) следует, снова цитируя APPP, что:

«клиенты [...] владеют абстрактными интерфейсами»

Это означает, что клиенты (вызывающий код) определяют требуемые им абстракции, а затем вы переходите к реализации этой абстракции.

Вытекающей абстракции , на мой взгляд, это абстракция , которая нарушает ДИП по каким - то образом , включая некоторые функции , которые клиент не нужно .

Синхронные зависимости

Клиент, который реализует часть бизнес-логики, обычно использует 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 году, но теперь переименован в новый заголовок Async инъекция .

Я планирую также опубликовать серию постов в блоге, чтобы сопровождать доклад. Эти статьи уже написаны и находятся в моей очереди, ожидая публикации, так что следите за обновлениями.

Марк Симанн
источник
На мой взгляд, это вопрос намерений. Если моя абстракция выглядит так, как будто она должна вести себя в одном направлении, но какая-то деталь или ограничение нарушают представленную абстракцию, то это утечка абстракции. Но в этом случае я явно представляю вам, что операция асинхронная - это не то, что я пытаюсь абстрагировать. Это отличается от вашего примера, где я (мудро или нет) пытаюсь абстрагироваться от факта, что есть база данных SQL, и я все еще выставляю строку подключения. Может быть, это вопрос семантики / перспективы.
Муравей P
Таким образом, мы можем сказать, что абстракция никогда не является утечкой "per se", вместо этого она является утечкой, если некоторые детали одной конкретной реализации просачиваются из открытых элементов и вынуждают потребителя изменить свою реализацию, чтобы удовлетворить форму абстракции. ,
Энрико
2
Интересно, что тот момент, который вы подчеркнули в своем объяснении, является одним из наиболее неправильно понятых моментов всей истории внедрения зависимостей. Иногда разработчики забывают принцип инверсии зависимостей и пытаются сначала спроектировать абстракцию, а затем адаптируют потребительский дизайн, чтобы справиться с самой абстракцией. Вместо этого процесс должен быть выполнен в обратном порядке.
Энрико
11

Это не дырявая абстракция вообще.

Быть асинхронным - это фундаментальное изменение в определении функции - это означает, что задача не завершена, когда вызов возвращается, но это также означает, что выполнение вашей программы будет продолжаться почти сразу, не с большой задержкой. Асинхронная и синхронная функции, выполняющие одну и ту же задачу, являются существенно разными функциями. Быть асинхронным не является деталью реализации. Это часть определения функции.

Если функция показывает, как она была сделана асинхронной, это будет утечка. Вы (не / не должны) заботиться о том, как это реализовано.

gnasher729
источник
5

asyncАтрибут метода является тег , который указывает на то, что особое внимание и обработка не требуется. Как таковой, он должен просочиться в мир. Асинхронные операции чрезвычайно сложно правильно составить, поэтому важно дать пользователю API преимущество.

Если вместо этого ваша библиотека должным образом управляет всей асинхронной активностью внутри себя, то вы можете позволить себе не async«вытекать» из API.

В программном обеспечении есть четыре аспекта сложности: данные, управление, пространство и время. Асинхронные операции охватывают все четыре измерения, поэтому требуют наибольшего внимания.

BobDalgleish
источник
Я согласен с вашим мнением, но «утечка» подразумевает что-то плохое, что является целью термина «утечка абстракции» - что-то нежелательное в абстракции. В случае async vs sync ничего не протекает.
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: Какой из них не принадлежит двум другим?

A: Асинхронный метод не тот, который стоит один. Тот, который стоит один - это метод, который возвращает void.

Для меня «утечка» здесь не является asyncключевым словом; это тот факт, что метод возвращает задачу. И это не утечка; это часть прототипа и часть абстракции. Асинхронный метод, который возвращает задачу, дает то же обещание, что и синхронный метод, который возвращает задачу.

Так что нет, я не думаю, что введение asyncформ само по себе является утечкой абстракции. Но вам, возможно, придется изменить прототип, чтобы он возвращал задачу, которая «просачивается» при изменении интерфейса (абстракция). И поскольку это часть абстракции, по определению это не утечка.

Джон Ву
источник
0

Это неплотная абстракция тогда и только тогда, когда вы не собираетесь создавать все реализующие классы для создания асинхронного вызова. Вы можете создать несколько реализаций, например, по одной для каждого типа базы данных, который вы поддерживаете, и это было бы совершенно нормально, если бы вам никогда не нужно было знать точную реализацию, используемую в вашей программе.

И хотя вы не можете строго применять асинхронную реализацию, само название подразумевает, что так и должно быть. Если обстоятельства меняются, и это может быть синхронный вызов по любой причине, тогда вам вполне может потребоваться рассмотреть вопрос об изменении имен, поэтому я советую сделать это только в том случае, если вы не думаете, что это будет очень вероятно в будущее.

Нил
источник
0

Вот противоположная точка зрения.

Мы не пошли от возвращения Fooк возвращению, Task<Foo>потому что мы начали хотеть, Taskа не просто Foo. Конечно, иногда мы взаимодействуем с, Taskно в большинстве реальных программ мы игнорируем это и просто используем Foo.

Более того, мы часто определяем интерфейсы для поддержки асинхронного поведения, даже когда реализация может быть или не быть асинхронной.

В сущности, интерфейс, который возвращает a, Task<Foo>говорит вам, что реализация может быть асинхронной, независимо от того, действительно ли она есть, даже если вам это может или не нужно. Если абстракция говорит нам больше, чем мы должны знать о ее реализации, это утечка.

Если наша реализация не асинхронная, мы изменяем ее на асинхронную, а затем мы должны изменить абстракцию и все, что ее использует, это очень утечка абстракции.

Это не суждение. Как уже отмечали другие, все абстракции протекают. Этот действительно имеет большее влияние, потому что он требует волнового эффекта асинхронного ожидания в нашем коде только потому, что где-то в его конце может быть что-то, что на самом деле является асинхронным.

Это похоже на жалобу? Это не мое намерение, но я думаю, что это точное наблюдение.

Связанным моментом является утверждение, что «интерфейс не является абстракцией». То, что кратко изложил Марк Симан, было немного оскорблено.

Определение «абстракция» не является «интерфейсом», даже в .NET. Абстракции могут принимать множество других форм. Интерфейс может быть плохой абстракцией или настолько близко отражать его реализацию, что в некотором смысле это вообще не абстракция.

Но мы абсолютно используем интерфейсы для создания абстракций. Таким образом, отбросить «интерфейсы не абстракции», потому что вопрос упоминает интерфейсы и абстракции не является просветляющим.

Скотт Ханнен
источник
-2

Это на GetAllAsync()самом деле асинхронный? Я имею в виду, что в названии есть «async», но это можно удалить. Поэтому я спрашиваю еще раз ... Разве невозможно реализовать функцию, которая возвращает а, Task<IEnumerable<User>>которая разрешается синхронно?

Я не знаю специфики типа .Net Task, но если невозможно реализовать функцию синхронно, то уверен, что это дырявая абстракция (таким образом), но в противном случае нет. Я же знаю , что если это было , IObservableа не задачи, он может быть реализован либо синхронно , либо асинхронным так ничего вне функции знает , и поэтому он не протекает этот конкретный факт.

Даниэль Т.
источник
Task<T> означает асинхронный. Вы сразу получаете объект задачи, но, возможно, придется ждать
череду
Возможно, придется ждать, не означает, что это обязательно асинхронно. Подождать будет означать асинхронность. Предположительно, если основная задача уже выполнена, вам не нужно ждать.
Даниэль Т.