16.2: Refaktoryzacja funkcji ruchu gracza

Cele lekcji

Po zakończeniu tej lekcji…

  • Dowiesz się, jak podzielić większą funkcję na kilka mniejszych – prostszych w obsłudze.
  • Będziesz potrafić przenieść funkcje z interfejsu użytkownika do „klas biznesowych”.

 

W poprzedniej lekcji stworzyliśmy ogromną funkcję odpowiadającą za przechodzenie gracza do nowej lokacji. Jednak nie była ona tylko duża, ona była ZA duża, więc jej obsługa była bardzo trudna.

Teraz skrócimy ją i poprzenosimy jej elementy, obniżając jej poziom komplikacji. Takie operacje są nazywane „refaktoryzacją”.

Refaktoryzacja to niezwykle obszerny temat obejmujący wiele technik. W naszej lekcji skupimy się na kilku popularnych, prostych metodach zapewniających największe korzyści. W miarę nauki programowania poznacie z pewnością kolejne techniki refaktoryzacji.

 

Tworzenie funkcji obsługujących dane wprowadzane przez użytkownika

 

Etap 1: Uruchom aplikację Visual Studio i otwórz swoje rozwiązanie.

 

Etap 2: Kliknij prawym przyciskiem myszy formularz SuperAdventure.cs w projekcie SuperAdventure, a następnie wybierz pozycję View Code.

 

Etap 3: Kliknij dwukrotnie klasę Player w projekcie Engine i zastąp ją kodem z klasy Player (nie aktualizuj jeszcze kodu SuperAdventure): https://gist.github.com/ScottLilly/6670da749c4ad7e7bff7

 

Etapy refaktoryzacji

Refaktoryzacja polega tylko na przenoszeniu kodu, co w rezultacie pozwala na wydajniejsze korzystanie z niego. Nie będziemy dodawać żadnych nowych funkcji, naprawiać błędów ani poprawiać wydajności.

Podczas refaktoryzacji można wykonać wiele operacji, ale my skoncentrujemy się na kilku najbardziej popularnych technikach.

 

Etap 1: Wyszukiwanie powielonego kodu.

Jeśli ten sam kod znajduje się w wielu miejscach, to taki kod zazwyczaj można przenieść do własnej funkcji. Następnie, w miejscach, które korzystały z tego kodu, należy wprowadzić zmiany, powodujące odwoływanie się do tej nowej funkcji.

Jest to szczególnie ważne w kontekście wprowadzania zmian w przyszłości.

Kiedy tworzyliśmy funkcję MoveTo(), mogliśmy wstawić cały ten kod do czterech funkcji, które powodowały przemieszczanie gracza. Jeśli jednak w przyszłości postanowimy zmienić sposób działania logiki ruchu, to będziemy musieli wprowadzić zmiany w czterech miejscach w kodzie. Może się więc okazać, że nieuważny programista wprowadzi zmiany tylko w trzech miejscach, zapominając o czwartym. Wtedy nagle postać zacznie zachowywać się dziwacznie, gdy będzie się poruszać w jednym kierunku – tym, w przypadku którego, nie wprowadzono zmiany.

W tej funkcji nie mamy powielonego kodu. W kilku miejscach kod jest bardzo podobny do siebie, ale nie jest identyczny, więc musimy wymyślić inną metodę refaktoryzacji tej funkcji.

 

Etap 2: Wyszukiwanie kodu mającego jeden cel i przeniesienie go do własnej funkcji.

W naszej ogromnej funkcji znajduje się mnóstwo kodu podpadającego pod ten przypadek.

Na przykład wiersze od 56 do 78 sprawdzają, czy istnieje przedmiot wymagany w danej lokalizacji, a jeśli tak, czy gracz ma go w swoim ekwipunku. Tę operację możemy przenieść do osobnej, mniejszej funkcji.

Przenosząc tę sekcję kodu, warto się zastanowić, czy można ją umieścić w lepszym miejscu, np. w innej klasie, ponieważ teraz znajduje się ona w sekcji kodu należącej do interfejsu użytkownika. Ten kod jest powiązany z ekwipunkiem gracza, więc uzasadnione jest przeniesienie do klasy Player.

Spójrzmy teraz na wiersze 28-48 nowego kodu klasy Player, które właśnie dodaliśmy.

 

W tej funkcji podajemy lokację i sprawdzamy, czy gracz może się tam przenieść – czyli weryfikujemy, że w lokacji nie jest wymagany żaden przedmiot albo że gracz ma wymagany przedmiot.

Nowa funkcja HasRequiredItemToMoveToThisLocation() składa się z 20 wierszy, wykonuje tylko jedną operację i jest na tyle mała, że łatwo można ją zrozumieć. Jeśli kiedykolwiek będziemy chcieli zmienić tę logikę, to modyfikacje będą wymagane tylko w tym jednym miejscu.

Na przykład, możemy zmienić zasady gry i wprowadzić wymóg minimalnego poziomu niezbędnego do przejścia do niektórych lokacji. Można to zrobić w tej funkcji i nie trzeba się przebijać przez 300 wierszy kody.

Funkcję już mamy w klasie Player, więc możemy oczyścić klasę SuperAdventure. Zastąp kod w wierszach 57-78 na następujący:

 

Właśnie 20 wierszy kodu zmieniło się w 6, a nasza funkcja stała się bardziej przejrzysta. Na tym właśnie polega refaktoryzacja.

 

Teraz do klasy Player przeniesiemy kod sprawdzający, czy gracz przyjął zadanie i wykonał je.

Spójrzmy na dwie funkcje w klasie Player w wierszach od 50 do 74:

 

W klasie SuperAdventure.cs zastąp wiersze 86 i 87 tymi wywołaniami do obiektu Player i usuń wiersze 88-100.

 

Teraz przesuniemy kod sprawdzający, czy gracz ma w swoim ekwipunku wszystkie przedmioty wymagane do wykonania zadania.

W klasie Player spójrz na wiersze 76-106:

 

Wróć do klasy SuperAdventure.cs i wiersz 96, zastąp poniższym kodem:

 

Następnie usuń wiersze 97-133.

 

Możemy również przenieść kod usuwający przedmioty wymagane do wykonania zadania z ekwipunku gracza do klasy Player.

W klasie Player w wierszach 108-122 mamy nową funkcję:

 

Teraz wróć do klasy SuperAdventure.cs i usuń wiersze 106-117 i w ich miejsce wprowadź ten kod:

 

Możesz również do klasy Player przenieść kod dodający nagrodę do ekwipunku gracza.

W klasie Player.cs spójrz na wiersze 124-139:

 

W klasie SuperAdventure.cs wiersze 119-138 zastąp następującym kodem:

 

W klasie Player.cs spójrz na funkcję w wierszach 141-154:

 

Wróć do klasy SuperAdventure.cs. Wiersze 122-132 zastąp kodem:

 

W pozostałej części funkcji znajduje się kod aktualizujący formanty ComboBox i DataGridView, ponieważ ekwipunek gracza mógł ulec zmianie z powodu wykonania zadania.

W klasie SuperAdventure.cs utwórz następujące nowe funkcje:

 

Następnie wiersze 183-272 zastąp kodem:

 

Czy funkcja jest teraz prostsza?

Wcześniej funkcja MoveTo() miała ponad 300 wierszy długości, a teraz składa się tylko ze 140.

Nadal jest ona długa, ale teraz znacznie łatwiej się ją czyta i jest prostsza do zrozumienia.

Wykonaliśmy również inną ważną operację – przenieśliśmy sporo kodu „logiki” poza klasę interfejsu użytkownika. Kod stosowany w klasie interfejsu użytkownika powinien odpowiadać tylko za obsługę danych wejściowych przekazywanych przez użytkownika i wyświetlanie danych wyjściowych, więc powinien zawierać jak najmniej kodu obsługującego logikę.

Teraz znaczna część logiki gry znajduje się w projekcie Engine – czyli tam, gdzie powinna.

Tę funkcję moglibyśmy jeszcze bardziej zrefaktoryzować, ale jak na nasze potrzeby operacje, które wykonaliśmy, są wystarczające.

Jeśli te kwestie Cię interesują, zapoznaj się z technologią .Net LINQ. Za jej pomocą można tworzyć jeszcze mniejsze nowe funkcje w klasie Player. Ale LINQ to zupełnie osobna kwestia i nie będę jej wprowadzał w naszym poradniku dla początkujących.

 

Podsumowanie

Refaktoryzacja nie zmienia działania programu, tylko oczyszcza istniejący kod, ułatwiając jego zrozumienie i obsługę.

Często operacje te polegają na wyszukiwaniu fragmentów funkcji, które można przenieść do własnych funkcji. Następnie pierwotna funkcja wywołuje te nowe, mniejsze funkcje, które łatwiej zrozumieć programiście, ponieważ nie są one „ukryte” w olbrzymich funkcjach. Natomiast duże funkcje łatwiej się czyta, ponieważ są one „odchudzone” i zawierają mniej kodu.

UWAGA: W kodzie wprowadziłem wiele zmian. Jeśli nie masz pewności, czy wszystko wpisujesz poprawnie, kod z poniższego łącza możesz wkleić do klasy SuperAdventure.cs.

 

Łącza do tej lekcji

Kod źródłowy w serwisie GitHub

Kod źródłowy w serwisie Dropbox

Leave a Reply

Your email address will not be published. Required fields are marked *