Czy interfejs ujawniający funkcje asynchroniczne jest nieszczelną abstrakcją?

13

Czytam książkę Zasady, praktyki i wzorce wstrzykiwania zależności i czytam o koncepcji nieszczelnej abstrakcji, która jest dobrze opisana w książce.

Obecnie refaktoryzuję bazę kodu C # przy użyciu wstrzykiwania zależności, aby zamiast blokowania były używane wywołania asynchroniczne. W tym celu rozważam interfejsy, które reprezentują abstrakcje w mojej bazie kodu i które należy przeprojektować, aby można było używać wywołań asynchronicznych.

Jako przykład rozważmy następujący interfejs reprezentujący repozytorium dla użytkowników aplikacji:

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

Zgodnie z definicją książki nieszczelna abstrakcja to abstrakcja zaprojektowana z myślą o konkretnej implementacji, tak że niektóre szczegóły implementacji „wyciekają” przez samą abstrakcję.

Moje pytanie brzmi: czy możemy rozważyć interfejs zaprojektowany z myślą o asynchronizacji, taki jak IUserRepository, jako przykład przeciekającej abstrakcji?

Oczywiście nie wszystkie możliwe implementacje mają coś wspólnego z asynchronią: tylko implementacje poza procesem (takie jak implementacja SQL), ale repozytorium w pamięci nie wymaga asynchronii (w rzeczywistości implementacja wersji interfejsu w pamięci jest prawdopodobnie bardziej trudne, jeśli interfejs udostępnia metody asynchroniczne, na przykład prawdopodobnie musisz zwrócić coś takiego jak Task.CompletedTask lub Task.FromResult (użytkownicy) w implementacjach metody).

Co sądzicie o tym ?

Enrico Massone
źródło
@ Neil Prawdopodobnie mam rację. Interfejs ujawniający metody zwracające Zadanie lub Zadanie <T> nie jest sam w sobie nieszczelną abstrakcją, jest po prostu umową z podpisem zawierającym zadania. Metoda zwracająca Zadanie lub Zadanie <T> nie oznacza posiadania implementacji asynchronicznej (na przykład jeśli utworzę zakończone zadanie za pomocą Task.CompletedTask, nie wykonuję implementacji asynchronicznej). Odwrotnie, implementacja asynchroniczna w języku C # wymaga, aby zwracany typ metody asynchronicznej był typu Zadanie lub Zadanie <T>. Innymi słowy, jedynym „nieszczelnym” aspektem mojego interfejsu jest sufiks asynchroniczny w nazwach
Enrico Massone
@ Neil faktycznie istnieje wytyczna nazewnictwa, która stwierdza, że ​​wszystkie metody asynchroniczne powinny mieć nazwę kończącą się na „Async”. Nie oznacza to jednak, że metoda zwracająca Zadanie lub Zadanie <T> musi mieć nazwę z sufiksem Async, ponieważ można ją zaimplementować bez użycia wywołań asynchronicznych.
Enrico Massone
6
Argumentowałbym, że „asynchroniczność” metody wskazuje fakt, że zwraca ona wartość a Task. Wytyczne dotyczące dodawania metod asynchronicznych słowem async miały rozróżniać między innymi identyczne wywołania API (wysyłanie w C # na podstawie typu zwracanego). W naszej firmie zrzuciliśmy to wszystko razem.
richzilla
Istnieje wiele odpowiedzi i komentarzy wyjaśniających, dlaczego asynchroniczna natura metody jest częścią abstrakcji. Bardziej interesującym pytaniem jest to, w jaki sposób język lub programistyczny interfejs API może oddzielić funkcjonalność metody od sposobu jej wykonania do momentu, w którym nie potrzebujemy już wartości zwracanych przez zadanie lub znaczników asynchronicznych? Wydaje się, że osoby zajmujące się programowaniem funkcjonalnym lepiej to zrozumiały. Zastanów się, jak zdefiniowane są metody asynchroniczne w języku F # i innych językach.
Frank Hileman
2
:-) -> „Funkcjonalni ludzie programowania” ha. Asynchronizacja nie jest bardziej nieszczelna niż synchroniczna, wydaje się, że tak jest, ponieważ jesteśmy przyzwyczajeni do pisania kodu synchronizacji. Jeśli wszyscy domyślnie kodujemy asynchronicznie, funkcja synchroniczna może wydawać się nieszczelna.
StarTrekRedneck

Odpowiedzi:

8

Można oczywiście powołać się na prawo nieszczelnych abstrakcji , ale nie jest to szczególnie interesujące, ponieważ zakłada, że ​​wszystkie abstrakcje są nieszczelne. Można argumentować za i przeciw tej hipotezie, ale to nie pomaga, jeśli nie podzielamy zrozumienia, co rozumiemy przez abstrakcję i co rozumiemy przez nieszczelność . Dlatego najpierw postaram się nakreślić, w jaki sposób widzę każdy z tych terminów:

Abstrakcje

Moja ulubiona definicja abstrakcji pochodzi z APPP Roberta C. Martina :

„Abstrakcja jest wzmocnieniem tego, co istotne, i wyeliminowaniem tego, co nieistotne”.

Zatem interfejsy same w sobie nie są abstrakcjami . Są tylko abstrakcjami, jeśli wydobędą na powierzchnię to, co ważne, a resztę ukryje.

Nieszczelny

Książka Zasady, wzorce i praktyki wstrzykiwania zależności definiuje pojęcie nieszczelnej abstrakcji w kontekście wstrzykiwania zależności (DI). W tym kontekście dużą rolę odgrywają polimorfizm i zasady SOLID.

Z zasady inwersji zależności (DIP) wynika, ponownie cytując APPP, że:

„klienci [...] posiadają abstrakcyjne interfejsy”

Oznacza to, że klienci (kod wywołujący) definiują abstrakcje, których potrzebują, a następnie przechodzisz i implementujesz tę abstrakcję.

Nieszczelny abstrakcja , moim zdaniem, jest abstrakcją, że narusza DIP przez niektóre funkcje jakoś w tym, że klient nie potrzeba .

Zależności synchroniczne

Klient, który implementuje logikę biznesową, zwykle używa DI do oddzielenia się od niektórych szczegółów implementacji, takich jak zwykle bazy danych.

Rozważ obiekt domeny, który obsługuje żądanie rezerwacji restauracji:

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);
    }
}

Tutaj IReservationsRepositoryzależność jest określana wyłącznie przez klienta, MaîtreDklasę:

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

Ten interfejs jest całkowicie synchroniczny, ponieważ MaîtreDklasa nie musi być asynchroniczna.

Zależności asynchroniczne

Możesz łatwo zmienić interfejs na asynchroniczny:

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

Jednak MaîtreDklasa nie potrzebuje tych metod, aby były asynchroniczne, więc teraz DIP jest naruszony. Uważam to za nieszczelną abstrakcję, ponieważ szczegół implementacji zmusza klienta do zmiany. TryAcceptMetoda ma teraz również stać asynchroniczny:

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);
}

Nie ma nieodłącznego uzasadnienia, aby logika domeny była asynchroniczna, ale aby wesprzeć asynchronię implementacji, jest to teraz wymagane.

Lepsze opcje

Na NDC Sydney 2018 wygłosiłem wykład na ten temat . W nim również zarysowuję alternatywę, która nie wycieka. Będę mówił o tym na kilku konferencjach w 2019 r., Ale teraz przemianowałem go na nowy tytuł zastrzyku Async .

Planuję również opublikować serię postów na blogu towarzyszących rozmowie. Artykuły te są już napisane i siedzą w mojej kolejce artykułów, czekając na publikację, więc bądźcie czujni.

Mark Seemann
źródło
Moim zdaniem jest to kwestia intencji. Jeśli moja abstrakcja wygląda tak, jakby zachowywała się w jedną stronę, ale jakiś szczegół lub ograniczenie psuje abstrakcję w przedstawionej formie, jest to abstrakcja nieszczelna. Ale w tym przypadku wyraźnie przedstawiam wam, że operacja jest asynchroniczna - nie to próbuję streścić. W moim umyśle różni się to od twojego przykładu, w którym (mądrze lub nie) próbuję oderwać się od faktu, że istnieje baza danych SQL i nadal ujawniam ciąg połączenia. Może to kwestia semantyki / perspektywy.
Ant P
Możemy więc powiedzieć, że abstrakcja nigdy nie jest nieszczelna „per se”, lecz jest nieszczelna, jeśli niektóre szczegóły jednej konkretnej implementacji wyciekają z odsłoniętych elementów i zmuszają konsumenta do zmiany jej implementacji, aby spełnić kształt abstrakcji .
Enrico Massone
2
Co ciekawe, punkt, który podkreśliłeś w wyjaśnieniu, jest jednym z najbardziej niezrozumiałych punktów całej historii wstrzykiwania zależności. Czasami programiści zapominają o zasadzie inwersji zależności i najpierw próbują zaprojektować abstrakcję, a następnie dostosowują projekt konsumencki, aby poradzić sobie z samą abstrakcją. Zamiast tego proces należy wykonać w odwrotnej kolejności.
Enrico Massone
11

To wcale nie jest nieszczelna abstrakcja.

Bycie asynchronicznym jest fundamentalną zmianą w definicji funkcji - oznacza to, że zadanie nie jest ukończone po powrocie wywołania, ale oznacza również, że przepływ programu będzie kontynuowany prawie natychmiast, bez długiego opóźnienia. Funkcja asynchroniczna i synchroniczna wykonująca to samo zadanie to zasadniczo różne funkcje. Bycie asynchronicznym nie jest szczegółem implementacji. Jest to część definicji funkcji.

Jeśli funkcja ujawni, w jaki sposób została asynchroniczna, byłoby to nieszczelne. Nie musisz / nie musisz dbać o to, jak to jest realizowane.

gnasher729
źródło
5

asyncCechą metody jest tag, który wskazuje, że wymagana jest szczególna ostrożność i manipulacja. Jako taki musi przedostać się na świat. Operacje asynchroniczne są niezwykle trudne do prawidłowego skomponowania, dlatego ważne jest, aby dać użytkownikowi interfejsu API informacje na ten temat.

Jeśli zamiast tego Twoja biblioteka właściwie zarządza wszystkimi działaniami asynchronicznymi w sobie, możesz pozwolić sobie na async„wyciek” z interfejsu API.

Istnieją cztery wymiary trudności w oprogramowaniu: dane, kontrola, przestrzeń i czas. Operacje asynchroniczne obejmują wszystkie cztery wymiary, dlatego wymagają największej ostrożności.

BobDalgleish
źródło
Zgadzam się z twoim sentymentem, ale „wyciek” oznacza coś złego, co jest intencją terminu „przeciekająca abstrakcja” - coś niepożądanego w abstrakcji. W przypadku asynchronizacji vs synchronizacji nic nie wycieka.
StarTrekRedneck
2

nieszczelna abstrakcja to abstrakcja zaprojektowana z myślą o konkretnej implementacji, tak że niektóre szczegóły implementacji „wyciekają” przez samą abstrakcję.

Nie do końca. Abstrakcja jest konceptualną rzeczą, która ignoruje niektóre elementy bardziej skomplikowanej konkretnej rzeczy lub problemu (aby uprościć rzecz / problem, z korzyścią lub z innych korzyści). Jako taki, niekoniecznie różni się od rzeczywistej rzeczy / problemu, a zatem będzie nieszczelny w niektórych podzbiorach przypadków (tj. Wszystkie abstrakcje są nieszczelne, jedynym pytaniem jest, w jakim stopniu - czyli w jakich przypadkach jest abstrakcja przydatne dla nas, jaka jest jego dziedzina zastosowania).

To powiedziawszy, jeśli chodzi o abstrakty oprogramowania, czasami (a może dość często?) Szczegóły, które zdecydowaliśmy się zignorować, nie mogą być faktycznie zignorowane, ponieważ wpływają na pewien aspekt oprogramowania, który jest dla nas ważny (wydajność, łatwość konserwacji, ...) . Tak więc nieszczelna abstrakcja to abstrakcja zaprojektowana, aby zignorować pewne szczegóły (przy założeniu, że było to możliwe i przydatne), ale potem okazało się, że niektóre z tych szczegółów są istotne w praktyce (nie można ich zignorować, więc "wyciek").

Tak więc, interfejs wystawienie szczegół wykonania nie jest nieszczelny per se (lub raczej interfejs, patrząc w izolacji, nie jest sama w sobie jest nieszczelny abstrakcji); zamiast tego nieszczelność zależy od kodu implementującego interfejs (czy jest on w stanie faktycznie obsługiwać abstrakcję reprezentowaną przez interfejs), a także od założeń poczynionych przez kod klienta (co stanowi abstrakcyjną koncepcję, która uzupełnia tę wyrażoną przez interfejs, ale sam nie może być wyrażony w kodzie (np. cechy języka nie są wystarczająco ekspresyjne, więc możemy to opisać w dokumentach itp.).

Filip Milovanović
źródło
2

Rozważ następujące przykłady:

Jest to metoda, która ustawia nazwę przed zwróceniem:

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

Jest to metoda, która ustawia nazwę. Dzwoniący nie może założyć, że nazwa jest ustawiona, dopóki zwrócone zadanie nie zostanie zakończone ( IsCompleted= prawda):

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

Jest to metoda, która ustawia nazwę. Dzwoniący nie może założyć, że nazwa jest ustawiona, dopóki zwrócone zadanie nie zostanie zakończone ( IsCompleted= prawda):

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

P: Który nie należy do pozostałych dwóch?

Odp .: Metoda asynchroniczna nie jest tą, która działa samodzielnie. Tym, który stoi sam, jest metoda, która zwraca pustkę.

Dla mnie „przeciek” tutaj nie jest asyncsłowem kluczowym; jest to fakt, że metoda zwraca zadanie. I to nie jest wyciek; to część prototypu i część abstrakcji. Metoda asynchroniczna zwracająca zadanie składa dokładnie taką samą obietnicę złożoną przez metodę synchroniczną, która zwraca zadanie.

Więc nie, nie sądzę, aby wprowadzenie asyncform samo w sobie stanowiło nieszczelną abstrakcję. Może być jednak konieczna zmiana prototypu w celu zwrócenia zadania, które „przecieka” poprzez zmianę interfejsu (abstrakcji). A ponieważ jest to część abstrakcji, z definicji nie jest to wyciek.

John Wu
źródło
0

Jest to nieszczelna abstrakcja tylko wtedy, gdy wszystkie klasy implementujące nie mają zamiaru tworzyć wywołania asynchronicznego. Możesz utworzyć wiele implementacji, na przykład jedną dla każdego obsługiwanego typu bazy danych, i byłoby to całkowicie w porządku, zakładając, że nie musisz nigdy znać dokładnej implementacji używanej w twoim programie.

I chociaż nie można ściśle wymusić implementacji asynchronicznej, nazwa wskazuje, że powinna być. Jeśli okoliczności się zmienią i może to być zsynchronizowane połączenie z jakiegokolwiek powodu, może być konieczne rozważenie zmiany nazwisk, więc radzę zrobić to tylko, jeśli nie uważasz, że będzie to bardzo prawdopodobne w przyszłość.

Neil
źródło
0

Oto przeciwny punkt widzenia.

Nie przeszliśmy od powrotu Foodo powrotu, Task<Foo>ponieważ zaczęliśmy chcieć Taskzamiast tylko Foo. To prawda, że ​​czasami wchodzimy w interakcję z, Taskale w większości realnych kodów ignorujemy go i po prostu używamy Foo.

Co więcej, często definiujemy interfejsy do obsługi zachowania asynchronicznego, nawet jeśli implementacja może być asynchroniczna lub nie.

W efekcie interfejs, który zwraca a, Task<Foo>mówi, że implementacja jest prawdopodobnie asynchroniczna, niezależnie od tego, czy tak naprawdę jest, nawet jeśli nie jest to ważne. Jeśli abstrakcja mówi nam więcej niż powinniśmy wiedzieć o jej implementacji, jest nieszczelna.

Jeśli nasza implementacja nie jest asynchroniczna, zmieniamy ją na asynchroniczną, a następnie musimy zmienić abstrakcję i wszystko, co z niej korzysta, to bardzo nieszczelna abstrakcja.

To nie jest osąd. Jak zauważyli inni, wszystkie abstrakcje przeciekają. Ten ma większy wpływ, ponieważ wymaga efektu asynchronicznego / asynchronicznego w całym naszym kodzie, tylko dlatego, że gdzieś na jego końcu może znajdować się coś, co jest rzeczywiście asynchroniczne.

Czy to brzmi jak skarga? To nie jest moja intencja, ale myślę, że jest to dokładna obserwacja.

Powiązanym punktem jest twierdzenie, że „interfejs nie jest abstrakcją”. To, co zwięźle stwierdził Mark Seeman, zostało trochę nadużyte.

Definicja „abstrakcji” nie jest „interfejsem”, nawet w .NET. Abstrakcje mogą przybierać wiele innych form. Interfejs może być słabą abstrakcją lub może odzwierciedlać jego implementację tak ściśle, że w pewnym sensie nie jest wcale abstrakcją.

Ale absolutnie używamy interfejsów do tworzenia abstrakcji. Wyrzucenie „interfejsy nie są abstrakcjami”, ponieważ pytanie wspomina o interfejsach, a abstrakcje nie są pouczające.

Scott Hannen
źródło
-2

Czy GetAllAsync()faktycznie jest asynchroniczny? Mam na myśli, że nazwa „asynchroniczna” jest w nazwie, ale można ją usunąć. Więc pytam jeszcze raz ... Czy nie jest możliwe zaimplementowanie funkcji zwracającej rozwiązanie, Task<IEnumerable<User>>które jest rozwiązywane synchronicznie?

Nie znam specyfiki typu .Net Task, ale jeśli nie można zaimplementować funkcji synchronicznie, to upewnij się, że jest to nieszczelna abstrakcja (w ten sposób), ale inaczej nie. I nie wiem, że gdyby to było IObservableraczej niż zadanie, to mogłoby być realizowane albo synchronicznie lub asynchronicznie więc nic poza funkcją wie i dlatego nie przecieka ten konkretny fakt.

Daniel T.
źródło
Task<T> oznacza asynchronię. Otrzymujesz obiekt zadania natychmiast, ale być może będziesz musiał czekać na sekwencję użytkowników
Caleth
Może trzeba czekać, co nie oznacza, że ​​jest to asynchroniczne. Będą czekać oznaczałoby asynchroniczny. Prawdopodobnie, jeśli podstawowe zadanie zostało już uruchomione, nie musisz czekać.
Daniel T.