Schowek na zmienne

0

Cześć,

Jako że jestem samoukiem i nie mam kontaktu z zawodowcami, muszę często sam wymyślać coś co zapewne zostało już stworzone w sposób optymalny dlatego mam pytanie. Gdzie przechowujecie duże ilości zmiennych do których musi być dostęp z różnych unitów?

Pozdrawiam
Robert

0

W liście parametrów do funkcji ew. z listy pol obiektu. Nowoczesne IDE to podpowiada. Zakładam, ze Delphi tez.

1
robertz68 napisał(a):

Gdzie przechowujecie duże ilości zmiennych do których musi być dostęp z różnych unitów?

A te zmienne są w jakiś sposób powiązane ze sobą?

0

chyba nie do końca się zrozumieliśmy, albo nie wytłumaczyłem dokładnie o co chodzi - tak to drugie :).
Chodzi o to że powiedzmy mam jakiś unit, nazwijmy go UMain i traktuje go jako główny w projekcie. Najczęściej dużo innych unitów w projekcie ma jakieś referencje do niego i fajnie jest gdy właśnie w nim przechowuję zmienne publiczne (bo powiedzmy są niezbędne do projektu).
Niestety taki główny unit też zazwyczaj ma dość dużo kodu w sobie. I tutaj pojawia się bardzo delikatny problem który chciałbym wiedzieć jak rozwiązują zawodowcy a mianowicie, długie przewijanie tego unitu w celu dotarcia do potrzebnej funkcji / procedury / zmiennej / stałej itp.
Wiem że np. w VS jest trend aby ichne unity były naprawdę krótkie (maksymalnie kilka ekranów) i wszystko rozbijane jest na osobne pliki.
Sam nie wiem czy to jest wygodne ale jednak spaghetti kod męczy mnie i dlatego pytam jak wy to robicie?
Osobny unit tylko na zmienne?

3

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.

3

God object to najgorszy antywzorzec moim zdaniem. Jak wszystko ma swoje racje bytu - np. w grach które spełniają mały wycinek odpowiedzialności, najczęściej są obsługiwane przez jedną osobą, przez określony krótki czas. Jeśli projektujemy system wysokiej dostępności, gdzie ma pracować ciągle, być skalowalny, odporny na awarie, umożliwić działaniu wielu użytkownikom w sposób asynchroniczny - to wtedy god class jest problemem. Taka hierarchia klas skutecznie utrudnia a w zasadzie uniemożliwia testy jednostkowe, bo nie przetestujesz jednostki - rozumiem, że logika jest rozproszona po obiektach a god je spina - co powoduje, że są zależne jedne od drugich i ich mockowania najczęściej jest niemożliwe.

W większych projektach stosuje się DI przez IoC - czyli wstrzykujemy zależności do funkcji/klasy przez kontener realizując wzorzec odwróconej kontroli. Wtedy cos zewnętrznego (kontener) decyduje jaki obiekt wejdzie do klasy, a nie ta god classa. To umożliwi przekazanie atrapy a nie konkretnego obiektu na potrzeby testów. Jak to osiągnąć? Interfejsami. Jeśli masz mały projekt, to okazać się może, że taki stworzony z interfejsów obiekt będzie podobny do good classy, tworzony na początku programu i będzie uruchamiał jakąś pętle, lub po prostu eventy będą go wołać. Niemniej tak stworzony obiekt może być testowalny, jego składowe są testowalne. Można na bieżąco podmieniać implementacje (np. żeby konfigurację nie trzymać w pliku a w bazie). Stosując to w systemie wielodostępowym, wielowątkowym można pod każdy wątek tworzyć nową instancje obiektu, jak coś się wywali to tylko w danej sesji, można zarządzać ilością obiektów utworzonych w ramach całej instancji programu. Tego z samą god classą nie osiągniesz.

Reasumując - god clasa sprawdzi się w małych jednoosobowych projektach i tylko tam. Każdy większy projekt będzie problemem.

Podejście ze wstrzykiwaniem zależności i kontenerem sprawdzi się tutaj i tutaj, mimo, że w przypadku małych programów może wydawać się zbędnym boilerplatem.

Niestety nie posiadam OS kodu w Delphi co to obrazuje. mORMot ułatwia takie pisanie. Inne platformy robią to lepiej, ale i w Delphi się da. Zazwyczaj pracowałem na jakiś autorskich fabryczkach i DI.

Za to zmienne globalne rozrzucone w unitach to jest najgorsze co może być. Niestety programiści Delphi jak uczyli się w Polsce pisać to nie potrafili programować, a potem często (i racjonalnie) zmieniali technologię . Nawet sporo książek po polsku propaguje antywzorce.

3

god clasa sprawdzi się w małych jednoosobowych projektach i tylko tam. Każdy większy projekt będzie problemem.

No ale właśnie o coś takiego chodzio @robertz68 gdy pisał Jako że jestem samoukiem i nie mam kontaktu z zawodowcami ;) W sensie - Robert sobie grzebie hobbystycznie, nie robi korpoprojektów, moim zdaniem takie zaawansowane (i zdecydowanie trudniejsze w implementacji) tematy są przerostem formy, armatą na komara itp. To jakby kupować TIRa żeby raz w roku przewieźć szafkę do ciotki. OK, można, ale czy to ma sens?

1

Tutaj jest zobrazowanie tego podejścia. https://www.finalbuilder.com/resources/blogs/a-simple-ioc-container-for-delphi Wtedy mając interfejs IMySettings kontener powinien zwracać implementacje, która będzie zachowywać te ustawienia i pewnie jedne dla całego programu(Singleton).

Z kolei jeśli masz konkretne potrzeby jak np. przechowywanie zmiennych, które są danymi - no to osobna para kaloszy - tutaj widzę wzorzec repozytorium. Repozytorium powinno gdzieś ukrywać dane. Ty powinieneś mieć interfejs repozytorium i o nie prosić (ofc. implementacje tego interfejsu repozytorium otrzymasz z DI). Samo repo zwraca obiekt po ID, lub w inny sposób (np. najnowszy pomiar) i tą część programu co używa tego repa nic więcej nie obchodzi. To już warstwa repa martwi się jak to będzie trzymać - czy zapisze na pliku, czy zapisze w statycznej tablicy, liście in memmory, bazie danych, czy zewnętrznym serwisie. Ta warstwa powinna skupić się na zachowaniu danych i ew. ich zwracaniu do użytkowników repa). Oczywiście repo może przechowywać prosty obiekt modelu, ale może też ten obiekt być wyposażony w jakieś metody spełniające jakąś wewnętrzną logikę na tym obiekcie - np. tłumaczyć pomiar na różne jednostki etc.

Reasumując ja system składał bym (i de facto składam ;p) z klocków składanych przez DI. Obiekty konfiguracyjne trzymał jako osobne implementacje, a wszystko co związane z danymi trzymał bym w repozytoriach.

2

@cerrato:

cerrato napisał(a):

god clasa sprawdzi się w małych jednoosobowych projektach i tylko tam. Każdy większy projekt będzie problemem.

No ale właśnie o coś takiego chodzio @robertz68 gdy pisał Jako że jestem samoukiem i nie mam kontaktu z zawodowcami ;)

Zgoda, ale po to pyta profesjonalistów, żeby się dowiedzieć jak robi się to w dużych firmach. W małych projektach można połapać się w spaghetii czy innym bigosie, ale po co? Mamy dwa podejścia - jedno sprawdzi się w jednym miejscu, drugie w większej ilości miejsc. No i ... widziałem zbyt wiele dużych projektów bo pracuje nie od wczoraj, które wyrastały z takich bigosów i potem był straszliwy problem z refraktorem, by godzić się na uznanie kaszanki kodowej za coś ok. Jak coś robimy i mamy tego świadomość, to róbmy to od początku dobrze.

4

@robertz68: nie ogarniam Delphi czy Pascala więc nie wiem co potrzebujesz konkretnie, ale polecam poczytać o czymś takim jak Dependency Injection (to nie to samo co konterner IoC).

0

Jedno pytanie @robertz68 - te zmienne są znane (tzn. czy można je zadeklarować w kodzie tak jak np. stałą czy klasę), czy mogą się zmieniać co do ilości, nazwy i typu danych podczas działania programu?

Co do cudownych rad o DI i IoC to...
Proszę zauważyć, że na 100 programistów Delphi, może z 10 wie o co tam chodzi i zna coś tam (bibliotekę) do tego (i nie, nie jest to mORMot - tam jest co najwyżej bardzo prosty mechanizm DI, i de-facto jest to wzorzec service locator), a 1 może nawet czasem używa.
I moim zdaniem winnym jest cały model szkolenia użytkowników Delphi - przez dekady wtłaczano nam do głów, ze RAD jest zajebisty.
A teraz dzieje się to samo, ale już nazywa się LowCode (i to ich cudownie spartolone podejście do LiveBindings, gdzie w oficjalnej dokumentacji napisano że lepiej to klikać, a nie programować bo można sobie kuku zrobić. Jprdl...).
A to jest prosta droga do klęski przy ciut bardziej złożonym projekcie.
Delphi nie jest złe, ale model szkolenia i marketing - łooo matko...

2

Moim zdaniem lepiej poczytać o tych cudownościach i zobaczyć jak reszta świata działa niż tkwić w RADowskim klikando bo można tylko czkawki dostać. Co do powodów - zgadzam się, ale rozwiązaniem nie jest ignorowanie tematu, a raczej pogłębienie wiedzy i wzorców, które można do Delphi przenieść.

3

Dodam ze Delphi mimo, że już strasznie niszowe to książki do niego nadal wychodzą, także o wzorcach projektowych:

Ta druga obecnie za $5 w promocji (jako ebook).

0

to przykład z jednego z moich programów.
Napisałem jakiś czas temu program do szybkiej sprzedaży w barze.
W trakcie logowania odczytuje z bazy dużo danych które później są potrzebne w działaniu aplikacji (id użytkownika, jego imię, nazwisko, kod, prawa dostępu itp. itd.).
Takie dane potrzebne są często, w trakcie wpłat, wypłat, sprzedaży, transakcji kartą. No naprawdę często i w dużej ilości paneli.
Od dawna robię sobie do tego rekordy aby jakoś to wszystko uporządkować, czyli mamy rekord ze zmiennymi dla użytkownika, dla aktualnej transakcji itp.
To wszystko działa w miarę przyzwoicie ale deklaracja tych zmiennych / rekordów to dziesiątki linijek. I właśnie o te linijki mi chodzi. Jak pisałem, najczęściej takie deklaracje zmiennych publicznych umieszczam w najważniejszym unicie do którego i tak większość ma jakąś referencję więc przynajmniej mam od razu do nich dostęp.
Ale jednak nie podoba mi się to. Kod w tym unicie jest bardzo długi a i tak, dużo rzeczy mam wyrzucone do osobnych unitów.
Dlatego chciałem się dowiedzieć jak robi się to w miarę profesjonalnie, czyli bez tego spaghetti.

0

Nie wiem jakie super techniki są w Delphi. Ale w C# do takich rzeczy używam zmiennych statycznych.

0

Ja bym zrobił klasę implementującą interface IUserContext, na początku bym pobierał na etapie logowania dane z bazy, tworzył obiekt tej klasy i rejestrował w kontenerze. Potem każdy konstruktor przyjmował by IUserContext i finał - zawsze dostajesz obiekt z tymi danymi. Minus - nie pobierzesz nowych danych z bazy - plus nie zmodyfikujesz uprawnień bez przelogowania (co może być minusem, w zależności od programu).

Innym sposobem jest przechowywanie w jakiejś klasie statycznej danych logowania i mieć IUserRepository i ten obiekt pobierał by z bazy na bieżąco i zwracał dane... ale nie wiem czy jest to konieczne... Te dane to bardziej dane konfiguracyjne niż biznesowe.

Można też do ich wszystkich dać zmienne statyczne jak pisze @Spine ale to się dużo nie różni od globalnych zmiennych po unitach.

Generalnie każde rozwiazanie ma plusy i minusy - trzeba by przenalizować przypadek i zdecydować.

1 użytkowników online, w tym zalogowanych: 0, gości: 1