Чи інтерфейс, що розкриває функцію асинхронізації, є хиткою абстракцією?


13

Я читаю книгу " Принципи, практики та схеми введення залежності", і читаю про концепцію непропускної абстракції, яка добре описана в книзі.

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

Як приклад, розглянемо наступний інтерфейс, що представляє репозиторій для користувачів додатків:

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

Відповідно до визначення книги, витікаюча абстракція - це абстракція, розроблена з урахуванням конкретної реалізації, так що деякі деталі реалізації "просочуються" через саму абстракцію.

Моє запитання таке: чи можемо ми розглядати інтерфейс, розроблений з урахуванням асинхронізації, наприклад IUserRepository, як приклад витікаючої абстракції?

Звичайно, не всі можливі реалізації мають щось спільне з асинхронією: це роблять лише реалізація процесів (таких як реалізація SQL), але сховище пам’яті не вимагає асинхронії (реально реалізація в пам'яті версії інтерфейсу, ймовірно, більше важко, якщо інтерфейс розкриває методи асинхронізації, наприклад, вам, ймовірно, доведеться повернути щось на зразок Task.CompletedTask або Task.FromResult (користувачів) у реалізаціях методу).

Що ти думаєш про це ?


@Neil Я, мабуть, зрозумів. Методи, що розкривають інтерфейс, що повертає Завдання або Завдання <T>, не є простою абстракцією сама по собі, це просто договір з підписом, що включає завдання. Метод, що повертає Task або Task <T>, не передбачає здійснення асинхронізації (наприклад, якщо я створюю виконане завдання за допомогою Task.CompletedTask, я не виконую асинхронну реалізацію). Навпаки, реалізація асинхронізації в C # вимагає, щоб тип повернення методу асинхронізації повинен бути типу Task або Task <T>. Інакше кажучи, єдиний "протікаючий" аспект мого інтерфейсу - це суфікс асинхрон в іменах
Enrico

@Neil насправді існує настанова щодо іменування, в якій зазначено, що всі методи асинхронізації повинні мати ім'я, що закінчується на "Асинхроніка". Але це не означає, що метод, що повертає завдання або завдання <T>, повинен бути названий суфіксом Async, оскільки він може бути реалізований, не використовуючи викликів async.
Енріко

6
Я б стверджував, що "асинхронність" методу вказується тим, що він повертає a Task. Вказівки щодо суфіксації методів асинхронізації зі словом async полягали в тому, щоб розрізняти інакше однакові виклики API (розсилка C # cant на основі типу повернення). У нашій компанії ми все це кинули.
richzilla

Існує низка відповідей та коментарів, що пояснюють, чому асинхронний характер методу є частиною абстракції. Більш цікавим питанням є те, як API мови чи програмування може відокремити функціональність методу від того, як він виконується, до того, коли нам більше не потрібні значення повернення Завдання чи асинхронні маркери? Люди функціонального програмування, схоже, зрозуміли це краще. Розглянемо, як визначені асинхронні методи у F # та інших мовах.
Френк Хілеман

2
:-) -> "Люди з функціональним програмуванням" га. Асинхронізація не є більш динамічною, ніж синхронна, це просто здається, тому що ми звикли писати код синхронізації за замовчуванням. Якщо ми за замовчуванням усі зашифрували асинхронізацію, синхронна функція може здатися пропускною.
StarTrekRedneck

Відповіді:


8

Можна, звичайно, посилатися на закон про проникнення абстракцій , але це не особливо цікаво, оскільки він стверджує, що всі абстракції є герметичними. Можна стверджувати , і проти цієї гіпотези, але це не допоможе , якщо ми не поділяємо розуміння того , що ми маємо в виду абстракції , і що ми маємо в виду Лики . Тому спершу спробую окреслити, як я бачу кожен із цих термінів:

Абстракції

Моє улюблене визначення абстракцій походить із додатка Роберта К. Мартіна :

"Абстракція - це посилення суттєвого та усунення нерелевантного".

Таким чином, інтерфейси самі по собі не є абстракціями . Вони є лише абстракціями, якщо винесуть на поверхню те, що має значення, а приховають решту.

Витік

Книга " Принципи, схеми та практики введення залежності" визначає термін протікання абстракції в контексті вприскування залежностей (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 році, але тепер я отримав нову назву ін'єкції Async .

Планую також опублікувати серію дописів у блозі для супроводу бесіди. Ці статті вже написані і знаходяться в черзі моїх статей, чекаючи їх публікації, тому слідкуйте за новинами.


На мій погляд, це питання наміру. Якщо моя абстракція виглядає так, ніби вона повинна вести себе в один бік, але якась деталь або обмеження порушує абстракцію, як це було представлено, це є непрохідною абстракцією. Але в цьому випадку я вам явно представляю, що операція асинхронна - це не те, що я намагаюся абстрагувати. Це на відміну від вашого прикладу, коли я (розумно чи ні) намагаюся абстрагувати той факт, що існує база даних SQL, і я все ще піддаю рядок з'єднання. Можливо, це питання семантики / перспективи.
Мураха P

Таким чином, ми можемо сказати, що абстракція ніколи не є протікаючою "сама по собі", натомість вона є протікаючою, якщо деякі деталі однієї конкретної реалізації витікають з експонованих членів і обмежують споживача змінити її виконання, щоб задовольнити форму абстракції. .
Енріко Массон

2
Цікаво, що пункт, який ви виділили у своєму поясненні, є одним із найбільш неправильно зрозумілих моментів усієї історії введення залежності. Іноді розробники забувають принцип інверсії залежності і намагаються спочатку розробити абстракцію, а потім адаптують споживчий дизайн, щоб впоратися з самою абстракцією. Натомість процес слід робити у зворотному порядку.
Енріко Массон

11

Це зовсім не витікаюча абстракція.

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

Якби функція виявила, як функцію зробили асинхронною, це було б проникливо. Ви (не / не повинні) дбаєте, як це реалізується.


5

asyncАтрибут методу є тег , який вказує на те, що особлива увага і обробка не потрібна. Як такий, йому потрібно просочитися у світ. Асинхронні операції складно скласти правильно, тому важливо надавати користувачеві API голову.

Якщо натомість ваша бібліотека належним чином керувала всіма асинхронними діями всередині себе, то ви можете дозволити собі не випускати async"протікання" з API.

Існує чотири виміри складності програмного забезпечення: дані, контроль, простір та час. Асинхронні операції охоплюють усі чотири виміри, таким чином, потребуючи найбільшої обережності.


Я погоджуюся з вашими настроями, але "протікання" передбачає щось погане, яке є наміром терміна "нещільна абстракція" - щось небажане в абстракції. У випадку асинхронізації проти синхронізації нічого не протікає.
StarTrekRedneck

2

витікаюча абстракція - це абстракція, розроблена з урахуванням конкретної реалізації, так що деякі деталі реалізації "просочуються" через саму абстракцію.

Не зовсім. Абстракція - це концептуальна річ, яка ігнорує деякі елементи більш складної конкретної речі або проблеми (щоб зробити річ / проблему простішою, простежуваною або через якусь іншу користь). Як така, вона обов'язково відрізняється від фактичної речі / проблеми, і, таким чином, вона буде протікати в деяких підгрупах випадків (тобто всі абстракції є герметичними, питання лише в якій мірі - сенсі, в яких випадках - абстракція. корисно для нас, яка сфера її застосування).

Однак, якщо мова йде про абстракції програмного забезпечення, то іноді (або, можливо, досить часто?) Деталі, які ми вирішили ігнорувати, насправді не можна ігнорувати, оскільки вони впливають на важливий для нас аспект програмного забезпечення (продуктивність, ремонтопридатність, ...) . Тож витікаюча абстракція - це абстракція була розроблена для ігнорування певних деталей (за припущенням, що це можна і корисно зробити), але потім виявилося, що деякі з цих деталей на практиці є важливими (їх не можна ігнорувати, тому вони "витекти").

Отже, інтерфейс, що розкриває деталі реалізації, не є випромінюваним сам по собі (точніше, інтерфейс, який розглядається ізольовано, сам по собі не є протікаючою абстракцією); натомість, герметичність залежить від коду, який реалізує інтерфейс (чи здатний він фактично підтримувати абстракцію, представлену інтерфейсом), а також від припущень, зроблених клієнтським кодом (що означає концептуальну абстракцію, що доповнює висловлену інтерфейс, але сам не може бути виражений у коді (наприклад, особливості мови недостатньо виразні, тому ми можемо описати це в документах тощо)).


2

Розглянемо наступні приклади:

Це метод, який встановлює ім'я перед його поверненням:

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

Це метод, який встановлює ім'я. Абонент не може вважати, що ім'я встановлено, поки повернене завдання не буде виконано ( IsCompleted= вірно):

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

Це метод, який встановлює ім'я. Абонент не може вважати, що ім'я встановлено, поки повернене завдання не буде виконано ( IsCompleted= вірно):

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

З: Хто з них не належить до двох інших?

Відповідь: Метод асинхронізації не той, який стоїть один. Той, що стоїть один, це метод, який повертає нікчемність.

Для мене "протікання" тут не є asyncключовим словом; це факт, що метод повертає завдання. І це не витік; це частина прототипу і частина абстракції. Метод асинхронізації, який повертає завдання, робить таку ж обіцянку, зроблену синхронним методом, який повертає завдання.

Так що ні, я не думаю, що впровадження asyncформує протікаючу абстракцію саме по собі. Але вам, можливо, доведеться змінити прототип, щоб повернути Завдання, яке "протікає", змінивши інтерфейс (абстракцію). А оскільки це частина абстракції, це не витік, за визначенням.


0

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

І хоча ви не можете суворо застосувати асинхронну реалізацію, назва означає, що це має бути. Якщо обставини змінюються, і це може бути синхронний дзвінок з будь-якої причини, то вам, можливо, доведеться розглянути зміну імен, тому моя порада буде робити це лише у тому випадку, якщо ви не вважаєте, що це буде дуже ймовірно майбутнє


0

Ось протилежна точка зору.

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

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

Насправді інтерфейс, який повертає a, Task<Foo>говорить вам про те, що реалізація, можливо, є асинхронною незалежно від того, справді це чи ні, навіть якщо ви можете чи не піклуєтесь про це. Якщо абстракція говорить нам більше, ніж ми повинні знати про її реалізацію, вона є прохідною.

Якщо наша реалізація не є асинхронною, ми змінюємо її на асинхронну, і тоді ми маємо змінити абстракцію і все, що її використовує, це дуже проникна абстракція.

Це не судження. Як зазначали інші, всі абстракції просочуються. Цей вплив має більший вплив, оскільки він вимагає пульсаційного ефекту асинхронізації / очікування у всьому нашому коді лише тому, що десь у кінці його може бути щось, що насправді є асинхронним.

Це звучить як скарга? Це не мій намір, але я думаю, що це точне спостереження.

Пов'язаний пункт - твердження, що "інтерфейс не є абстракцією". Те, що Марк Семан лаконічно заявив, мало зловживало.

Визначення "абстракції" не є "інтерфейсом", навіть у .NET. Абстракції можуть приймати багато інших форм. Інтерфейс може бути поганою абстракцією або він може відображати його реалізацію настільки близько, що в певному сенсі це навряд чи є абстракцією.

Але ми абсолютно використовуємо інтерфейси для створення абстракцій. Отже, викидати "інтерфейси - це не абстракції", оскільки питання згадує інтерфейси, а абстракції - не просвітлюючі.


-2

Це GetAllAsync()насправді асинхронізація? Я маю на увазі, що "асинхроніка" є в назві, але це можна видалити. Тому я запитую ще раз ... Чи неможливо реалізувати функцію, яка повертає a, Task<IEnumerable<User>>що вирішується синхронно?

Я не знаю специфіки типу .Net Task, але якщо неможливо синхронно реалізувати функцію, то переконайтеся, що це протікає абстракція (Таким чином), але в іншому випадку - ні. Я ж знаю , що якщо це було , IObservableа не завдання, він може бути реалізований або синхронно , або асинхронним так нічого поза функцією знає , і тому він не протікає цей конкретний факт.


Task<T> означає асинхронізація. Ви отримуєте об'єкт завдання негайно, але, можливо, доведеться почекати послідовності користувачів
Caleth

Може зачекати, не означає, що це обов'язково асинхронізація. Якщо чекати, це означатиме асинхронізацію. Імовірно, якщо основна задача вже виконана, вам не доведеться чекати.
Даніель Т.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.