sobota, 7 grudnia 2019

Dependency Injection - sposoby wstrzykiwania zależności

Dependency injection - po polsku wstrzykiwanie zależności. DI jest wzorcem projektowym opisującym sposoby przekazania zależności do obiektów.
Tradycyjne podejście tworzenia oprogramowania nie zakłada przekazywania zależności za pomocą DI, ale tworzenie obiektów gdy są potrzebne. Najczęściej podczas nauki programowania rozpoczynamy pisanie projektów tworząc obiekty, kiedy chcemy z nich skorzystać. DI to wzorzec, który ma na celu wyeliminowanie tworzenia obiektów ad-hoc do fabryk czy specjalistycznych kontenerów DI.

Ten post ma na celu omówienie sposobów realizacji wstrzykiwania zależności.

Wstrzykiwanie przez konstruktor (constructor injection)

Jest to najczęściej wykorzystywana metoda przekazywania zależności do klasy. Całość opiera się o zastosowanie jednego konstruktora, który przyjmuje wszystkie zależności klasy. Przykład:

    public class Mapper
    {
        private readonly IAddressMapper _addressMapper;
        private readonly IPhoneMapper _phoneMapper;
        private readonly IEmailMapper _emailMapper;

        public Mapper(IAddressMapper addressMapper, IPhoneMapper phoneMapper, IEmailMapper emailMapper)
        {
            _addressMapper = addressMapper ?? throw new ArgumentNullException(nameof(addressMapper));
            _phoneMapper = phoneMapper ?? throw new ArgumentNullException(nameof(phoneMapper));
            _emailMapper = emailMapper ?? throw new ArgumentNullException(nameof(emailMapper));
        }
    }

Kod możemy podzielić na sekcje:
  • prywatne pola, przechowujące referencje do implementacji interfejsów (należy podkreślić, że klasa Mapper nie zna szczegółów implementacji wstrzykiwanych zależności). Pola dodatkowo oznaczone są jako readonly co zapewnia nas, że raz zainicjowane nie zostaną w trakcie wykonywania programu zmienione. 
  • publiczny konstruktor, który przypisuje referencję do zależności w celu ich późniejszego wykorzystania
  • Guard Clause - inaczej znaną także jako null check validation - sprawdzamy, czy wstrzykiwane zależności nie są wartościami null
Zalety i wady tego sposobu wstrzykiwania zależności:
+ dokumentacja klasy - widząc jakie zależności są wstrzykiwane, wiemy czego możemy się po danej klasie spodziewać
+ sprawdza się zarówno z kontenerami DI jak i bez nich
+ stan pól (zależności) klasy jest zawsze prawidłowy po utworzeniu obiektu (zależności nie są nullami)
+ podwójna ochrona: kompilator pilnuje aby wszystkie zależności zostały podane podczas tworzenia klasy, Guard Clause sprawdza aby nie były nullami
- jeżeli klasa wymaga zbyt dużej ilości zależności należy rozważyć czy kodu nie należy przeorganizować. Oczywiście nie ma górnego ani dolnego limitu ilości zależności - wszystko zależy od konkretnego przypadku
- jeżeli klasa ma być serializowana, będzie potrzebny domyślny konstruktor
- nie każda metoda wymaga wszystkich zależności - w takim wypadku należy zastanowić się czy aby na pewno architektura klasy jest prawidłowa i czy część kodu nie powinna zostać wydzielona do osobnej klasy/biblioteki

Mimo wszystkich zalet i wad jest to najczęściej polecana technika wstrzykiwania zależności i najłatwiejsza do implementacji za pomocą dostępnych kontenerów. 

Wstrzykiwanie przez właściwość (property injection)

Drugi sposób wstrzykiwania zależności to wstrzykiwanie przez właściwość. Inne nazwy, które można spotkać to: property injection, setter injection. Sposób ten zalecany jest gdy istnieje domyślna implementacja, czyli inaczej mówiąc zależność jest opcjonalna. Jeżeli nie istnieje dobra implementacja domyślna, zalecane jest wykorzystanie wstrzykiwania przez konstruktor. 
Zobaczmy na przykład:

    public class Report
    {
        private IFormat reportFormatter;
        public IFormat ReportFormatter
        {
            get
            { return reportFormatter ?? (ReportFormatter = new TxtFormatter()); }
            set
            {
                if (value == null) throw new ArgumentNullException(nameof(value));
                if (reportFormatter != null) throw new InvalidOperationException();
                reportFormatter = value;
            }
        }

        public void CreateReport()
        {
            //some code that generates report data
            var reportData = new ReportData { Name = "Average salary in company", CreateDate = DateTime.Now, NumberOfEmployee = 1000, AverageSalary = 5000 };
            var report = ReportFormatter.Convert(reportData);
            //rest of code, for example saving to file
        }
    }

    public interface IFormat
    {
        string Convert(ReportData reportData);
    }

    public class TxtFormatter : IFormat
    {
        public string Convert(ReportData reportData)
        {
            var reportTextBuilder = new StringBuilder();
            reportTextBuilder.AppendLine($"Report name: {reportData.Name}");
            reportTextBuilder.AppendLine($"Create date: {reportData.CreateDate}");
            reportTextBuilder.AppendLine($"Number of employees in company: {reportData.NumberOfEmployee}");
            reportTextBuilder.AppendLine($"Average salary in company: {reportData.AverageSalary}");
            return reportTextBuilder.ToString();
        }
    }

    public class ReportData
    {
        public string Name { get; set; }
        public DateTime CreateDate { get; set; }
        public int NumberOfEmployee { get; set; }
        public decimal AverageSalary { get; set; }
    }

Powyżej mamy do czynienia z klasą tworzącą raport. Raport chcemy przed zapisem skonwertować do zadanego formatu. Domyślną implementacją jest zwykły tekst. Property przyjmujące zależność formatującą dane raportu możemy podzielić na następujące części:
  • getter - jeżeli formatter istnieje - zwraca go, w przeciwnym razie podstawia domyślną implementację
  • setter:
    • sprawdzamy czy przesłana wartość nie jest nullem. Podobnie jak w przypadku wstrzykiwania przez konstruktor chcemy uchronić się przed złym stanem obiektu.
    • sprawdzamy czy już wcześniej zależność nie została zainicjowana. Nie chcemy pozwolić aby podczas pracy z klasą w różnych miejscach była inicjowana w różny sposób co mogłoby skutkować niestabilnym czy też niezrozumiałym zachowaniem aplikacji
    • jeżeli powyższe warunki nie mają miejsca zwracana jest zainicjowana zależność

Zalety i wady wstrzykiwania przez właściwości:
+ zależność może zostać zmieniona podczas działania programu
+ elastyczność
- obiekt może mieć nieprawidłowy stan
- większy narzut kodu aby kod był odporny na błędy programistyczne 
- mniej intuicyjne

Wstrzykiwanie przez metodę

Kiedy zależność zmienia się dla każdego wywołania metody, możemy ją przesłać jako argument wywołania metody. 
W poprzednich typach wstrzykiwania zależności (przez konstruktor, przez właściwość) była ona dostarczana podczas budowania drzewa zależności. Wstrzykiwanie przez metodę pozwala dostarczyć zależność do już istniejącego obiektu.
Technika ta szczególnie może być przydatna podczas implementacji encji (Entity) w podejściu DDD (Domain-driven design). 
Spójrzmy na przykład:

    public class Engine
    {
        public int Accelerate(int fuelValue, ITurboEngine turboEngine)
        {
            if (turboEngine == null)
            {
                throw new ArgumentNullException(nameof(turboEngine));
            }

            return turboEngine.IsTurboEngine() ? (int) (value * 1.2) : value;
        }
    }

Klasa Engine posiada metodę Accelerate która na podstawie wartości wtryskiwanego paliwa zwraca o ile samochód przyśpieszy (oczywiście w realnym świecie obliczenie wartości przyspieszenia jest znacznie bardziej skomplikowane, tutaj tylko prosty przykład :)). Interfejs ITurboEngine posiada metodę determinującą czy silnik posiada turbodoładowanie. Jeżeli mamy do czynienia z silnikiem turbodoładowanym osiągnięte przyśpieszenie będzie większe. 
Powyższy przykład pokazuje w jaki sposób możemy wstrzyknąć zależność poprzez metodę. Najczęściej będziemy mieli do czynienia z konstrukcją w której przekażemy konkretną wartość dla metody oraz serwis - czyli kontekst wykonania (zależność). 

Zalety i wady tej techniki:
+ pozwala wstrzyknąć zależność w odniesieniu do kontekstu wywołania metody
+ zależności można wstrzykiwać poza Composition Root
- małe spektrum zastosowań
- zależność staje się publiczną częścią API klasy

Brak komentarzy:

Prześlij komentarz