Skip to content

Blog Emi

O grach z kobiecej perspektywy

  • GameDev
  • Programowanie
  • Gry
  • Offtop
  • O mnie
Menu

Zenject in depth cz.1 – DI i Unity? A po co?

Posted on 27 września, 201928 września, 2019 by Emi

Co gdybym powiedziała Ci, że istnieje jeden prosty sposób, aby Twój kod był dużo czystszy, łatwiejszy w utrzymaniu i bardziej testowalny? Że nie musisz tonąć w spaghetti przy każdym większym projekcie, bo jest łatwy i przyjemny sposób na pisanie luźno powiązanego kodu w Unity?

Dependency Injection to była miłość od pierwszego wejrzenia. Odkąd poznałam ten wzorzec, używałam go w każdym projekcie nad którym pracowałam – jeszcze zanim zajęłam się tworzeniem gier w Unity. Używałam go pisząc jakieś stronki w ASP .NET a także tworząc aplikacje desktopowe w WPFie. Bardzo mi go brakowało, kiedy zaczęłam pisać skrypty Unity, ale jakoś… Nie miałam pomysłu jak go użyć. W gruncie rzeczy wydawało mi się, że tak się nie robi i że nie da się zastosować DI do tego rodzaju projektów. Gdy jednak odkryłam Zenjecta – plugin do DI stworzony specjalnie z myślą o Unity – przekonałam się, że jak najbardziej jest to możliwe.

Czym zatem jest Dependency Injection? Cóż, jest jedną z implementacji wzorca Inversion of Control. 🙂 W tradycyjnie napisanym programie, nasz kod wykonuje pewnie kroki, używają metod z bibliotek do których posiada referencje. Przy podejściu IoC, pewien zewnętrzny kawałek oprogramowania wywołuje nasze fragmenty kodu. Czyli niejako „oddajemy kontrolę” nad naszym programem pewnej zewnętrznej bibliotece.

W podejściu Dependency Injection mamy możliwość wstrzykiwania zależności do naszych klas. Zależności te – na przykład implementacje interfejsów – definiowanie są za pomocą tzw. kontenera DI. Kluczowe jest tutaj to, że nasza klasa nie tworzy sobie wewnętrznie żadnych zależności – ja już osobiście powoli zaczynam mieć alergię na słowo „new” w kodzie. 🙂 Jedyne co może zrobić nasza klasa, to grzecznie poprosić o przekazanie jej tych zależności przez kontener, w momencie tworzenia jej instancji.

Jak może prosić? Na wiele sposobów. 🙂 Może utworzyć specjalne właściwości oznaczone odpowiednim atrybutem, może zdefiniować je w parametrze konstruktora albo za pomocą oznaczonych metod. Zobaczmy, jak może to wyglądać na przykładzie:

public class MyClass
{
	private OtherClass otherClassInstance;

	public MyClass()
	{
		otherClassInstance = new OtherClass();
	}
}

Tutaj zaś zależność jest wstrzyknięta za pomocą udekorowanej metody:

public class MyClass
{
	private OtherClass otherClassInstance;

	[Inject]
	public void ConstructWithInjection(OtherClass otherClass)
	{
		otherClassInstance = otherClass;
	}
}

Skoro zatem klasa nie tworzy sobie zależności, kto się tym zajmuje? Tutaj właśnie może się wykazać nasz kontener. Przechowuje on definicje zależności między klasami i interfejsami i tworzy ich instancje wtedy, kiedy są potrzebne. Utworzone instancje mają już przekazane potrzebne im referencje do innych klas – jak widać jest tutaj możliwość stworzenia zależności cyklicznych, czego należy się wystrzegać. Podczas tworzenia kontenera, nasz plugin (czyli w moim przypadku np. Zenject) tworzy drzewo zależności między klasami i inicjalizuje je w odpowiedniej kolejności.

Zatem możemy po prostu w całej naszej aplikacji używać instancji kontenera do pobierania potrzebnych nam zależności?

Teoretycznie możemy. 🙂 Jednak zabiłoby nam to większość naszych wspaniałych benefitów używania wzorca DI w naszym kodzie. Przyjrzyjmy się zatem zaletom wynikającym z takiego podejścia (i generalnie z praktyki polegania na abstrakcjach zamiast na konkretnych implementacjach):

  1. Zyskujemy jedno, konkretnie zdefiniowane miejsce, gdzie określamy używane przez nas implementacje interfejsów. W momencie, gdy zmienia nam się logika biznesowa, wystarczy zmiana jednej linijki aby przypiąć nową klasę do danego interfejsu. Jeśli reszta kodu operuje na abstrakcji, ich postać nie będzie musiała być zmieniona aby program dobrze działał.
  2. Używanie kontenera bardzo pomaga w przypadku tworzenia wieloplatformowych aplikacji. Pisałam kiedyś sporo klas (np. do obsługi mikrotransakcji bądź analityki), które musiały odwoływać się do różnych bibliotek w zależności od platformy – kod był poszatkowany dyrektywami w stylu #if UNITY_IOS. Szybko stawało się to pokręcone i trudne w utrzymaniu. Gdy dysponujemy jednym miejscem definiowania implementacji, możemy właśnie tam – w installerze – dodać te dyrektywy. Po prostu tworzymy interfejs, pod który – zależności od platformy pod którą aktualnie kompilujemy projekt – podpinamy odpowiednią implementację.
  3. Gdy wszystkie zależności naszych klas przekazywane są np. przez konkretną metodę, wszystkie te klasy stają dużo bardziej testowalne. Możemy po prostu przez tę metodę przekazać nasze mockowe instancje zależności, dzięki czemu przetestujemy jednostkowo tylko naszą konkretną klasę.
  4. DI pozwala nam pisać bardziej SOLIDny kod – ułatwiając nam stosowanie się do zasady Dependency Inversion i pisać luźniej powiązany kodzik. W bardzo dynamicznie zmieniającym się środowisku jakim jest game dev, zmiana danej funkcjonalności nie powinna zmuszać nas do przeorania reszty apki. 🙂
  5. Definicje zależności naszych klas są ładnie ułożone na szczycie pliku, więc bardzo wyraźnie widać, jakie kawałki kodu mogą mieć wpływ na tę naszą konkretną klasę. Ilość tych zależności jest pewnego rodzaju wskaźnikiem – jeśli robi się ich podejrzanie dużo, to być może łamiemy zasadę Single Responsibility.

Jest jeszcze jedna duża zaleta implementacji DI w naszym projekcie. Sprawia, że wzorzec Singleton znów staje się użyteczny. 🙂 Nie oszukujmy się, każdy z nas potrzebuje czasem starego, dobrego singletona i nie chodzi mi tutaj o szklaneczkę dobrej whisky. 😉

Zawsze istnieje potrzeba użycia różnego rodzaju serwisów, utilsów itp., które nie potrzebują tworzenia wielu instancji. Tradycyjne użycie singletonów ma jednak tak wiele wad, że zaczęto go nawet nazywać „antywzorcem” – powoduje trudności w testowaniu klas i wprowadza silne zależności w kodzie. Używając kontenera wystarczy zadeklarować daną klasę jako „pojedynczą” – wówczas w kontenerze zostanie stworzona tylko jedna instancja tej klasy, która zostanie przekazana wszystkim innym jako zależność (za pomocą interfejsu, oczywiście). Inne klasy nawet nie zauważą, że mają do czynienia z singletonem. 🙂

Tak wiele informacji, a prawie nie używałam słowa „Zenject”. Cóż, Zenject jest tylko, i aż, implementacją wzorca DI dostosowaną konkretnie pod Unity. Zawiera wszystko czego potrzebujemy – wstrzykiwanie zależności w skrypty MonoBehaviour, definiowanie różnego rodzaju kontekstów, np. per scena i per projekt a także całą masę różnych installerów – klas w których definiujemy zależności. Umożliwiają nam nawet instalowanie w kontenerze GameObjectów bądź plików konfiguracyjnych w formie ScriptableObjectów.

Ale o tym wszystkim opowiem Wam ze szczegółami w kolejnych odcinkach mojej opowieści o Zenject. Przyjrzymy się ze szczegółami wszystkim jego elementom, a także przedstawię najlepsze praktyki korzystania z niego. Stworzyłam już kilka różnych projektów z jego użyciem i zdążyłam popełnić kilka błędów i wyciągnąć z nich wnioski. 😉 Do usłyszenia!

Posted in gamedev, programmingTagged czystykod, Unity, Zenject

Nawigacja wpisu

Programistka po biol-chemie cz.1 – O tym, jak podjęłam decyzję o zmianie planów na przyszłość
Programistok 2019 – relacja

Related Post

  • O Boiling Frogs 2020, o tym czym jest rzemiosło, o silnym postanowieniu i o tym, po co jest after party
  • Zenject – najlepsze praktyki i tipsy
  • Zenject in depth cz.3 – Bindings & Injections

2 thoughts on “Zenject in depth cz.1 – DI i Unity? A po co?”

  1. Pingback: Zenject in depth cz.2 – Contexts & Installers – Blog Emi
  2. Pingback: DaftShot jesień 2019 – czyli poznaj naszą firmę od środka – Blog Emi

Dodaj komentarz Anuluj pisanie odpowiedzi

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Copyright © AllTopGuide 2023 • Theme by OpenSumo