Facebook - konwersja
Czytaj fragment
Pobierz fragment

Programowanie gier - ebook

Data wydania:
1 stycznia 2020
Format ebooka:
EPUB
Format EPUB
czytaj
na czytniku
czytaj
na tablecie
czytaj
na smartfonie
Jeden z najpopularniejszych formatów e-booków na świecie. Niezwykle wygodny i przyjazny czytelnikom - w przeciwieństwie do formatu PDF umożliwia skalowanie czcionki, dzięki czemu możliwe jest dopasowanie jej wielkości do kroju i rozmiarów ekranu. Więcej informacji znajdziesz w dziale Pomoc.
Multiformat
E-booki w Virtualo.pl dostępne są w opcji multiformatu. Oznacza to, że po dokonaniu zakupu, e-book pojawi się na Twoim koncie we wszystkich formatach dostępnych aktualnie dla danego tytułu. Informacja o dostępności poszczególnych formatów znajduje się na karcie produktu.
, MOBI
Format MOBI
czytaj
na czytniku
czytaj
na tablecie
czytaj
na smartfonie
Jeden z najczęściej wybieranych formatów wśród czytelników e-booków. Możesz go odczytać na czytniku Kindle oraz na smartfonach i tabletach po zainstalowaniu specjalnej aplikacji. Więcej informacji znajdziesz w dziale Pomoc.
Multiformat
E-booki w Virtualo.pl dostępne są w opcji multiformatu. Oznacza to, że po dokonaniu zakupu, e-book pojawi się na Twoim koncie we wszystkich formatach dostępnych aktualnie dla danego tytułu. Informacja o dostępności poszczególnych formatów znajduje się na karcie produktu.
(2w1)
Multiformat
E-booki sprzedawane w księgarni Virtualo.pl dostępne są w opcji multiformatu - kupujesz treść, nie format. Po dodaniu e-booka do koszyka i dokonaniu płatności, e-book pojawi się na Twoim koncie w Mojej Bibliotece we wszystkich formatach dostępnych aktualnie dla danego tytułu. Informacja o dostępności poszczególnych formatów znajduje się na karcie produktu przy okładce. Uwaga: audiobooki nie są objęte opcją multiformatu.
czytaj
na tablecie
Aby odczytywać e-booki na swoim tablecie musisz zainstalować specjalną aplikację. W zależności od formatu e-booka oraz systemu operacyjnego, który jest zainstalowany na Twoim urządzeniu może to być np. Bluefire dla EPUBa lub aplikacja Kindle dla formatu MOBI.
Informacje na temat zabezpieczenia e-booka znajdziesz na karcie produktu w "Szczegółach na temat e-booka". Więcej informacji znajdziesz w dziale Pomoc.
czytaj
na czytniku
Czytanie na e-czytniku z ekranem e-ink jest bardzo wygodne i nie męczy wzroku. Pliki przystosowane do odczytywania na czytnikach to przede wszystkim EPUB (ten format możesz odczytać m.in. na czytnikach PocketBook) i MOBI (ten fromat możesz odczytać m.in. na czytnikach Kindle).
Informacje na temat zabezpieczenia e-booka znajdziesz na karcie produktu w "Szczegółach na temat e-booka". Więcej informacji znajdziesz w dziale Pomoc.
czytaj
na smartfonie
Aby odczytywać e-booki na swoim smartfonie musisz zainstalować specjalną aplikację. W zależności od formatu e-booka oraz systemu operacyjnego, który jest zainstalowany na Twoim urządzeniu może to być np. iBooks dla EPUBa lub aplikacja Kindle dla formatu MOBI.
Informacje na temat zabezpieczenia e-booka znajdziesz na karcie produktu w "Szczegółach na temat e-booka". Więcej informacji znajdziesz w dziale Pomoc.
Czytaj fragment
Pobierz fragment
94,00

Programowanie gier - ebook

Największym wyzwaniem dla wielu programistów jest ukończenie pracy nad grą. Większość projektów nie dobiega końca, gdyż ich autorzy są przytłoczeni złożonością i poziomem skomplikowania kodu.
Książka Programowanie gier. Wzorce rozwiązuje ten problem. Opierając się na swoim wieloletnim doświadczeniu, autor zebrał sprawdzone wzorce projektowania gier, aby zoptymalizować proces ich tworzenia. Zostały one zorganizowane jako niezależne przepisy, dzięki czemu czytelnik może wybrać tylko te wzorce, których potrzebuje w swojej pracy.

Omówione wzorce mają sprawić, że kod będzie czystszy, łatwiejszy do zrozumienia i szybszy.

Książka jest podzielona na trzy duże części. Pierwsza stanowi wprowadzenie i nadaje ogólne ramy całej pracy. Część druga omawia kilka klasycznych i uniwersalnych wzorców programowania. W każdym rozdziale autor wypróbowuje jeden z nich i omawia jego zastosowanie w programowaniu gier. Trzecia, ostatnia część to gwóźdź programu. Przedstawia ona trzynaście wzorców projektowych niezbędnych w procesie programowania gier.

Kategoria: Gry
Zabezpieczenie: Watermark
Watermark
Watermarkowanie polega na znakowaniu plików wewnątrz treści, dzięki czemu możliwe jest rozpoznanie unikatowej licencji transakcyjnej Użytkownika. E-książki zabezpieczone watermarkiem można odczytywać na wszystkich urządzeniach odtwarzających wybrany format (czytniki, tablety, smartfony). Nie ma również ograniczeń liczby licencji oraz istnieje możliwość swobodnego przenoszenia plików między urządzeniami. Pliki z watermarkiem są kompatybilne z popularnymi programami do odczytywania ebooków, jak np. Calibre oraz aplikacjami na urządzenia mobilne na takie platformy jak iOS oraz Android.
ISBN: 978-83-01-21049-6
Rozmiar pliku: 16 MB

FRAGMENT KSIĄŻKI

Podziękowania

Słyszałem, że jedynie inni autorzy książek wiedzą, z czym wiąże się ich pisanie. Istnieje jednak i inne plemię, które dokładnie zna ciężar tego brzemienia – ci, którzy na swoje nieszczęście są w związku z piszącym. Napisałem tę książkę w czasie, który moja żona Megan skrupulatnie wykroiła dla mnie z gęstej skały życia. Zmywanie naczyń czy kąpanie dzieciaków być może nie jest „pisaniem”, jednak gdyby tego nie robiła, książka ta nigdy by nie powstała.

Prace nad tym projektem rozpoczynałem będąc programistą w Electronic Arts. Nie sądzę, aby firma wiedziała do końca, co z tym zrobić, i jestem wdzięczny Michaelowi Malone’owi, Olivierowi Nalletowi i Richardowi Wifallowi za ich wsparcie oraz szczegółowe i wnikliwe uwagi do kilku pierwszych rozdziałów.

W połowie pisania postanowiłem zrezygnować z tradycyjnego wydawcy. Wiedziałem, że stracę przez to przewodnictwo redaktora, otrzymałem jednak e-maile od dziesiątek czytelników, którzy informowali mnie, gdzie chcieliby, by dotarła ta książka. Wiedziałem, że stracę przez to korektorów, otrzymałem jednak ponad 250 zgłoszeń dotyczących błędów, które pomogły mi poprawić tekst. Wiedziałem, że stracę bodziec w postaci harmonogramu, mając czytelników klepiących mnie po plecach po skończeniu każdego rozdziału, byłem jednak mocno zmotywowany.

Nie straciłem jednak dobrego fachowca od składu. Lauren Briese pojawiła się dokładnie wtedy, gdy jej potrzebowałem i wykonała znakomitą pracę.

Nazywa się to „samodzielnym publikowaniem” (self publishing), bardziej odpowiedni byłby tu jednak termin „publikowanie społecznościowe” (crowd publishing). Pisanie może być samotnym zajęciem, nigdy jednak nie byłem sam. Nawet gdy na dwa lata zawiesiłem pracę nad tą książką, zachęty nie ustawały. Bez dziesiątek osób, które nie pozwoliły mi zapomnieć o tym, że czekają na kolejne rozdziały, nigdy bym do niej nie wrócił i jej nie skończył.

Specjalne podziękowania należą sią Colmowi Sloanowi, który dwukrotnie przestudiował każdy rozdział tej książki i dał mi mnóstwo fantastycznych rad, a wszystko to z dobroci serca. Jestem Ci winien piwo albo i dwadzieścia piw.

Moje serce przepełnia wdzięczność dla każdego, kto wysłał mi e-mail lub podzielił się komentarzem, dał kciuk w górę lub dodał do ulubionych, zatweetował lub retweetował, odezwał się do mnie, powiedział swojemu znajomemu o tej książce albo przesłał mi zgłoszenie błędu. Ukończenie tej książki było jednym z największych celów w moim życiu, a wy sprawiliście, że stało się to możliwe. Dziękuję!

Bob Nystrom

6 września 2014 r.I

Wprowadzenie

Rozdział 1: Architektura, wydajność i gry

W piątej klasie ja i moi znajomi zyskaliśmy dostęp do małej, nieużywanej sali lekcyjnej, w której znajdowało się kilka zdezelowanych komputerów TRS-80. Mając nadzieję, że nas to zainspiruje, nauczyciel odnalazł wydruk jakichś prostych programów w BASIC-u, przy których moglibyśmy pomajstrować.

Czytniki kaset magnetofonowych w komputerach były popsute, więc za każdym razem, gdy chcieliśmy uruchomić jakiś kod, musieliśmy starannie wklepywać go od zera. Sprawiło to, że preferowaliśmy programy, które miały jedynie kilka wierszy:

Być może gdyby komputer wypisał to wystarczająco dużo razy, w magiczny sposób stałoby się to prawdą.

Mimo to proces ten był pełen niebezpieczeństw. Nie znaliśmy się na programowaniu, więc drobne błędy składni były dla nas nieprzeniknione. Jeśli program nie działał – co zdarzało się często – zaczynaliśmy od początku. Z tyłu stosu stron znajdowało się prawdziwe monstrum: program, który zabierał kilka gęsto zapisanych stron kodu. Minęło trochę czasu nim nabraliśmy odwagi, aby choćby spróbować się z nim zmierzyć. Nie mogliśmy jednak mu się oprzeć – tytuł znajdujący się powyżej kodu głosił, że mamy do czynienia z programem „Tunele i Trole”. Nie mieliśmy pojęcia, na czym polegał ten program, ale brzmiało to jak jakaś gra, a co mogłoby być lepszego niż gra komputerowa, którą zaprogramowało się samemu?

Nigdy nie udało nam się jej uruchomić, a po roku wynieśliśmy się z tej sali. (Znacznie później, gdy faktycznie znałem trochę BASIC, zdałem sobie sprawę, że był to jedynie generator znaków dla gry planszowej, a nie sama gra). Kości jednak zostały rzucone – odtąd chciałem zostać programistą gier.

Gdy miałem naście lat, do mojej rodziny trafił Macintosh wyposażony w QuickBASIC, a później THINK C. Spędzałem większą część moich letnich wakacji, klecąc gry. Samodzielne uczenie się było powolne i bolesne. Uruchomienie czegoś, co działało, było łatwe – na przykład ekranu z mapą lub niewielkiej łamigłówki – ale w miarę tego, jak program się rozrastał, stawało się coraz trudniejsze. Początkowo wyzwaniem było nawet to, aby coś zaczęło działać. Następnie stawało się nim wykombinowanie, jak napisać program większy niż coś, co mogłoby zmieścić się w mojej głowie. Zamiast po prostu poczytać o tym, „Jak programować w C++”, spróbowałem poszukać książek o tym, w jaki sposób organizować programy.

Swoje wakacje wielokrotnie spędzałem też łapiąc węże i żółwie na bagnach południowej Luizjany. Gdyby na zewnątrz nie było tak piekielnie gorąco, istnieją spore szanse, że książka ta byłaby poświęcona płazom lub gadom, a nie programowaniu.

Kilka lat później mój znajomy wręczył mi książkę Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku¹. Nareszcie! Książka, której szukałem od czasów, gdy byłem nastolatkiem. Za jednym zamachem przeczytałem ją od deski do deski. Nadal zmagałem się z moimi własnymi programami, ale dostrzeżenie, że inni ludzie również zmagają się ze swoimi i dochodzą do jakichś rozwiązań, przyniosło mi ogromną ulgę. Poczułem, jakbym wreszcie miał do dyspozycji trochę narzędzi, a nie tylko nagie ręce.

Było to nasze pierwsze spotkanie i pięć minut po tym, jak zostałem przedstawiony, usiadłem na jego kanapie i spędziłem kilka następnych godzin kompletnie go ignorując i czytając. Mam nadzieję, że od tego czasu moje umiejętności społeczne nieco się poprawiły.

W 2001 roku dostałem swoją pracę marzeń: zostałem inżynierem oprogramowania w firmie Electronic Arts. Nie mogłem się doczekać, kiedy będę mógł rzucić okiem na jakieś prawdziwe gry i popatrzeć, w jaki sposób profesjonaliści składają je w całość. Jak wyglądała architektura gry tak ogromnej, jak Madden Football? W jaki sposób różne systemy wchodziły ze sobą w interakcję? W jaki sposób udawało im się sprawić, że pojedyncza baza kodu działała na wielu platformach?

Wgryzanie się w kod źródłowy było uczącym pokory i zadziwiającym doświadczeniem. Grafika, sztuczna inteligencja, animacje i efekty graficzne zawierały genialny kod. Mieliśmy ludzi, którzy wiedzieli, jak wycisnąć ostatnie cyki z procesora i dobrze je wykorzystać. Ci ludzie jeszcze przed lunchem robili rzeczy, o których nie wiedziałem nawet, że są możliwe.

Architektura, od której zależał ten genialny kod, była jednak często wynikiem namysłu już po fakcie. Ludzie ci byli tak skupieni na funkcjonalnościach, że na sposób organizacji kodu patrzono przez palce. Sprzężenia między modułami były częste. Nowe funkcjonalności były często przyśrubowane do kodu źródłowego tam, gdzie akurat dało się je wpasować. Wyglądało na to, że wielu z tych programistów nawet nie zerknęło do Wzorców projektowych, a już na pewno nie przebrnęło przez wzorzec Singleton (rozdz. „Singleton”).

W rzeczywistości nie było rzecz jasna tak źle. Wyobrażałem sobie programistów gier siedzących w jakiejś pełnej tablic suchościeralnych wieży z kości słoniowej, tygodniami spokojnie omawiających najdrobniejsze szczegóły architektury. Rzeczywistość była taka, że kod, na który patrzyłem, był napisany przez ludzi starających się dotrzymać ostatecznych terminów. Robili wszystko, co mogli, a to, co mogli zrobić, było często bardzo dobre, z czego stopniowo zdawałem sobie sprawę. Im więcej czasu spędzałem pracując nad kodem gry, tym więcej odnajdywałem genialnych elementów kryjących się pod powierzchnią.

Niestety, „kryjących się” było często trafnym opisem. W kodzie ukryte były prawdziwe perły, ale wiele osób przechodziło tuż obok nich. Widziałem współpracowników usiłujących wymyśleć na nowo dobre rozwiązania, podczas gdy przykłady tego, czego potrzebowali, znajdowały się w tej samej bazie kodu, przy której pracowali.

Jest to problem, który usiłuje rozwiązać ta książka. Wykopałem i wypolerowałem najlepsze wzorce, jakie znalazłem w grach, i przedstawiłem je tutaj, byśmy mogli poświęcać swój czas na wymyślanie nowych rzeczy, a nie na wymyślanie ich raz jeszcze.

Co nas czeka?

Powstały już dziesiątki książek o programowaniu gier. Dlaczego miałaby powstać kolejna? Większość książek poświęconych programowaniu gier, które widziałem, wpada do jednej z dwóch kategorii:

- Książki specyficzne dla określonej dziedziny. Te tytuły są poświęcone wąskim zagadnieniom i wnikają głęboko w określony aspekt programowania gier. Można się z nich dowiedzieć czegoś na temat grafiki 3D, renderowania w czasie rzeczywistym, symulowania fizyki, sztucznej inteligencji czy dźwięku. Są to obszary, w których wielu programistów zajmujących się grami specjalizuje się w miarę rozwoju swoich karier.
- Książki poświęcone całości silnika. Te dla odmiany próbują objąć wszystkie najróżniejsze elementy całości silnika gry. Zorientowane są na budowę kompletnego silnika dostosowanego do jakiegoś konkretnego gatunku gier, zwykle pierwszoosobowych strzelanek 3D.

Lubię oba te rodzaje książek, myślę jednak, że zostawiają one pewną lukę. Publikacje specyficzne dla tej dziedziny rzadko przedstawiają, w jaki sposób ten spory kawałek kodu wchodzi w interakcje z resztą gry. Możemy być czarodziejami od fizyki czy renderingu, ale czy wiemy, w jaki sposób zręcznie powiązać te kwestie ze sobą?

Druga kategoria książek obejmuje te kwestie, często jednak są one zbyt monolityczne i ograniczone do konkretnego gatunku. Zwłaszcza wraz z powstaniem gier mobilnych i rekreacyjnych znaleźliśmy się w czasach, w których tworzonych jest wiele różnych gatunków gier. Dziś nie jest już tak, że wszyscy klonujemy Quake’a. Książki, które przeprowadzają nas przez pojedynczy silnik nie pomogą nam, jeśli nasza gra nie będzie do niego pasować.

Zamiast tego, to, co próbuję tu zrobić, jest bardziej à la carte. Każdy z rozdziałów tej książki opisuje odrębny pomysł, który możemy wykorzystać, pracując nad swoim kodem. Możemy wymieszać je i dopasować w sposób, który najlepiej sprawdzi się w przypadku gry, którą my chcemy stworzyć.

Inny przykład tego stylu à la carte stanowi ukochana przez wielu seria Game Programming Gems.

W jaki sposób wiąże się to z wzorcami projektowymi?

Każda książka dotycząca programowania mająca w tytule słowo „wzorce” w jasny sposób nawiązuje w jakiś sposób do klasycznej pozycji Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, autorstwa Ericha Gammy, Richarda Helma, Ralpha Johnsona i Johna Vlissidesa (złowieszczo nazywanych „Bandą czterech”).

Sama książka Wzorce projektowe była z kolei zainspirowana inną książką. Pomysł wykonania języka wzorców w celu opisania otwartych rozwiązań problemów pochodzi z książki A Pattern language Chrisophera Alexandra (oraz Sarah Ishikawy i Murraya Silversteina).

Nazywając tę książkę Programowanie gier. Wzorce, nie próbuję sugerować, że książki napisanej przez Bandę czterech nie można zastosować do gier. Przeciwnie: część „Wzorce projektowe raz jeszcze” niniejszej książki obejmuje wiele wzorców z Wzorców projektowych, kładąc jednak nacisk na to, w jaki sposób można je zastosować do programowania gier.

Ich książka była poświęcona architekturze (takiej prawdziwej architekturze, z budynkami, ścianami i tak dalej), mieli oni jednak nadzieję, że inni wykorzystają tę samą strukturę do opisania rozwiązań w innych obszarach. Wzorce projektowe to próba podjęta przez Bandę czterech, której celem było zrobienie tego w odniesieniu do oprogramowania.

Myślę, że jest odwrotnie i ta książka znajdzie zastosowanie również w przypadku oprogramowania innego niż gry. Również dobrze mógłbym ją zatytułować Jeszcze więcej wzorców projektowych, myślę jednak, że gry dostarczają bardziej interesujących przykładów. Czy naprawdę chcielibyśmy przeczytać jeszcze jedną książkę o ewidencji pracowników i kontach bankowych? Choć przedstawione tu wzorce są użyteczne w przypadku innych rodzajów oprogramowania, myślę jednak, że szczególnie dobrze pasują do wyzwań inżynieryjnych pojawiających się często w przypadku gier:

- Czas i sekwencjonowanie stanowią często kluczowy składnik architektury gry. Rzeczy muszą się dziać w odpowiedniej kolejności i w odpowiednim czasie.
- Cykle deweloperskie są bardzo napięte, a niektórzy programiści muszą być w stanie szybko zbudować i iterować na dużym zbiorze różnych zachowań, bez nadeptywania sobie wzajemnie na palce czy zostawiania śladów w całej bazie kodu.
- Gdy całe zachowanie jest już zdefiniowane, zaczyna wchodzić w interakcje. Potwory kąsają bohatera gry, eliksiry mieszają się ze sobą, a bomby niszczą tak wrogów, jak i przyjaciół. Te interakcje muszą zachodzić, nie powodując, że baza kodu stanie się splątanym kłębkiem wełny.
- I wreszcie: w przypadku gier wydajność jest krytyczna. Programiści gier uczestniczą w bezustannym wyścigu o to, kto wyciśnie najwięcej ze swojej platformy. Triki służące zaoszczędzeniu kilku cykli mogą stanowić o różnicy między chwaloną przez wszystkich grą i milionami sprzedanych egzemplarzy a utraconymi klatkami i rozzłoszczonymi recenzentami.

Jak czytać tę książkę?

Książka Programowanie gier. Wzorce jest podzielona na trzy duże części. Pierwsza z nich stanowi wprowadzenie i nadaje ogólne ramy całej książce. Składa się na nią ten rozdział oraz rozdział kolejny.

Druga część zatytułowana „Wzorce projektowe raz jeszcze” omawia kilka wzorców z książki autorstwa Bandy czterech. W każdym rozdziale wypróbowuję jakiś wzorzec i omawiam to, jak ma się on do programowania gier.

Ostatnia trzecia część to gwóźdź programu. Przedstawia ona trzynaście wzorców projektowych, o których przekonałem się, że są przydatne. Są one pogrupowane w cztery kategorie: „Wzorce sekwencyjne”, „Wzorce zachowania”, „Wzorce oddzielania” oraz „Wzorce optymalizacji”.

Każdy z tych wzorców jest opisany za pomocą jednolitej struktury, tak aby można było wykorzystać tę książkę w charakterze materiału źródłowego i szybko znaleźć to, co jest potrzebne.

- Podrozdział „Motywacja” zawiera opis przykładowego problemu, do którego wzorzec zostanie zastosowany. W odróżnieniu od konkretnych algorytmów, wzorzec pozostaje zwykle pozbawiony formy dopóty, dopóki nie zastosuje się go do jakiegoś konkretnego problemu.
- Podrozdział „Wzorzec” wydobywa istotę przykładowego wzorca użytego we wcześniejszym podrozdziale. Jeśli potrzebujemy jego suchego, podręcznikowego opisu – oto on. Będzie to również dobre utrwalenie, jeśli znamy już wzorzec i chcemy upewnić się, że nie zapomnieliśmy o żadnym składniku.
- Do tej pory wzorzec został wyjaśniony jedynie w kategoriach pojedynczego przykładu. Skąd jednak wiadomo, że sprawdzi się w odniesieniu do naszego problemu? Podrozdział „Kiedy z niego korzystać” zawiera wskazówki dotyczące tego, w jakich okolicznościach wzorzec jest użyteczny, a kiedy najlepiej jest go unikać. Podrozdział „Do zapamiętania” zwraca uwagę na konsekwencje i ryzyka, jakie niesie ze sobą wykorzystanie wzorca.
- Jeśli chcielibyście coś zrozumieć, potrzebujecie konkretnych przykładów – tak jak często dzieje się to ze mną – podrozdział „Przykładowy kod” jest dla nas. Przeprowadza on krok po kroku przez pełną implementację wzorca, tak byśmy mogli dokładnie zobaczyć, w jaki sposób on działa.
- Wzorce różnią się od pojedynczych algorytmów, ponieważ mają charakter otwarty. Za każdym razem, gdy korzystamy z wzorca, prawdopodobnie zaimplementujemy go inaczej. Kolejny podrozdział, „Decyzje projektowe”, zgłębia ten obszar i pokazuje różne możliwości, które powinniśmy wziąć pod uwagę, stosując wzorzec.
- Krótki podrozdział „Patrz również” służy podsumowaniu oraz pokazuje, w jaki sposób dany wzorzec wiąże się z innymi, a także wskazuje rzeczywiste przykłady kodu open source, w którym są one wykorzystywane.

Kilka słów o przykładowym kodzie

Przykładowy kod w tej książce jest napisany w C++, nie należy jednak z tego wnosić, że wzorce te znajdują zastosowanie jedynie w tym języku, ani że język C++ nadaje się do tego lepiej od innych. Prawie każdy język dobrze się sprawdzi, choć niektóre wzorce wydają się zakładać, że język, którym się posługujemy, uwzględnia obiekty i klasy.

Wybrałem C++ z kilku powodów. Po pierwsze, jest to najpopularniejszy język tworzenia komercyjnych gier. To uniwersalny język całej branży. Co więcej, składnia C, na której opiera się C++, stanowi również podstawę języków Java, C#, JavaScript i wielu innych. Nawet jeśli nie znamy C++, istnieją spore szanse, że przy odrobinie wysiłku będziemy w stanie zrozumieć zawarte tu próbki kodu.

Nauczenie C++ nie jest celem tej książki. Przykłady zostały dobrane tak, aby były tak proste, jak to możliwe i nie są reprezentatywne dla dobrego stylu korzystania z C++. Należy je czytać, zwracając uwagę na ideę, jaką wyrażają, a nie na wyrażający ją kod.

W szczególności kod ten nie został napisany we „współczesnym” stylu charakterystycznym dla C++ w wersji 11 lub nowszej. Nie korzysta on ze standardowej biblioteki i jedynie z rzadka wykorzystuje szablony. Sprawia to, że powstaje „kiepski” kod C++, mam jednak nadzieję, że dzięki tym ograniczeniom będzie on bardziej przystępny dla osób korzystających na co dzień z C, Objective-C, Javy i innych języków.

Aby uniknąć marnowania miejsca na kod, który już widzieliśmy lub który nie ma znaczenia dla danego wzorca, fragmenty kodu zostaną niekiedy pominięte w przykładach. W takich przypadkach miejsca, w którym znajduje się brakujący fragment kodu, w przykładowym kodzie zostanie oznaczony wielokropkiem.

Rozważmy funkcję, która będzie coś robić, a następnie zwracać jakąś wartość. Z punktu widzenia wyjaśnianego wzorca istotna jest tylko zwracana wartość, a nie to, co robi ta funkcja. W takim przypadku przykładowy kod będzie wyglądać tak:

Co dalej?

Wzorce to bezustannie rozwijający się i zmieniający element tworzenia oprogramowania. Niniejsza książka stanowi kontynuację procesu rozpoczętego przez Bandę czterech, polegającego na dokumentowaniu i dzieleniu się napotkanymi wzorcami oprogramowania, który to proces będzie trwać jeszcze po tym, jak wyschnie tusz na tych kartkach.

Jesteście kluczowym elementem tego procesu. Opracowując swoje własne wzorce i doskonaląc (lub odrzucając!) wzorce opisane w tej książce, przyczyniacie się do rozwoju społeczności pracującej nad oprogramowaniem. Jeśli chcecie podzielić się swoimi sugestiami, poprawkami lub innego rodzaju uwagami dotyczącymi zagadnień opisanych w tej książce – bądźmy w kontakcie!1

Architektura, wydajność i gry

Przyszło mi do głowy, że nim skoczymy na główkę do basenu pełnego wzorców, dobrym pomysłem mogłoby być przedstawienie kontekstu w postaci moich przemyśleń dotyczących architektury oprogramowania i jej zastosowań w świecie gier. Pomoże to lepiej zrozumieć dalsze rozdziały tej książki. Nawet jeśli nie przyda nam się to w żadnym innym celu, dostaniemy nieco amunicji pozwalającej nam toczyć spory o to, jak okropne (lub jak wspaniałe) są wzorce projektowe i architektura oprogramowania.

Zauważmy, że nie zakładam, po której stronie tego sporu mielibyśmy stanąć. Jak każdy handlarz bronią, oferuję towar wszystkim.

Czym jest architektura oprogramowania?

Jeśli przeczytamy tę książkę od początku do końca, nie poznamy algebry liniowej stojącej za grafiką 3D ani analizy matematycznej będącej podstawą fizyki gier. Nie dowiemy się też, w jaki sposób przyciąć drzewo poszukiwań naszej sztucznej inteligencji za pomocą algorytmu alfa-beta ani jak zasymulować pogłos w jakimś pokoju w trakcie odtwarzania dźwięków.

Oj! Ten akapit byłby fatalną reklamą tej książki.

Zamiast tego, książka ta poświęcona jest kodowi znajdującemu się pomiędzy tym wszystkim. W mniejszym stopniu dotyczy pisania kodu niż tego, w jaki sposób powinien on być zorganizowany. Każdy program jest zorganizowany w jakiś sposób, nawet jeśli jest to po prostu zasada „wpakujmy wszystko do main() i zobaczmy, co się stanie”. Myślę więc, że bardziej interesujące będą rozważania dotyczące tego, na czym polega dobra organizacja. W jaki sposób odróżnić dobrą architekturę od złej?

Rozmyślałem nad tym pytaniem przez około pięć lat. Tak jak każdy z nas, posiadam jakąś intuicję dotyczącą tego, czym charakteryzuje się dobry projekt. Wszyscy musieliśmy borykać się z bazą kodu tak złą, że najlepsze, co mogliśmy dla niej zrobić, to zlikwidować ją, skracając jej cierpienia.

Przyznajmy wprost, większość z nas jest odpowiedzialna za kilka takich przypadków.

Kilku szczęśliwców zgromadziło przeciwne doświadczenia, mając okazję pracować z pięknie zaprojektowanym kodem – z bazami kodu, które sprawiały wrażenie, jakby były doskonale urządzonymi, luksusowymi hotelami pełnymi lokajów gotowych gorliwie spełnić każdą naszą zachciankę. Na czym polega różnica między nimi?

Czym jest dobra architektura oprogramowania?

Według mnie projekt jest dobry, gdy dokonując jakiejś zmiany, mam wrażenie, że cały program został skrojony tak, jak gdyby jej oczekiwał. Mogę rozwiązać zadanie za pomocą tylko kilku wywołań funkcji wyboru, które pasują tak doskonale, że nie pozostawiają nawet najmniejszej zmarszczki na idealnie gładkiej tafli kodu.

Ładnie to brzmi, ale trudno przełożyć to na działania. „Po prostu napisz kod tak, by zmiany nie deformowały jego gładkiej tafli”. Tak, jasne...

Pozwólcie, że podzielę to na nieco mniejsze fragmenty. Pierwszy istotny element to to, że w architekturze chodzi o zmianę. Ktoś musi modyfikować bazę kodu. Jeśli nikt go nie tyka – czy to dlatego że jest już doskonały i kompletny, czy dlatego że jest tak nędzny, że nikt nie chce kalać nim swego edytora kodu – to, jak jest zaprojektowany, nie ma znaczenia. Miarą jakości projektu jest to, jak łatwo dostosowuje się do zmian. Jeśli do zmian nie dochodzi, jest jak sprinter, który nigdy nie opuszcza bloków.

Jak dokonujemy zmian?

Zanim będziemy mogli zmienić kod w celu dodania nowej funkcjonalności, naprawienia błędu czy z dowolnego innego powodu, który sprawił, że odpaliliśmy nasz edytor, musimy zrozumieć, co robi istniejący kod. Nie musimy rzecz jasna znać całego programu, ale musimy załadować wszystkie jego istotne fragmenty do naszego mózgu ssaka naczelnego.

To dziwne, ale jest to dosłownie proces OCR.

Zwykle staramy się prześliznąć się przez ten krok, ale często jest to najbardziej czasochłonny element programowania. Jeśli uważamy, że stronicowanie jakiejś ilości danych z dysku do pamięci RAM jest powolne, spróbujmy przestronicować je do naszego mózgu za pośrednictwem pary nerwów wzrokowych. Kiedy cały poprawny kontekst znajdzie się już w naszej pamięci, zastanawiamy się przez moment i dochodzimy do rozwiązania. Czasem możemy przez dłuższą chwilę poruszać się tam i z powrotem, ale często jest to proste. Kiedy już zrozumiemy problem i fragmenty kodu, których dotyczy, samo kodowanie okazuje się trywialne.

Przez chwilę stukamy naszymi mięsistymi palcami w klawiaturę, aż na ekranie pokażą się światełka we właściwych kolorach. I po wszystkim, prawda? Chwila, moment! Zanim napiszemy testy i prześlemy kod do inspekcji, często czeka nas trochę czyszczenia.

Czy powiedziałem „testy”? O, rzeczywiście! Ciężko jest napisać testy jednostkowe jakiegoś kodu gry, ale duża część bazy kodu znakomicie nadaje się do testowania.

Upchnęliśmy w naszej grze jeszcze trochę kodu, ale nie chcemy, aby kolejna osoba, która się tu pojawi, potknęła się o zmarszczki, które pozostawiliśmy w źródle. Wyłączywszy przypadki minimalnych zmian, chcąc zintegrować nasz kod i wygładzić cały program, czeka nas odrobina reorganizacji. Jeśli zrobimy to poprawnie, kolejna pojawiająca się osoba nie będzie w stanie stwierdzić, kiedy został napisany jakiś wiersz kodu.

Nie będę się nad tym w tym miejscu rozwodzić, ale naprawdę warto w większym stopniu polegać na testach automatycznych (jeśli jeszcze tego nie robimy). Czy naprawdę nie mamy nic lepszego do roboty i za każdym razem chcemy sprawdzać różne rzeczy ręcznie?

Krótko mówią, diagram przepływu dla programowania wygląda jakoś tak:

Rysunek 1.1. Dzień pracy programisty w pigułce

Teraz, gdy o tym myślę, fakt, że z tej pętli nie ma wyjścia, wydaje mi się odrobinę niepokojący.

W jaki sposób oddzielanie może pomóc?

Choć nie jest to oczywiste, sądzę, że architektura oprogramowania zależy przede wszystkim od fazy poznawania kodu. Załadowanie kodu do neuronów jest tak okropnie wolne, że opłaca się poszukać strategii redukujących jego objętość. Cała osobna część tej książki jest poświęcona wzorcom służącym oddzielaniu, a duża część książki Wzorce projektowe jest poświęcona temu samemu.

„Oddzielanie” możemy zdefiniować na wiele sposobów, jednak moim zdaniem jeśli dwa fragmenty kodu są sprzężone, to znaczy że nie można zrozumieć jednego z nich bez zrozumienia drugiego. Jeśli je oddzielimy, o każdym z nich możemy rozumować niezależnie. To świetna sprawa, ponieważ jeśli nasz problem dotyczy tylko jednego z nich, będziemy musieli załadować do naszego małpiego mózgu tylko jeden fragment, a drugiego już nie.

Jeśli chodzi o mnie, jest to kluczowe zadanie stojące przed architekturą oprogramowania: zminimalizować ilość wiedzy, jaką musimy mieć w głowie, nim będziemy mogli posunąć sprawy do przodu.

W grę wchodzą oczywiście również późniejsze stadia. Inna definicja oddzielania skupia się na tym, aby zmiana w jednym fragmencie kodu nie wymuszała zmian w innym. Jasne, trzeba będzie coś zmienić, ale im mniej mamy sprzężeń, tym mniej zmarszczek pojawi się za sprawą tych zmian w innych miejscach tafli naszej gry.

Jakim kosztem?

Brzmi świetnie, prawda? Oddzielimy wszystko, co trzeba, a będziemy w stanie kodować niczym wicher. Każda zmiana będzie się wiązać z modyfikacją tylko jednej lub dwóch wybranych metod, a my będziemy mogli tańczyć na tafli kodu źródłowego, nie zostawiając nawet cienia śladu.

To uczucie sprawia, że ludzie tak ekscytują się abstrakcjami, modularnością, wzorcami projektowymi i architekturą oprogramowania. Praca nad programem o dobrej architekturze to naprawdę radosne doświadczenie, a każdy lubi być bardziej produktywny. Dobra architektura ma ogromny wpływ na produktywność. Trudno przecenić jak głęboki może on być.

Jak wszystko w życiu, ma to jednak swoją cenę. Dobra architektura wymaga prawdziwego wysiłku i dyscypliny. Za każdym razem, gdy dokonujemy jakiejś zmiany lub implementujemy jakąś funkcjonalność, musimy się ciężko napracować, aby zgrabnie zintegrować ją z resztą programu. Musimy starannie zadbać zarówno o dobrą organizację kodu źródłowego, jak i o to, by utrzymać ten stan w trakcie dokonywania tysięcy drobnych zmian składających się na cykl produkcyjny oprogramowania.

Druga część – poświęcona utrzymaniu naszego projektu – zasługuje na szczególną uwagę. Widziałem wiele programów, których początki były piękne, a które następnie dogorywały w wyniku tysiąca drobnych cięć w miarę jak programiści raz za razem dodawali „jeszcze tylko jeden, malutki kawałek”.

Musimy się zastanowić, między którymi częściami programu powinny zostać oddzielone zależności i w tych miejscach wprowadzić abstrakcje. Podobnie musimy określić, gdzie powinna zostać uwzględniona możliwość rozszerzenia, tak by przyszłe zmiany były łatwiejsze do wprowadzenia.

Jak w ogrodnictwie – nie wystarczy posadzić nowe rośliny, trzeba je również przycinać i odchwaszczać.

Ludzie naprawde się tym ekscytują. Wyobrażają sobie, że w przyszłości programiści (albo po prostu oni sami) zagłębiają się w bazę kodu i orientują się, że jest ona otwarta, potężna i wprost prosi się o to, by ją rozszerzyć. Wyobrażają sobie „Silnik Gry nad Wszystkie Inne Silniki”.

Ale to właśnie tu zaczynają się schody. Zawsze gdy dodajemy warstwę abstrakcji lub miejsce wspierające rozszerzalność, spekulujemy, że w przyszłości ta elastyczność będzie nam potrzebna. Zwiększamy objętość oraz złożoność kodu naszej gry, co wymaga poświęcenia czasu na zaprogramowanie, usunięcie usterek i utrzymanie.

Wysiłek się opłaci, jeśli zgadniemy poprawnie i w przyszłości wrócimy do tego kodu. Przewidywanie przyszłości jest jednak trudne, a gdy taka modułowość koniec końców nie okaże się pomocna, szybko zaczyna aktywnie szkodzić. Ostatecznie jest to dodatkowy kod, z którym musimy sobie radzić.

Niektórzy ukuli termin „YAGNI” od angielskiego „You aren’t gonna need it” (Nie będzie ci to potrzebne) i stosują go jak mantrę, aby zwalczyć pragnienie spekulowania o tym, czego może potrzebować nasze przyszłe „ja”.

Gdy ludzie zaczynają podchodzić do tego zbyt gorliwie, dostajemy bazę kodu, której architektura wymknęła się spod kontroli. Wszędzie mamy do czynienia z interfejsami i abstrakcjami. Systemy wtyczek, abstrakcyjne klasy bazowe, obfitość metod wirtualnych i wszelkiego rodzaju punkty rozszerzenia.

Poruszanie się po całym tym rusztowaniu zajmie nam wieczność, nim znajdziemy jakiś rzeczywisty kod, który faktycznie coś robi. Gdy musimy coś zmienić – jasne, pewnie znajdzie się interfejs, który nam w tym pomoże. Powodzenia w jego odszukaniu! W teorii całe to oddzielanie znaczy, że mamy mniej kodu, który musimy zrozumieć, zanim będziemy mogli go rozszerzyć, ale przecież same warstwy abstrakcji też wypełniają nasz mentalny dysk.

Tego typu bazy kodu sprawiają, że wiele osób krzywo patrzy na architekturę oprogramowania, a na wzorce projektowe w szczególności. Łatwo jest tak mocno zaplątać się w sam kod, że tracimy z oczu to, że próbujemy dostarczyć na rynek jakąś grę. Syreni śpiew rozszerzalności wciąga niezliczonych programistów, którzy spędzają lata, pracując nad „silnikiem”, nie rozumiejąc nawet, czemu ten silnik ma służyć.

Wydajność i szybkość

Istnieje jeszcze inny rodzaj krytyki architektury oprogramowania i abstrakcji, o którym niekiedy się słyszy, zwłaszcza w świecie programistów gier. Głosi on, że szkodzą one ich wydajności. Wiele wzorców, które uelastyczniają nasz kod, zależy od wirtualnych dyspozytorów, interfejsów, wskaźników, wiadomości i innych mechanizmów. Wszystkie one generują jakiś koszt w postaci czasu wykonania.

Interesujący kontrprzykład stanowią szablony w C++. Metaprogramowanie szablonów może niekiedy dać nam abstrakcję od interfejsów bez negatywnych konsekwencji po stronie czasu wykonania. Istnieje tu całe spektrum elastyczności. Gdy piszemy kod wywołujący konkretną metodę w jakiejś klasie, określamy tę klasę w momencie pisania kodu – na sztywno zakodowaliśmy, którą klasę wywołujemy. Gdy korzystamy z metody wirtualnej lub interfejsu, klasa, która zostaje wywołana, jest znana dopiero w momencie wykonania. Jest to rozwiązanie znacznie elastyczniejsze, ale zakłada, że istnieją pewne rezerwy po stronie czasu wykonania. Metaprogramowanie szablonów znajduje się gdzieś pośrodku. W tym przypadku decyzję o tym, którą klasę wywołać, podejmujemy w momencie kompilacji, gdy tworzona jest instancja szablonu.

Istnieje po temu powód. W architekturze oprogramowania w dużej mierze chodzi o to, aby nasze programy były bardziej elastyczne, a ich modyfikacja wymagała mniejszego wysiłku. Znaczy to, że w programie zakodowane zostanie mniej założeń. Korzystamy z interfejsów, by nasz kod działał z dowolną klasą, która go implementuje, a nie tylko z tą jedną, która pozwala na to dzisiaj. Korzystamy z obserwatorów (rozdz. „Obserwator”) i komunikatów (rozdz. „Kolejka zdarzeń”), by pozwolić dwóm elementom gry na rozmowę ze sobą po to, aby potem z łatwością mogły to robić trzy lub cztery.

Wydajność to natomiast w całości kwestia założeń. W praktyce optymalizacja rozkwita dzięki konkretnym ograniczeniom. Czy możemy bezpiecznie założyć, że nigdy nie będziemy mieli więcej niż 256 oponentów? Świetnie, możemy wsadzić ID do pojedynczego bajta. Czy będziemy tu wywoływać jakąś metodę na jednym konkretnym typie? Świetnie, możemy to statycznie wyekspediować lub skorzystać z konstrukcji inline. Czy wszystkie jednostki będą tej samej klasy? Świetnie, możemy z nich utworzyć sympatyczną, ciągłą tablicę (rozdz. „Lokalizacja danych”).

Nie znaczy to, że elastyczność jest czymś złym! Pozwala nam szybko wprowadzić zmiany do naszej gry, a szybkość rozwoju oprogramowania jest absolutnie kluczowa, jeśli chcemy stworzyć produkt pozwalający na dobrą zabawę. Nikt, nawet Will Wright, nie może opracować zrównoważonego projektu gry na papierze. Wymaga to wielu iteracji i eksperymentowania. Im szybciej jesteśmy w stanie wypróbowywać pomysły i sprawdzać, jakie dają odczucia, tym więcej rzeczy możemy wypróbować i tym większe są szanse na to, że trafimy na coś fantastycznego. Nawet gdy znajdziemy już prawidłową mechanikę, mnóstwo czasu zajmie nam dostrajanie. Drobne zaburzenie równowagi może zniweczyć frajdę z gry.

Nie ma tu łatwej odpowiedzi. Aby sprawić, żeby nasz program stał się bardziej elastyczny, dzięki czemu łatwiej będzie prototypować, będziemy musieli ponieść pewien koszt po stronie wydajności. Podobnie optymalizacja kodu sprawi, że będzie on mniej elastyczny.

Z mojego doświadczenia wynika jednak, że łatwiej jest zwiększyć szybkość działania gry sprawiającej frajdę, niż sprawić, aby szybka gra zaczęła ją sprawiać. Możliwy kompromis polega na utrzymywaniu elastyczności kodu do czasu aż projekt się ustabilizuje i usunięciu kilku abstrakcji później w celu zwiększenia wydajności.

Co jest dobrego w kiepskim kodzie?

Prowadzi nas to do kolejnej kwestii: jest czas i miejsce na różne style kodowania. Większa część tej książki jest poświęcona tworzeniu łatwego w utrzymaniu, czystego kodu, więc moje przywiązanie do robienia tego we „właściwy” sposób jest jasne, niemniej niedbały kod również ma pewną wartość.

Pisanie kodu o dobrej architekturze wymaga starannego przemyślenia i ma swoje przełożenie na czas. Więcej nawet – utrzymanie dobrej architektury w trakcie życia projektu wymaga mnóstwa wysiłku. Musimy traktować naszą bazę kodu tak, jak dobry harcerz traktuje swoje obozowisko, zawsze próbując zostawić ją po sobie w trochę lepszym stanie, niż ją zastaliśmy. Ma to sens, jeśli przez długi czas będziemy żyć z tym kodem i nad nim pracować. Tak jak wspomniałem wcześniej, projektowanie gier wymaga jednak mnóstwa eksperymentów i poszukiwań. Szczególnie w początkowym stadium pisanie kodu, o którym wiadomo, że ostatecznie trafi do kosza, jest czymś powszechnym.

Jeśli po prostu chcemy dowiedzieć się, czy jakiś pomysł dotyczący rozgrywki w ogóle ma sens, pieczołowita dbałość o jego architekturę znaczy, że poświecimy więcej czasu, nim rzeczywiście pojawi się on na ekranie i będziemy w stanie uzyskać jakąś informację zwrotną. Jeśli okaże się, że pomysł się nie sprawdza, czas poświęcony na stworzenie eleganckiego kodu, który usuniemy, będzie stracony.

Prototypowanie – wklepanie kodu, który z ledwością działa wystarczająco dobrze, by można było uzyskać odpowiedź na pytanie projektowe – to całkowicie zasadna praktyka programistyczna. Istnieje jednak bardzo poważne zastrzeżenie. Jeśli piszemy niskiej jakości kod jednorazowego użytku, musimy mieć pewność, że będziemy w stanie go usunąć. Widziałem, jak kiepscy menedżerowie raz za razem grali w następującą gierkę:

Szef: „Hej, mamy ten pomysł, który chcielibyśmy wypróbować. Chodzi nam tylko o prototyp, więc nie musisz tego robić porządnie. Jak szybko mógłbyś to złożyć do kupy?”.

Programista: „Cóż, jeśli przytnę tu i tam, podaruję sobie testy, odpuszczę dokumentację i przymknę oko na tony błędów, wstępny kod mogę dostarczyć w ciągu kilku dni”.

Szef: „Znakomicie!”.

Mija kilka dni:

Szef: „Hej, ten prototyp jest świetny! Czy mógłbyś poświęcić kilka godzin na jego lekkie doczyszczenie, tak żebyśmy mieli już gotowy produkt?”.

Musimy się upewnić, że ludzie korzystający z jednorazowego kodu rozumieją, że nawet jeśli wygląda, jakby działał, nie można go zachować i trzeba go przepisać. Jeśli istnieje szansa, że koniec końców zostanie on z nami na dłużej, w ramach samoobrony możemy napisać go od razu porządnie.

Istnieje jeden trik gwarantujący, że nasz prototypowy kod nie będzie musiał stać się prawdziwym kodem. Polega on na napisaniu go w języku innym niż język wykorzystywany przez naszą grę. Dzięki temu trzeba będzie go przepisać, zanim trafi do rzeczywistej gry.

Osiąganie równowagi

W naszym polu oddziałuje kilka sił:

- Chcemy mieć porządną architekturę tak, aby kod był łatwiejszy do zrozumienia w trakcie życia projektu.
- Chcemy dobrej wydajności i krótkiego czasu wykonania.
- Chcemy szybko uporać się z przewidzianymi na dziś funkcjonalnościami.

Myślę, że to ciekawe, że wszystkie one w jakiś sposób dotyczą szybkości: tempa, w jakim programujemy w długiej perspektywie czasu, szybkości, z jaką wykonuje się gra i naszego tempa programowania w krótkiej perspektywie czasu.

Cele te przynajmniej do pewnego stopnia są sobie przeciwstawne. Dobra architektura zwiększa produktywność w długim okresie, ale jej utrzymanie znaczy, że każda zmiana wymaga dodatkowej odrobiny wysiłku, aby utrzymać porządek.

Implementacja, która jest najszybsza do napisania, rzadko jest tą, która działa najszybciej. Z kolei optymalizacja wymaga poświęcenia dużej ilości czasu na prace inżynieryjne. Gdy już dobiegnie końca, sprawia zazwyczaj, że baza kodu tężeje: dobrze zoptymalizowany kod jest nieelastyczny i bardzo trudny do zmiany.

Zawsze istnieje presja, aby skończyć to, co trzeba skończyć dzisiaj, a o całą resztę pomartwić się jutro. Jednak jeśli będziemy dorzucać funkcjonalności tak szybko, jak jesteśmy w stanie, w naszej bazie kodu zacznie panować bałagan i pojawi się mnóstwo hacków, błędów oraz niespójności, które osłabią naszą produktywność w przyszłości.

Nie ma tu prostej odpowiedzi – tylko „coś za coś”. Z e-maili, które do mnie docierają, wnoszę, że wiele osób to zniechęca. W szczególności nowicjusze, którzy po prostu chcą zrobić jakąś grę, mogą być nieco przestraszni, gdy usłyszą, iż „nie ma dobrej odpowiedzi, tylko różne odcienie złych”.

Dla mnie jest to jednak ekscytujące! Spójrz na dowolną dziedzinę, w której ludzie doskonalą się przez całą swoją karierę, a w jej centrum zawsze znajdziemy jakiś zbiór powiązanych ze sobą ograniczeń. Przecież jeśli istniałoby jakieś proste rozwiązanie, wszyscy po prostu by z niego skorzystali. Dziedzina, którą możemy opanować w tydzień w ostateczności jest nudna. Nie słyszy się o ludziach, którzy zrobiliby nieprzeciętne kariery na kopaniu rowów.

Może inni tak zrobili, ale ja nie zgłębiałem tej analogii. Z tego, co wiem, mogą istnieć gorliwi hobbyści zajmujący się kopaniem rowów, konwencje dotyczące kopania rowów i cała otaczająca je subkultura. Kimże jestem, aby to oceniać?

Jak dla mnie, ma to wiele wspólnego z samymi grami. Gry takiej jak szachy nie da się opanować w doskonały sposób, ponieważ wszystkie figury są ze sobą tak idealnie zrównoważone. Znaczy to, że możemy spędzić życie, zgłębiając rozległe przestrzenie możliwych strategii. Źle zaprojektowana gra wali się za sprawą jednej zwycięskiej taktyki, którą możemy stosować raz za razem, aż nam się to znudzi i damy sobie spokój.

Prostota

Ostatnimi czasy mam poczucie, że jeśli istnieje jakakolwiek metoda, która jest w stanie coś poradzić na te ograniczenia, to jest nią prostota. Obecnie za pomocą kodu usilnie staram się napisać najczystsze, najbardziej bezpośrednie rozwiązanie danego problemu. Po zapoznaniu się z kodem tego rodzaju, dokładnie rozumiemy, co on robi i nie jesteśmy w stanie wyobrazić sobie żadnego innego rozwiązania.

Staram się poprawnie ująć struktury danych i algorytmy (mniej więcej w tej kolejności), a następnie ruszyć dalej. Zauważyłem, że jeśli jestem w stanie zrobić coś w prosty sposób, powstaje mniej kodu. Znaczy to, że mniej kodu musi zostać załadowane do mojej głowy, aby go zmienić. Często działa on szybko, ponieważ po prostu jest tu niewielki narzut i niewiele kodu do wykonania. (Choć z pewnością nie zawsze tak jest. W niewielkiej ilości kodu można upakować sporo pętli i rekursji).

Zauważmy jednak, że nie twierdzę, że napisanie prostszego kodu zabiera mniej czasu. Można by sądzić, że tak jest, ponieważ finalnie dostajemy mniej kodu, ale dobre rozwiązanie nie powstaje dzięki przyrastaniu kodu, ale dzięki jego destylacji.

Blaise Pascal zakończył jeden ze swych znanych listów w następujący sposób: „List ten jest dłuższy jedynie dzięki temu, iż nie miałem czasu napisać go krócej”².

Inny cytat pochodzi od Antoine’a de Saint-Exupery’ego: „Wydaje się, że doskonałość osiąga się nie wtedy, kiedy nie można już nic dodać, ale raczej wtedy, gdy nie można nic ująć”³.

Mówiąc wprost, zauważyłem, że za każdym razem, gdy wracam do któregoś rozdziału tej książki, staje się on krótszy. Niektóre rozdziały stają się krótsze o 20%, nim zostaną ukończone.

Rzadko stawia się przed nami elegancki problem. Zamiast tego dostajemy stertę przypadków użycia. Chcemy, aby X robiło Y, gdy Z, a W, gdy A i tak dalej. Innymi słowy, mamy do czynienia z długą listą różnych przykładowych zachowań. Rozwiązanie wymagające najmniejszego umysłowego wysiłku polega na prostym zakodowaniu tych przypadków użycia, po jednym na raz. Jeśli przyjrzymy się początkującym programistom, zauważymy, że często zachowują się w następujący sposób: masowo produkują mnóstwo logiki warunkowej dla każdego przypadku, który przyjdzie im do głowy.

Nie ma w tym jednak nic eleganckiego, a kod napisany w takim stylu wali się, gdy pojawiają się dane wejściowe choćby odrobinę różne od tych, jakie pojawiały się w przykładach rozważanych przez programistę. Gdy myślimy o eleganckim rozwiązaniu, często mamy na myśli takie, które ma charakter ogólny: odrobina logiki, która nadal może poprawnie obsłużyć wiele przypadków użycia.

Odnalezienie takiego rozwiązania przypomina poniekąd dopasowywanie wzorców czy rozwiązywanie zagadki. Trzeba wysiłku, aby w różnorodności przypadków użycia odnaleźć leżący u ich podstawy ukryty porządek. Gdy uda nam się tego dokonać, uczucie jest niesamowite.

Zaczynajmy już!

Niemal każdy pomija wstępne rozdziały, cieszę się więc, że zabrnęliśmy tak daleko. Nie mogę zaoferować wiele w podzięce za okazaną cierpliwość, ale mogę dać kilka rad, które – mam nadzieję – okażą się pomocne:

- Abstrakcja i oddzielanie sprawiają, że ewolucja naszego oprogramowania jest szybsza i łatwiejsza, nie traćmy jednak na nie czasu, jeśli nie mamy pewności, że kod, którego dotyczą, będzie potrzebować tej elastyczności.
- Myślmy o wydajności i twórzmy projekty, mając ją na względzie w trakcie całego cyklu rozwoju oprogramowania, ale na tak późny etap, jak to tylko możliwe, odłóżmy niskopoziomowe, zasadnicze optymalizacje, które wbetonowują założenia w nasz kod.

- Szybko rozpocznijmy eksplorację przestrzeni projektowej naszej gry, ale nie róbmy tego w nadmiernym tempie, aby nie zostawić po sobie bałaganu. Ostatecznie będziemy musieli z tym żyć.
- Jeśli zamierzamy odrzucić jakiś kod, nie traćmy czasu na jego upiększenie. Gwiazdy rocka dewastują pokoje hotelowe, ponieważ wiedzą, że następnego dnia je opuszczą.
- Przede wszystkim jednak, jeśli chcemy, aby coś sprawiało frajdę, tworząc to coś, sami powinniśmy ją mieć.

Wierzcie mi, dwa miesiące przed dostarczeniem produktu to nie jest dobry moment na to, aby zacząć martwić się tym drobnym, uporczywym problemem objawiającym się tym, że gra działa z płynnością jednej klatki na sekundę.II

Wzorce projektowe raz jeszcze

Rozdział 2: Polecenie

Rozdział 3: Pyłek

Rozdział 4: Obserwator

Rozdział 5: Prototyp

Rozdział 6: Singleton

Rozdział 7: Stan

Jeśli wierzyć mojemu kalendarzowi, książka Wzorce projektowe ma już niemal 20 lat. O ile nie spoglądacie mi właśnie przez ramię, gdy będziecie czytać te słowa, będzie już mogła kupić w sklepie alkohol⁴. W przypadku tak dynamicznej branży jak tworzenie oprogramowania są to praktycznie czasy starożytne. Utrzymująca się popularność tej książki mówi co nieco o tym, jak ponadczasowe jest projektowanie w porównaniu do wielu platform i metodologii.

Chociaż sądzę, że książka ta ciągle jest ważna, w ciągu kilku ostatnich dekad wiele się nauczyliśmy. W tej części omówimy garść oryginalnych wzorców udokumentowanych przez Bandę czterech. Mam nadzieję, że na temat każdego z nich będę miał do powiedzenia coś pożytecznego. Uważam, że niektóre wzorce są nadużywane (Singleton), podczas gdy inne pozostają niedocenione (Polecenie). Kilka znalazło się tu, ponieważ chcę zgłębić ich znaczenie w przypadku gier (Pyłek oraz Obserwator). Wreszcie, niekiedy uważam po prostu, że pokazanie, w jaki sposób wzorce są uwikłane w szerszy obszar programowania może być zabawne (Prototyp oraz Stan).Przypisy

1 Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, przeł. Tomasz Walczak, Helion 2017 (przyp. tłum.).

2 Cyt. za: Blaise Pascal, Prowincjałki, przeł. Tadeusz Boy-Żeleński. Bezpłatne elektroniczne wydanie książki dostępne jest na stronach projektu „Wolne Lektury”: https://wolnelektury.pl/katalog/lektura/prowincjalki/.

3 Cyt. za: Antoine de Saint-Exupéry, Ziemia, planeta ludzi, przeł. Wiera i Zbigniew Bieńkowscy, Muza, 2002.

4 W USA alkohol można legalnie zakupić dopiero po ukończeniu 21 roku życia (przyp. tłum.).

5 Cytat za: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Wzorce projektowe. Elementy oprogramowania obiektowego wielokrotnego użytku, przeł. Tomasz Walczak, Helion 2017 (przyp. tłum.).
mniej..

BESTSELLERY

Kategorie: