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 IReservationsRepository
zależność jest określana wyłącznie przez klienta, MaîtreD
klasę:
public interface IReservationsRepository
{
Reservation[] ReadReservations(DateTimeOffset date);
int Create(Reservation reservation);
}
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:
public interface IReservationsRepository
{
Task<Reservation[]> ReadReservations(DateTimeOffset date);
Task<int> Create(Reservation reservation);
}
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:
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.