Problem zależności w kodzie dotyczy na przykład sytuacji jak poniżej:
public class AudiA4 { public void Run() { //zależność var engine = new GasEngine(); engine.Run(); } }
Zasada odwrócenia zależności głosi, że:
Moduły wysokiego poziomu nie powinny zależeć od modułów niskiego poziomu. Jedne i drugie powinny być zależne od pewnych abstrakcji.
Abstrakcje same w sobie także nie powinny zależeć od szczegółów. Dzięki temu dodawanie nowych komponentów lub ich wymiana nie będzie powodować zmian w modułach wysokiego poziomu.
Sposobem na zastosowanie tej zasady jest "wzorzec" Inversion of Control. Cudzysłów bierze się stąd, że IoC definiowane jest na stosunkowo wysokim poziomie abstrakcji w porównaniu z innymi wzorcami. Możemy go zastosować do różnego rodzaju kontroli:
- flow aplikacji
- sterowanie pomiędzy systemami za pomocą wspólnego interfejsu
- tworzenie i binding zależności w programie
Flow aplikacji - przykładem na odwrócenie zależności jest zastosowanie graficznego interfejsu użytkownika. W starych aplikacjach konsolowych, program pyta użytkownika o konkretną daną, oczekuje na odpowiedź i wykonuje się dalej. Po odwróceniu zależności, użytkowik z GUI dostarcza do programu te informacje, które akurat chce dostarczyć. Następuje zatem przejście od kodu proceduralnego do programu będącego reakcjami na zdarzenia użytkownika. Wszystko zgodnie z zasadą "Don't call us, we will call you".
Interface Inversion - przyjmując, że aplikacja składa się z providerów usług i ich konsumentów, możemy zauważyć, że tradycyjnie to konsument zależy od providerów. Po odwróceniu zależności to konsument, poprzez pewien interfejs decyduje o tym, czego potrzebuje od providerów. Konsument posiada zatem referencję do pewnego interfejsu, który implementują providerzy, sam nie musi się do niczego dostosowywać.
Tworzenie i binding obiektów - polega na tym, aby tworzyć obiekty niskiego poziomu poza obiektami wysokiego poziomu.
Pewnym typem IoC jest Dependency Injection. Cały problem polega na tym, że skoro nie chcemy tworzyć obiektów niskiego poziomu w obiektach wysokiego poziomu, to jakiś mechanizm musi nam je wstrzykiwać. Mamy zatem do dyspozycji trzy podstawowe pomysły na wstrzykiwanie zależności:
- przez konstruktor - wymusza wstrzykiwanie przy tworzeniu obiektu
IEngine gasEngine = new GasEngine(); AudiA4 car = new AudiA4(gasEngine); public class AudiA4 { private readonly _engine; public AudiA4(IEngine engine) { _engine = engine; } public void Run() { _engine.Run(); } }
- przez setter - dzięki temu podejściu można korzystać z klasy przed wstrzyknięciem zależności, w C# można korzystać bezpośrednio z properties, w innych językach trzeba posiłkować się funkcjami.
AudiA4 car = new AudiA4(); car.Engine = new GasEngine(); public class AudiA4 { public IEngine Engine { get; set; } public void Run() { Engine.Run(); } }
- przez interfejs - jawnie pokazujemy, że do danej klasy wstrzykiwane będą zależności.
AudiA4 car = new AudiA4(); var engine = new GasEngine(); ((IDependsOnEngine)car).Inject(engine); public class AudiA4 : IDependsOnEngine { private IEngine _engine; public void Run() { _engine.Run(); } public void Inject(IEngine engine) { _engine = engine; } } public interface IDependsOnEngine { void Inject(IEngine engine); }
Dependency Injection jest genialnym mechanizmem, jednak warto pamiętać o kilku jego wadach, aby nie nadużywać go, gdy nie jest potrzebny. Przede wszystkim narusza zasadę enkapsulacji - silnik wyciągany jest poza obiekt samochodu. Często także zależności tworzymy wcześniej niż ich potrzebujemy, bo na przykład zmusza nas do tego konstruktor. Nadużywanie DI może także doprowadzić do sytuacji, gdy klasa implementuje kilkanaście interfejsów, co także nie jest raczej wskazane.
Brak komentarzy:
Prześlij komentarz