Odpowiem na podstawie kodu Fairtrisa.
Jeśli o mnie chodzi, to preferuję podział danych i logiki na małe klasy, o pojedynczej odpowiedzialności. Dzielę sobie kod na kupkę małych obiektów ogólnego przeznaczenia — np. settings
, input
, sounds
, scores
. Klasa każdego takiego głównego obiektu jest dzielona na mniejsze klasy — tak robię aż do osiągnięcia pełnego drzewka małych klas. Najwyżej w hierarchii są klasy ogólnego przeznaczenia, najniżej te, które implementują małą część danych/logiki i które są ogólnego przeznaczenia (np. jakiś mały kontener na dane).
Następnie tworzę sobie zmienne globalne dla każdej głównej klasy (tych stojących najwyżej w hierarchi, najbardziej abstrakcyjnych), w module zawierającym implementację klasy. Łącznie w projekcie jest ich ~kilkanaście. Każdy taki globalny obiekt może używać innego globalnego obiektu, bez żadnych ograniczeń. To powoduje, że ogranicza się przepychanie danych w parametrach metod i duplikację referencje — dostęp do czegokolwiek jest bezpośredni. Możliwe jest też, że dwa obiekty będa korzystać z siebie nawzajem. Dzięki temu, że globalne zmienne są rozsiane po modułach, nie ma problemu z błędami „circular reference”.
Na koniec deklaruję sobie ukochany przez wszystkich ”god object”, którego zadaniem jest tworzenie instancji głównych klas, ich inizjalizacja oraz zwalnianie z pamięci. Obiekt ten jest tworzony i zwalniany w głównym pliku projektu (u mnie .lpr
), istnieje przez całą sesję. W Fairtrisie, jego dodatkowym zadaniem jest wykonywanie głównej pętli gry, czyli wywoływanie w odpowiedniej kolejności najbardziej abstrakcyjnych metod z głównych obiektów.
Problem jaki pojawia się w takim przypadku, to kontrola nad tym, co te główne obiekty robią same ze sobą — w końcu mogą z siebie korzystać do woli. Aby go wykluczyć, ”god object” najpierw tworzy główne obiekty, a następnie po kolei wywołuje ich metody inicjalizacji. Dopiero po inicjalizacji, komunikacja pomiędzy głównymi obiektami jest możliwa, bo każdy z nich ma już kompletne dane (np. pobrane z systemu, wczytane z plików) i przygotowane do użytku. Tak więc tutaj trzeba zadbać o to, aby każdy główny obiekt korzystał z innych dopiero po inicjalizacji ich wszystkich.
Trochę linków do mojego crap-kodu:
W większości pozostałych modułów znajdują się implementacje klas głównych obiektów.
Nie będziesz wiedział od razu jak to wszystko działa, bo kodu jest łącznie 15k linijek, więc sugeruję pobrać projekt, otworzyć w Lazarusie, przejść do modułu .lpr
, postawić breakpoint na pierwszej instrukcji i linijka po linijce sprawdzić co jest wykonywane (F7
aby wejść do metody, F8
aby ją wykonać bez wchodzenia do niej, Shift+F8
aby wyjść z danej metody). Gra jest jednowątkowa, więc możesz bez problemu debugować każdy kawałek kodu, w dowolnym momencie.
To nie jest sposób powszechnie określany jako prawidłowy — został on opracowany przeze mnie, sposób, który najbardziej mi odpowiada, najmocniej ułatwia pracę z kodem i najmniej komplikuje logikę. Dlatego też traktujcie to raczej jako ciekawostkę, nie jako wzór do naśladowania.
Co prawda struktura gry nijak ma się do struktury aplikacji okienkowej (a tym bardziej biznesowej), ale z powodzeniem stosowałem tę technikę w aplikach okienkowych (np. w CTCT czy Richtrisie). W przyszłości zamierzam stworzyć dużą grę, której wielkość oceniam na 100-150k LoC bez komentarzy (plus dodatkowe narzędzia okienkowe) i z moich obserwacji wynika, że nie będę miał żadnego problemu w wykorzystaniu swojej techniki.