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 ?
źródło
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.Odpowiedzi:
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 :
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:
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:
Tutaj
IReservationsRepository
zależność jest określana wyłącznie przez klienta,MaîtreD
klasę:Ten interfejs jest całkowicie synchroniczny, ponieważ
MaîtreD
klasa nie musi być asynchroniczna.Zależności asynchroniczne
Możesz łatwo zmienić interfejs na asynchroniczny:
Jednak
MaîtreD
klasa 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.TryAccept
Metoda ma teraz również stać asynchroniczny: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.
źródło
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.
źródło
async
Cechą 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.
źródło
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.).
źródło
Rozważ następujące przykłady:
Jest to metoda, która ustawia nazwę przed zwróceniem:
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):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):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
async
sł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
async
form 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.źródło
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ść.
źródło
Oto przeciwny punkt widzenia.
Nie przeszliśmy od powrotu
Foo
do powrotu,Task<Foo>
ponieważ zaczęliśmy chciećTask
zamiast tylkoFoo
. To prawda, że czasami wchodzimy w interakcję z,Task
ale w większości realnych kodów ignorujemy go i po prostu używamyFoo
.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.
źródło
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łoIObservable
raczej niż zadanie, to mogłoby być realizowane albo synchronicznie lub asynchronicznie więc nic poza funkcją wie i dlatego nie przecieka ten konkretny fakt.źródło
Task<T>
oznacza asynchronię. Otrzymujesz obiekt zadania natychmiast, ale być może będziesz musiał czekać na sekwencję użytkowników