Dependency Injection – Inversion of Control

Nastał czas na drugi artykuł(ale to melancholijnie zabrzmiało 🙂 ) w poprzednim artykule zrefaktoryzowaliśmy kod prototypu gry webowej, ale i tak dalej tam można jeszcze dużo poprawić, używając głównie dependency injection co właśnie w tym artykule zrobimy, jedziem 🙂


Wstęp

Mamy już kod tej gry trochę uporządkowany oraz konkretne funkcjonalności wyciągnięte do osobnych klas.

Zanim przejdziemy do artykułu chciałbym wyjaśnić kwestie klas postaci. Jeden z czytelników wspomniał, że klasy postaci są zbędne, że lepiej zrobić do tego funkcje bo te klasy tylko zmieniają swój stan a nie mają oddzielnej logiki, ma rację. Jednak byłoby to cięższe do zrozumienia a ja nie robie komercyjnej gry, która bedzie utrzymywana ileś tam lat, tylko chcę, żeby było to łatwe do zrozumienia, czasami trzeba przyjąć mój punkt widzenia 🙂

Jednak gdybyśmy robili jakiś bardziej skomplikowany przykład gry i każda z klas miałby oddzielne funkcje dla konkretnej klasy postaci to już same funkcje sprawy by nie załatwiły bo to już by była oddzielna logika. No cóż wszystko jest dobre tak naprawdę, jeśli będzie dopasowane do problemu 🙂

Jeśli będziecie robić w przyszłości jakąś dużą grę załóżmy RPG a klasy postaci będą tylko zmieniać swój stan i nie będą miały jakiejś oddzielnej logiki to faktycznie lepiej zrobić funkcje niż klasy, ale gdyby np klasa barbarzyńcy będzie miała swoje funkcje jakiś specjalnych umiejętności np:

A klasa załóżmy maga bedzie wyglądać tak:

To w takim wypadku pozostałbym przy klasach, dla łatwiejszego utrzymywania, modyfikowania, testowania itede, itepe 🙂

A wracając do artykułu… 🙂

Dalej musimy jak najbardziej ograniczyć używanie słowa  „new” w kodzie aplikacji    Pokaże również jak stosować dependency injection, jeśli ktoś tworzy projekty w TDD.

Oraz pokaże jak korzystać tylko z typów abstrakcyjnych a nie konkretnych wtedy kiedy klasa potrzebuje jakichś zależności z innych klas tak jak klasa  CharacterSkillPoints  z poprzedniego artykułu.

Nie powinno sie i nawet nie można umieszczać typów konkretnych klas i innych klasach.

No dobra, ale tu jest jeszcze jeden szkopuł, tak jak powiedziałem na początku musimy unikać słowa „new” w aplikacji a tworzymy instancje w konstruktorze czego właśnie nie powinniśmy robić…

I teraz ktoś może pomyśleć „No dobra, ale jak to obejść? Przecież jakoś muszę tworzyć obiekty!”

Spokojnie i na to jest sposób 🙂

No cóż do boju!

Jak będe chciał pokazać jakiś kod z poprzedniego przykładu to wkleje link do tego kodu na githubie, żeby nie zawalać artykułu ścianą kodu 🙂


Interfejsy w Dependency Injection

Klasy postaci są bez zmian, zacznijmy od klas walidujących login i hasło.

Klasy do walidacji loginu i hasła z poprzedniego przykładu wyglądały tak:

Klasa walidacji loginu:
https://github.com/Slaw145/Dependency-injection–refactored-game-project/blob/master/RefactoredGameProject/LoginValidator.cs

Klasa walidacji hasła:
https://github.com/Slaw145/Dependency-injection–refactored-game-project/blob/master/RefactoredGameProject/PasswordValidator.cs

A teraz wyglądają tak:

Klasa LoginValidator

Oraz klasa PasswordValidator

Zmiana w kodzie jest niewielka jedynie dodaliśmy interfejsy, ale po co te interfejsy?

Interfejsy dają bardzo dużo między innymi dwie rzeczy.

Po pierwsze teraz zamiast uzależniać klasę od typów konkretnych, będziemy mogli uzależniać ją od abstrakcji co jest poprawnym sposobem, tak jak np klasę GameServer uzależnialiśmy w poprzednim artykule od typów konkretnych, a poprawne uzależnianie od abstrakcji wygląda tak:

Teraz wystarczy, że w miejsce klas typów konkretnych wstawimy interfejsy i sprawa załatwiona, ale przejdziemy do klasy GameServer później, dalej w artykule pokaże cały kod klasy GameServer 🙂 ale wszystko po kolei 🙂

Po drugie dzieki interfejsom możemy przetestować naszą aplikację bez uruchamiania jej co jest bardzo ważne dla mnie jak i dla programistów co stosują TDD. Co zaraz pokaże na przykładzie 🙂


Mockowanie interfejsów

Załóżmy, że nie mamy zaimplementowanej klasy LoginValidator a tylko mamy jej interfejs a my chcemy implementować testy a nie uruchamiać aplikację w takim wypadku dzięki interfejsom możemy zamockować sobie tą klasę, tak jak poniżej.

I w ten sposób możemy przetestować interfejs nie mając go zaimplementowanego w kodzie produkcyjnym.

Dzięki interfejsom można również zamockować klasę kiedy nie obchodzą nas na jakimś poziomie szczegóły dotyczące walidacji loginu czy hasła, ale to pokaże później w artykule nie chcę mieszać chcę, żeby było wszystko zrozumiałe i uporządkowane.

W testach jednostkowych do klas walidujacych login i hasło zmieniliśmy tylko typ konkretny klas LoginValidator i PasswordValidator na ich interfejsy na końcu artykułu tak jak zwykle umiesze link do githuba z cały kodem źródłowym, więc będzie można wszystko przejrzeć 🙂


Pozostałe klasy z DI

Przejdźmy do klasy CharacterSkillPoints

W poprzednim artykule wyglądała ona tak: https://github.com/Slaw145/Dependency-injection–refactored-game-project/blob/master/RefactoredGameProject/CharacterSkillPoints.cs

Nie wygląda to zbyt zachęcająco… użytkownik tworzy tylko jedną klasę a my mamy utworzone wszystkie klasy „na wszelki wypadek” oraz metody do każdej z klas, trzeba to poprawić…

A teraz wygląda tak:

To już wygląda 100 razy lepiej tworzymy tylko potrzebną klasę postaci i przekazujemy ją do metody, która rozdaje punkty umiejetności dla tej klasy, bomba 🙂

Zobaczmy jak wyglądały testy tej klasy z poprzedniego artykułu:

Przed zmianami: https://github.com/Slaw145/Dependency-injection–refactored-game-project/blob/master/NUnitTestGameProject/CharacterSkillPointsTest.cs

Po zmianach:

To też wygląda o niebo lepiej nie mamy teraz tyle testów i testujemy tylko tą klasę postaci, którą aktulanie stworzyliśmy w grze.

Zobaczmy teraz jak wygląda główna klasa GameServer, która zarządza tymi wszystkim klasami do walidacji danych klienta oraz klasami postaci

Najpierw spójrzmy na samą górę na konstruktor tak właśnie pozbyliśmy się new z kodu aplikacji oraz zamieniliśmy typy klas konkretnych na ich interfejsy 🙂 A obiekty tych klas tworzymy w kliencie. Tak właśnie wygląda poprawne tworzenie obiektów.

Jednak ten konstruktor dalej można ulepszyć. Załóżmy, że mielibyśmy 10 zależności w takim wypadku ten konstruktor w ogóle nie byłby czytelny, ale i z tym sobie poradzimy w następnym artykule. Przejdźmy dalej.

Metoda RegisterUser() wywołuje metody odpowiedzialne za walidację loginu i hasła, następnie sprawdzamy czy obydwie metody zwróciły true, jeśli tak zwracamy true, jeśli nie, zwracamy false. A wyjątek wyświetlamy w kliencie. Tutaj zwracamy prawdę lub fałsz, żeby można było łatwo przetestować tą metodę.

Przejdźmy do metody CreateCharacter() mamy teraz tam tylko jedną linijkę, która przekazuje stworzoną klasę postaci do klasy CharacterSkillPoints. W poprzednim artykule mieliśmy coś takiego w tej metodzie:

Czyli nie trzeba bawić się w żadne wzorce 🙂 Mówiłem, że dependency injection ułatwia życie 🙂

W ostatniej metodzie StartGame() sprawdzamy czy użytkownik poprawnie się zalogował i stworzył postać.

Zobaczmy tylko jak wyglądają jeszcze testy intergracyjne klasy GameServer, czyli takie testy, które sprawdzają czy wszystkie klasy współgrają ze sobą i tu właśnie bardzo się przyda metoda StartGame() 🙂

Aa oo prosze :

Z pomocą metody StartGame() sprawdzamy wszystkie przypadki co mogłoby pójść nie tak przy starcie gry. Testujemy te przypadki w którch gra się wywali i te w których gra się odpali. No proszę nawet się rymuje 🙂

Zobaczmy czy wszystkie testy przechodzą.

A teraz załóżmy, że na tym poziomie abstrakcji nie obchodzą nas szczegóły tworzenia postaci, więc musimy klasę postaci sobie zamockować 🙂 Wystarczy, że zamockujemy sobie interfejs ICharacter

I przekażemy zamockowany interfejs do metody StartGame() 🙂

A metody CreateCharacter() już nie potrzebujemy 🙂

Tak jak mówiłem link do całego kodu jak zwykle będzie w podsumowaniu, będzie można się nim pobawić 🙂

Sprawdźmy jeszcze raz czy testy przechodzą 🙂

Zobaczmy jeszcze klienta, czyli klasę WebServer. W niej po prostu korzystamy z wszystkich klas, które utworzyliśmy, logujemy się tutaj, tworzymy klasę postaci i włączamy grę.

Funkcje logujące użytkownika, tworzące klasę postaci i odpalające grę wydzieliłem do osobnych metod oraz wszystkie komunikaty są w kliencie nie w kodzie gry tak jak powinno być.

Wynik:


Testowanie niezaimplementowanego interfejsu – problemy

No dobra wcześniej pokazałem jak przetestować interfejs ILoginValidator nie mając jeszcze jego implementacji, ale to była tylko demonstracja, żeby pokazać o co chodzi, bo mamy owy interfejs zaimplementowany, więc załóżmy, że chcemy teraz dodać interfejs ICharacterRace, żeby rozbudować grę o rasy, czyli elf, krasnolud, niziołek itd, ale nie chcemy nic uruchamiać tylko implementować testy a nie interfejs bo to nudne 🙂

To zobaczmy jak wygląda interfejs ICharacterRace

Załóżmy, że ta metoda ma zwrócić utworzoną rasę postaci. Dodajmy jeszcze odpowiedni kod w klasie GameServer dla tego interfejsu.

W konstruktorze:

I metodę, która wywołuje metodę interfejsu ICharacterRace.

No i w kliencie.

Ale zaraz co to jest w konstruktorze GameServer? Przecież nie da się utworzyć instancji interfejsu! Co za człowiek… To nie będzie się nawet kompilować. Mogę sobie jedynie zamockować ten interfejs.

Ale skoro to nie będzie się kompilować to nie utworzymy klasy GameServer, nawet nie odpalimy testów. I co teraz? Powinniśmy rozłożyć ręce i płakać?

Spokojnie 🙂 jak to w IT chodzi głównie o to, żeby rozwiązywać problemy i w następnym odcinku rozwiążemy ten problem, rozwiążemy również problem konstruktorów z masą zależności oraz usuniemy new nawet z poziomu klienta.


Podsumowanie

W następnym artykule przejdziemy do tzw kontenerów, które rozwiążą nasze problemy, które wyżej opisałem 🙂

Link do githuba ze wszystkimi przykładami: https://github.com/Slaw145/DependencyInjectionInversionOfControl

Standardowo, przypominam o newsletterze, którym wysyłam powiadomienia o nowych wpisach oraz dodatkowe informacje na temat, ogółem mówiąc, świecie IT.

KONIECZNIE dołącz do społeczności DevmanCommunity na fb, części społeczności jest w jednym miejscu

-strona na fb: Devman.pl-Sławomir Kowalski

-grupa na fb: DevmanCommunity

Pytaj, komentuj pod spodem na końcu wpisu, podziel się nim, oceń go, rób co wolisz 🙂

Do następnego! Do zobaczenia!

 
Jeśli ten wpis ci się przydał podziel się nim ze swoimi znajomymi :)

4
Post a comment

avatar
2 Comment threads
2 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
3 Comment authors
SlawekflatmateBartek Recent comment authors
  Subscribe  
newest oldest evaluated
Notify about
Bartek
Guest
Bartek

Hej, kilka linijek można ładnie zastąpić jedną: if (loginmatchresult.Success) { return true; } else { return false; } jako return loginmatchresult.Success; dużo przejrzyściej.

flatmate
Guest
flatmate

Świetny poradnik! Fajnie wytłumaczone i łatwe do zrozumienia. Czekam na dalszą część 😀