Skip to content

Blog Emi

O grach z kobiecej perspektywy

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

Zenject in depth cz.3 – Bindings & Injections

Posted on 16 grudnia, 201916 grudnia, 2019 by Emi

Wiemy już, czym jest kontener oraz gdzie definiujemy kontrakty obecne w naszej grze. Czas dowiedzieć się, jak je tworzymy oraz jak właściwie używamy Zenjecta pisząc resztę kodu.

Przyjrzyjmy się, jak może wyglądać najbardziej podstawowy kod instalatora:

using Zenject;

public class UtilsInstaller : ScriptableObjectInstaller
{
	public override void InstallBindings()
	{
		Container.Bind<IStringSerializer>()
                         .To<JsonSerializer>()
                         .AsSingle();
	}
}

Kluczowym elementem naszej pracy z Depencency Injection będzie oczywiście definiowanie kontraktów – bindingów, czyli określanie, jaki obiekt faktycznie będzie stworzony, gdy klasa deklaruje inną klasę jako zależność. W tym konkretnym przypadku pisząc klasę, która na przykład zapisuje stan gry gracza do pliku, nie musimy zastanawiać się, jaki konkretnie serializer będzie potrzebny – wystarczy, że nasza klasa zadeklaruje jako wstrzykiwaną zależność obiekt IStringSerializer. Kotener wie, że w takim przypadku powinien jej przekazać obiekt klasy JsonSerializer. Jak zmienimy zdanie odnośnie naszej architektury i zechcemy użyć innego formatu danych, wystarczy, że podmienimy JsonSerializer na inną klasę w instalatorze i voila! Cała nasza aplikacja zacznie korzystać z tego zmienionego formatu.

Rodzaje bindingów

Charakterystykę naszego wiązania tak naprawdę budujemy chainując różne opcje po Container.Bind. Ja właściwie dzielę wszystkie kontrakty na takie główne grupy:

Czyli albo definiujemy tylko wiązanie, zostawiając kontenerowi kwestię tworzenia obiektów, albo wstrzykujemy gotowy już, stworzony wcześniej obiekt, np. przeciągnięty za pomocą Inspektora (konfiguracja/element sceny/prefab/itp.). Przy czym należy pamiętać, że używając pierwszej opcji koniecznie musimy zdefiniować zakres tworzonego obiektu – czy będzie on singletonem, czyli dla każdej klasy, która o niego poprosi, zostanie dostarczony ten sam obiekt, czy też za każdym razem kontener powinien stworzyć nowy obiekt.

Tak naprawdę elementy ze schematu powyżej w zupełności wystarczają w większości przypadków i pozwalają na rozpoczęcie pisania projektu z użyciem DependencyInjection, jednak w dokumenacji Zenjecta możemy zobaczyć, że mamy bardzo dużo opcji konfiguracji naszego bindingu:

Container
.Bind<ContractType>()
.WithId(Identifier)
.To<ResultType>()
.FromConstructionMethod()
.AsScope()
.WithArguments(Arguments)  
.OnInstantiated(Callback)
.When(Condition)   
.(Copy|Move)Into(All|Direct)SubContainers()
.NonLazy()
.IfNotBound();

Oto kilka z nich, których zdarzyło mi się użyć:

  • WithId – id definiujemy, jeżeli chcemy zdefiniować kilka wiązań do tej samej klasy/interfejsu, aby mieć potem możliwość rozróżnienia, którą z nich kontener powinien wstrzyknąć,
  • FromConstructionMehod – widzieliśmy już wyżej „FromInstance”, czasami przydaje się np. FromMethod – jeśli inicjalizacja tworzonego obiektu jest zdefiniowana w jakiejś zewnętrznej metodzie (nie w konstruktorze),
  • When – binding warunkowy, w pewnym sytuacjach jest bardzo przydatny, np. jeśli uzależniamy konfigurację kontenera od zmiennej środowiskowej obecnej w systemie albo jakiegoś innego kawałka konfiguracji. Wzięcie całego bindingu w ifa też tak zadziała, ale ta metoda może być czasem nieco czytelniejsza,
  • NonLazy – używamy tego właściwie zawsze tworząc singletony. W większości przypadków chcemy, aby obiekt ten stworzył się od razu po stworzeniu bindingu a nie dopiero w momencie, gdy jakiś inny obiekt o niego poprosi – co mogłoby się wydarzyć np. podczas gameplayu i spowodować spadek FPSów.

Injections

Stworzyliśmy już wszystkie kontrakty obecne w naszej aplikacji, pogrupowaliśmy je w instalatory i konteksty – czas zacząć ich używać! Tu sprawa wygląda już prosto:

public class MyClass
{
	private OtherClass otherClassInstance;
	private ISomeClass someClassInstance;

	[Inject]
	private IAnotherClass anotherClassInstance;

	[Inject]
	public void ConstructWithInjection(ISomeClass object)
{
	someClassInstance = object;
} 

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

Mamy trzy metody wstrzykiwania zależności:

Konstruktor – zdecydowanie preferowana metoda, spodziewamy się, że w konstruktorze występuje kod odpowiedzialny za inicjalizację klasy, zazwyczaj tam mamy resztę kodu, która już wymaga podanych zależności, a klasa z tak zdefiniowanymi zależnościami jest bardzo ładnie testowalna – w teście jednostkowym wystarczy do tego konstruktora przekazać mocki.

Metoda – jak wiadomo, w skryptach dziedziczących po MonoBehaviour nie używamy konstruktorów, więc tutaj najlepszym sposobem będzie zdefiniowanie specjalnej metody do celu inicjalizacji. Metoda ta może być prywatna, chociaż jeśli chcemy zapewności testowalność naszej klasie, musimy zostawić ją jako publiczną. Warto przyjąć w całej aplikacji jakąś uniwersalną nazwę tej metody, np. ConstructWithInjection. Metoda ta jest też fajnym miejscem na przeprowadzenie wszelkich innych operacji związanych z inicjalizacją skryptu.

Pole – najwygodniejszy sposób :). Pole może być private readonly, gdyż kontener używa mechanizmu refleksji do wyszukiwania oznaczonych pól i metod. Wystarczy dodać atrybut [Inject] i bez żadnego dodatkowego kodu nasz obiekt zostanie odpowiednio zainicjalizowany. Jeśli nie zamierzamy pisać testów jednostkowych, to ten sposób jest całkiem ok, choć ja zazwyczaj i tak tworzyłam dodatkowo bezargumentową, udekorowaną metodkę Init, tylko po to, żeby mieć właśnie to wygodne miejsce do inicjalizacji skryptu. Myślę jednak, że warto poświęcić te parę minutek na tworzenie dodatkowych metod – jak jednak zechcemy zacząć pisać testy jednostkowe to nie będziemy musieli refaktorować całego kodu. 🙂

Podsumowując – najlepiej jest definiować zależności klasy w konkstruktorze. Jeśli mamy do czynienia ze skryptem MonoBehaviour – w metodzie, a jeśli zależy nam tylko na wygodzie i nie dbamy o testowalność – pole dobrze się sprawdzi.

„Nie dbamy o testowalność” – brzmi trochę jak zarzut, ale trzeba przyznać, że jeśli chodzi o testowanie jednostkowe skryptów Unity to nie jest to łatwe zadanie i większość osób i tak tego nie robi. 😉

ZenAutoInjecter

Jak to w ogóle się dzieje, że skrypty mają wstrzyknięte zależności? Otóż w momencie uruchomienia gry i załadowania sceny, jeśli na danej scenie znajduje się SceneContext, Zenject skanuje wszystkie GameObjecty w poszukiwaniu skryptów z atrybutem Inject. Obiekty stworzone później (np. elementy jakiegoś UI, które tworzone są z kodu na podstawie prefabów) nie będą miały wstrzykniętych zależności i w konsoli przywitają nas czerwone NullReferenceException. Z pomocą przychodzi tutaj komponent ZenAutoInjecter, który wystarczy przypiąć do głównego obiektu w prefabie. Skrypt ten podczas stworzenia obiektu użyje wybranego kontekstu i poprosi kontener o wstrzyknięcie zależności.

I już – macie już całą wiedzę potrzebną aby rozpocząć przygodę z Zenjectem. W kolejnym artykule znajdziecie trochę porad i wskazówek jak sprawić, żeby praca z nim była przyjemnością. 🙂

Posted in gamedev, programmingTagged czystykod, Unity, Zenject

Nawigacja wpisu

DaftShot jesień 2019 – czyli poznaj naszą firmę od środka
Zenject – najlepsze praktyki i tipsy

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
  • Zdjęcie przedstawia wnętrze kuchni DaftCode, gdzie grupka ludzi pije drinki DaftShot jesień 2019 – czyli poznaj naszą firmę od środka

2 thoughts on “Zenject in depth cz.3 – Bindings & Injections”

  1. Zachi pisze:
    23 grudnia, 2019 o 21:16

    Hej,

    Mam taką jedną uwagę do wpisu numer dwa – najnowsza wersja Zenjecta to aktualnie Extenject ( https://github.com/svermeulen/Extenject ) – jakieś spory prawne, można by link uaktuanić.

    I niezwiązane z powyższym pytanie – Dużo małych instalatorów czy kilka większych?
    Jeśli mam menu i w tym menu mam zakładki do wyboru poziomu, postaci, trybu gry to lepiej się sprawdzi jeden installer na całe menu czy do każdej sekcji (strony?) osobny?

    Jest jakaś sprawdzona reguła?

    Odpowiedz
    1. Emi pisze:
      30 grudnia, 2019 o 11:49

      Hej 🙂
      Racja, nawet ostatnio w pracy trafiliśmy na buga związanego z wielowątkowością który jest naprawiony w extenjectie 🙂 aktualnie jestem poza krajem, poprawię jak wrócę!

      Co do pytania – raczej instalator per ficzer. Menu to tylko widok, więc raczej bym nie tworzyła struktury pod tym kątem – ficzer to np customizacja postaci która miałaby swój instalator, zarządzanie progresem też itp. Jeśli widoki są skomplikowane to wtedy instalator per ficzer zgrupowane w installerze menu. Mi się lepiej pracuje jak jest więcej małych installerów, kod jest wtedy luźniej powiązany:)

      Odpowiedz

Dodaj komentarz Anuluj pisanie odpowiedzi

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

Copyright © AllTopGuide 2023 • Theme by OpenSumo