![]() |
|
Programowanie:
Artykuły
FAQ
Download
Książki
WWW:
Artykuły
Narzędzia
Kursy
Darmowe
FAQ
Skrypty
Humor
Inne:
Forum
Wiki
Liczniki
Linki
Chat
Grafika
Video
Inne
|
Jak kończyć projekty ?czyli inzynieria oprogramowania w praktyceWstępArtykuł ten przeznaczony jest dla osób zajmujących się programowaniem. Obejmuje praktyczne aspekty inżynierii oprogramowania oraz doświadczenia autora dotyczące procesu programowania w sensie tworzenia programów.Wstęp ProjektFilozofia programowania Podział programowania Początkujący a zaawansowani Natura hakera Programowanie jako praca twórcza Kończenie projektówProjektowanie Inżynieria oprogramowania Podejmowanie decyzji Formy projektowania Etapy projektowania Jak projektować?Programowanie Programowanie a projektowanie Co trzeba umieć? Jak programować? Kod Komentarze Dokumentacja Kopie bezpieczeństwaTestowanie Szukanie błędów Testy OptymalizacjaZakończenie Literatura Skąd ten tytuł? Czy artykuł nie powinien być raczej zatytułowany "Inżynieria oprogramowania w praktyce"? Nie, ponieważ wszystkie przedstawione tu informacje, porady i metody mają wspólny cel - zwiększenie prawdopodobieństwa ukończenia prowadzonych projektów programistycznych, zwiększenie ilości ukończonych programów. Do kogo adresowany jest ten artykuł? Przede wszystkim do młodych pasjonatów programowania, którzy zajmują się indywidualnym pisaniem niekomercyjnych programów i gier. Pisanie zespołowe wymaga trochę innego podejścia i wzięcia pod uwagę dodatkowych zagadnień (jak komunikacja czy wspólna praca nad kodem), których ten tekst nie obejmuje. Mimo tego niektóre zawarte w nim wskazówki mogą się okazać przydatne także w takim przypadku. Dużo łatwiej powiedzieć, do kogo ten artykuł adresowany z całą pewnością nie jest:
Projekt Na zakończenie tego wstępu chciałbym wspomnieć, że słowo "projekt" jest moim zdaniem bardzo niefortunne i osobiście nie lubię go. Jego popularność pośród osób zajmujących się programowaniem pochodzi zapewne od angielskiego słowa "project", które według słownika angielsko-angielskiego oznacza przede wszystkim: "task that requires a lot of time and effort", czyli "zadanie wymagające dużo czasu i wysiłku". Tymczasem słownik języka polskiego nie pozwala na używanie słowa "projekt" w takim znaczeniu. Po polsku "projekt" to wyłącznie plan, zamiar zrobienia czegoś, np. projekt budynku. Angielskie "project" należałoby więc tłumaczyć bardziej jako "przedsięwzięcie" czy "zadanie". Skoro jednak to określenie jest tak powszechne, że praktycznie nikt nie zwraca uwagi na problemy z jego znaczeniem, ja również będę go używał w tym artykule mimo, że wtedy będzie trzeba mówić o "projektowaniu projektu" i "pisaniu projektu", bo projektowaniem trzeba też nazwać sporządzanie prawdziwego projektu (w takim znaczeniu, jakie obowiązuje w języku polskim). Mamy więc do czynienia z "projektem projektu" :) Filozofia programowaniaNa początek chciałbym trochę pofilozofować i przedstawić swoje przemyślenia na temat istoty zajmowania się programowaniem.Podział programowania Wydaje mi się, że uprawiane przez nas programowanie można podzielić na takie trzy rodzaje:
Początkujący a zaawansowani Ten podrozdział prezentuje moją teorię na temat cech odróżniających osoby początkujące od zaawansowanych. Są to w dalszym ciągu rozważania na temat programowania, które mają nas wprowadzić w klimat :) Początkujący, to ten, dla którego liczy się głównie efekt i chce go otrzymać jak najszybciej, jak najprościej, po linii najmniejszego oporu. Zaawansowany to ten, któremu przyjemność sprawia samo programowanie a satysfakcję sama świadomość, że napisany kod działa, nawet bez widocznych i namacalnych efektów. To normalne, że prawie każdy rozpoczyna swoją przygodę z programowaniem licząc na efekty. Z czasem jednak powinno zacząć podobać mu się samo programowanie jako proces twórczy. Jeśli sam ten proces (szczególnie kiedy wymaga poznania czegoś nowego, głębszego zastanowienia się, przeanalizowania problemu) jest dla kogoś męczarnią, a nie sprawia mu przyjemności, to powinien poważnie zastanowić się, czy w ogóle nadaje się na programistę. Początkujący mówi, że po prostu nie potrafi, nie jest w stanie napisać niektórych rzeczy - bo nie umie czegoś, co jest mu do tego potrzebne. Czasami także wielkość projektu po prostu go przerasta. Zaawansowany natomiast potrafi nauczyć się czegoś nowego, co jest mu akurat potrzebne - algorytmu, biblioteki, jakiś trików, metod, a nawet pewnych zagadnień z matematyki. Duże projekty natomiast to dla niego po prostu kwestia bardzo dobrego zaprojektowania i bardzo dużej ilości czasu potrzebnego na realizację. Często też początkujący porywają się z motyką na słońce rozpoczynając realizację projektów, których nie są w stanie wykonać. Wynika to oczywiście z nieprzemyślenia sprawy samego pisania, które ich czeka, a jedynie z marzenia o efekcie końcowym. Początkujący bazują wyłącznie na kursach (ang. "tutorials") znalezionych w Sieci mimo, że w większości z nich opisane są tylko niektóre możliwości, funkcje, zagadnienia z danego tematu. Perspektywa korzystania z prawdziwej, oficjalnej dokumentacji ich przeraża. Zaawansowani potrafią korzystać z dokumentacji, która pochodzi od autorów danej biblioteki czy języka i opisuje w sposób systematyczny wszystkie jego funkcje. To oczywiste, że nikt nie uczy się i nie zna na pamięć wszystkich potrzebnych mu funkcji. Oczywiste jest też, że oryginalna, systematyczna dokumentacja w postaci spisu funkcji i ich działania nienajlepiej nadaje się do nauki danego zagadnienia od podstaw. Jednak poznawanie każdej nowej dziedziny, biblioteki, języka itp. powinno przebiegać tak:
Istotą sprawy, do której zmierzam, jest przedstawienie pojęcia "haker". Nie chodzi tu jednak o to znaczenie tego słowa, które jest najpopularniejsze i które lansują media - o internetowego wandala czy nawet o postępującego według jakiegoś kodeksu etycznego młodzieńca, który tylko dla sportu włamuje się przez Internet. Prawdziwe, pierwotne znaczenie określenia "haker" (ang. "hacker") powstało w latach pięćdziesiątych w MIT (Massachusetts Institute of Technology). Pochodzi od słowa "hack", którym określano wtedy śmieszny, ale nieszkodliwy żart wymagający sporej wiedzy technicznej. Hakowanie miało też dużo wspólnego z włamywaniem się - ówcześni studenci często wchodzili w miejsca opatrzone napisem "Wstęp wzbroniony", np. żeby dostać się do nieużywanych akurat komputerów. W tym sensie haker to pewna natura ludzka, pewien sposób myślenia. Niektórzy twierdzą nawet, że hakerem można być w każdej dziedzinie, nie tylko w informatyce, w programowaniu. Trudno powiedzieć, czy hakerem trzeba się urodzić, czy też można nim zostać. Prawda leży pewnie gdzieś pośrodku. Wszyscy rodzimy się z z pewnymi cechami charakteru, a bycie hakerem to po prostu posiadanie pewnego zespołu cech. W największym skrócie można wymienić niektóre cechy typowego hakera: biegłość w zagadnieniach technicznych, zaradność, znajdowanie upodobania w rozwiązywaniu problemów i przekraczaniu ograniczeń. Hakerzy rozwiązują problemy i tworzą rzeczy wierząc w wolność i wzajemną pomoc. Są kreatywni i bardzo efektywni (szybko się uczą i szybko pracują), jeśli tylko mają motywację. Nuda jest ich wrogiem. Ponadto hakerów cechuje często aspołeczność, nocny tryb życia, używanie środków psychoaktywnych, jak kofeina (ale równocześnie niechęć do narkotyków), wolnościowe poglądy, obojętność wobec spraw religijnych, niebywały talent do nauk ścisłych, zainteresowanie fantastyką i grami RPG, a także zainteresowania językowe. Mówi się natomiast, że hakerzy rzadziej, niż inni ludzie dbają o wygląd zewnętrzny, o rozwój emocjonalny czy o kondycję fizyczną. Rzadziej też interesują się sportem mimo tego, że praktycznie cała populacja hakerów jest płci męskiej. Programowanie jako praca twórcza Co jest istotą programowania? Chociaż informatyce bliżej do nauk technicznych (czy wręcz matematycznych) niż do dziedzin humanistycznych, to jednak programowanie jest pracą twórczą. Dlatego opiera się nie tylko na wiedzy, ale w bardzo dużym stopniu także na doświadczeniu praktycznym. Wymaga kreatywności i samodzielności, twórczego myślenia i umiejętności rozwiązywania problemów. Programowanie jest jednocześnie nauką i sztuką. Istnieje nawet takie pojęcie jak poezja kodu. W tych instrukcjach dla maszyny istotnie musi być coś magicznego, skoro programiści postrzegają je tak odmiennie od pozostałych ludzi, dla których programowanie jawi się jako zagadnienie nudne, wręcz jako czarna robota. Co to takiego? Kod, jak każdy tekst, oddaje pewne intencje autora, jest środkiem wyrazu, odzwierciedla jego niepowtarzalny styl. Każde zadanie różni programiści rozwiążą na różne sposoby. Kod to jednak coś więcej niż tekst. Jego można nie tylko czytać. On się wykonuje - działa - można obserwować efekty jego pracy - i właśnie to jest moim zdaniem tak niezwykłe w programowaniu. Kończenie projektów Na zakończenie tych rozważań zadajmy sobie pytanie: Po co w ogóle kończyć projekty? Samo programowanie też sprawia przyjemność i przynosi doświadczenie. Jednak taka potrzeba wydaje się oczywista. Sens i cel kończenia projektów można ująć w kilku punktach:
ProjektowanieRealizację projektu podzieliłem na trzy etapy: projektowanie, programowanie i testowanie. Rozpoczynamy teraz omawianie pierwszego z nich. Zanim jednak zaczniemy, musisz poznać w skrócie teoretyczne podstawy dziedziny zwanej inżynierią oprogramowania.Inżynieria oprogramowania Inżynieria oprogramowania nie zajmuje się tylko samym programowaniem, ale tworzeniem programów. Traktuje programy jako produkty (tak jak inne rzeczy w sklepach) i tak też podchodzi do ich wytwarzania. Powstawanie programu (albo szerzej: jego cykl życia) według tzw. modelu kaskadowego dzieli na fazy:
Książkę poświęconą inżynierii oprogramowania przeczytałem jakiś czas temu, a w niniejszym artykule prezentuję kompilacje metod i technik praktycznych, zebranych przede wszystkim na podstawie mojego własnego doświadczenia. Jakiekolwiek echa inżynierii oprogramowania są więc tutaj mocno zrewidowane i dostosowane do konkretnych potrzeb. Stopień, w jakim obmyślasz, projektujesz, a po napisaniu testujesz swój program musi być zawsze adekwatny do jego wielkości i złożoności. Podejmowanie decyzji Rozpocznijmy więc nareszcie omawianie tych praktycznych metod. Pierwszym wyróżnionym przeze mnie etapem realizacji projektu jest projektowanie. Pod tym pojęciem rozumiem wszystko to, co wedle inżynierii oprogramowania zawiera się w fazie strategicznej, fazie określania wymagań, fazie analizy i projektowania. Mówiąc prościej - chodzi o zaplanowanie programu, zanim rozpoczniemy jego pisanie. To ważne, bo bezmyślne włączenie IDE i rozpoczęcie pisania rzadko owocuje czymkolwiek pozytywnym. Nie można przesadzać w żadną stronę, ale warto zawsze trochę przemyśleć i zaprojektować swój program, zanim rozpocznie się programowanie. Co najwyżej projekt upadnie, zanim jeszcze na dobre się rozpocznie :) Najpierw chciałbym nauczyć cię podejmowania decyzji. To bardzo cenna i ważna umiejętność, bo podczas programowania bardzo często musisz dokonywać różnych wyborów, a podczas projektowania będziesz to robił jeszcze częściej. W sposób intuicyjny można podejmować tylko najprostsze i najbardziej oczywiste decyzje. Do pozostałych warto poznać pewne systematyczne metody. Liczenie na palcach Decyzje do podjęcia bywają różne. Najprostszą sytuacją jest, kiedy zadajesz sobie pytanie czy powinieneś coś zrobić, czy też nie. Najprostszą techniką do podejmowania takich decyzji jest z kolei liczenie na palcach - na prawej ręce zalet, a na lewej ręce wad. Dla przykładu rozpatrzmy taki problem: Czy swoją grę powinienem pisać w 3D? Na prawej ręce odliczam zalety: 1. Gra będzie ładniejsza, bardziej atrakcyjna 2. Nauczę się przy okazji 3D Na lewej ręce odliczam wady: 1. Będę się musiał nauczyć 3D... 2. ...To będzie trudne... 3. ...Pisanie potrwa dłużej... 4. ...Mniejsze są szanse, że to skończę Decyzja: Więcej jest wad, piszę grę dwuwymiarową! Przy okazji może się okazać, jak w przykładzie powyżej, że zalety trzeba było specjalnie wynajdywać, a wady można było bez trudu wymieniać jednym ciągiem. Czasami wydaje się nawet, że takim formalnym podejmowaniem decyzji próbuje się tylko potwierdzić, uargumentować swoją znaną wcześniej odpowiedź. To potwierdza moją hipotezę, że każde rozwiązanie, każda odpowiedź jest gdzieś w nas - trzeba tylko umieć po nią sięgnąć. Tabelka Kiedy problem jest trochę bardziej skomplikowany (albo kiedy brakuje palców do liczenia :) warto wziąć kartkę, długopis i sporządzić sobie tabelkę. W swojej prostszej odmianie będzie to tabelka nieważona - każdy z argumentów jest tak samo ważny. Za pomocą tabelki też można podejmować decyzje typu "tak/nie", ale można również dokonywać wyboru pomiędzy różnymi możliwościami. Będziemy znowu zliczali, ile jest argumentów przemawiających za każdą z nich. Najpierw trzeba je wypisać w kolumnach, a potem podliczyć. Oto przykład:
6*10 + 0*5 + 2*7 + 1*3 = 60 + 0 + 14 + 3 = 77 Analogicznie trzeba policzyć pozostałe kolumny. Wygrywa ten wybór, który otrzyma najwięcej punktów. Jeśli wszystkie pozostałe mają dużo mniej (jak w tym przykładzie), to dobrze - decyzja jest jednoznaczna. Jeśli dwa lub więcej mają równie dużo, trzeba się jeszcze raz zastanowić. Po takich obliczeniach może się zdarzyć, że będziesz miał uczucie, że nie takiego wyniku oczekiwałeś. Czy to oznacza, że takie formalne podejmowanie decyzji jest bez sensu? Absolutnie nie. To znaczy, że powinieneś jeszcze raz przemyśleć wagi (ważność) poszczególnych zagadnień. Być może też zapomniałeś dodać jakieś ważne kryterium (zagadnienie), które może zadecydować o innym wyniku. Podsumowanie Takie tabelki można rysować sobie na komputerze - w Notatniku, w Pancie albo lepiej w arkuszu kalkulacyjnym, jak Microsoft Excel. Osobiście do projektowania zwykle wolę jednak kartkę papieru. Chociaż nie można czegoś na niej przenieść, czegoś skasować i nic się na niej samo nie policzy, to ma jedną wielką zaletę - daje ogromne możliwości formatowania :) Można swobodnie rysować kreski, symbole, krawędzie tabel itp. Dodatkową techniką podejmowania decyzji jest odrzucanie tych opcji, które w ogóle nie mają sensu. Najczęściej będą to rzeczy zbyt duże, zbyt poważne albo za trudne. Odrzucić trzeba też jakąś opcję wtedy, kiedy jakaś inna opcja jest lepsza od niej pod wszystkimi możliwymi względami. Jeśli w takiej sytuacji nie chcesz takiej opcji odrzucić, to znaczy, że widzisz pewnie jakąś jej zaletę, tylko zapomniałeś jej uwzględnić. Kiedy odrzucać? Przede wszystkim wtedy, kiedy możliwych opcji jest dużo. Wtedy odrzucanie tych najgorszych to doskonały sposób na uproszczenie problemu. Czasami już na wstępie widać, że jakieś rozwiązanie kompletnie się nie nadaje, nie wchodzi w grę. Można je wtedy odrzucić, albo dla świętego spokoju uwzględnić przy podejmowaniu decyzji - jego niższość powinna wtedy wyraźnie wyjść z obliczeń. Niektóre możliwości można też odrzucić już po wpisaniu do tabelki wartości liczbowych. Przeprowadzanie formalnego, pisemnego podejmowania decyzji (na kartce albo na komputerze) ma jeszcze jedną wielką zaletę - stanowi dowód podjętej decyzji razem ze spisanymi argumentami, które o niej zdecydowały. Dlatego takie pliki czy kartki należy zawsze zostawiać. Jeśli kiedyś najdą cię wątpliwości co do słuszności takiej decyzji, odnajdziesz wtedy tą kartkę, spojrzysz na nią i upewnisz się, że podjąłeś słuszną decyzję. To ważne, bo do raz podjętych decyzji nie należy wracać. Chyba, że od czasu jej podjęcia zmieniły się argumenty albo ich ważność - wtedy można podjąć decyzję jeszcze raz. Formy projektowania Pisemny projekt to jakby przedłużenie umysłu - zapisujesz w nim to, co trudno jest ogarnąć myślami (bo jest za duże i zbyt skomplikowane), żeby rozpisać to w sposób systematyczny, ujrzeć i zachować przed zapomnieniem jakiegoś szczegółu. W tym podrozdziale chciałbym opisać moje sposoby na organizację projektu i formę, w jakich warto sporządzać projekty. Na początek zajmijmy się doborem odpowiednich narzędzi. Narzędzia Wbrew temu, co mogłoby się wydawać, papier wcale nie odchodzi do lamusa. Moim zdaniem do projektowania warto używać kartki papieru, bo daje ona ogromne możliwości formatowania, niedostępne w żadnym programie. Na kartce możesz swobodnie i prosto pisać, podkreślać, przekreślać, rysować strzałki, a nawet całe rysunki, schematy, diagramy i tabele. Jednak niektóre rzeczy warto robić na komputerze. Najlepiej nadaje się do tego zwykły, systemowy Notatnik. Dokument tekstowy na komputerze ma tą przewagę nad notatką na papierze, że możesz go swobodnie przerabiać, organizować, dopisywać i usuwać jego części w różnych miejscach. Ma też swoje wady - daje mniejszą swobodę formatowania i wymaga włączania komputera. Czasami warto projektować przy wyłączonym komputerze - a raczej czas, kiedy komputer nie jest włączony poświęcać na projektowanie. Pośród narzędzi bardziej zaawansowanych, niż kartka papieru czy Notatnik, warto wymienić arkusz kalkulacyjny (np. Microsoft Excel). Od czasu do czasu taki program może się przydać, kiedy trzeba przeprowadzić jakieś obliczenia (np. podjąć jakąś trudną decyzję z pomocą tabelki ważonej). Jednak sposób organizacji informacji w arkuszu kalkulacyjnym czyni go raczej nieprzydatnym do jakichkolwiek innych zastosowań w projektowaniu. Raczej nieprzydatne są też moim zdaniem edytory tekstu sformatowanego (jak Microsoft Word). Wygodniej i lepiej jest pisać w zwykłym Notatniku, a formatowania i wypunktowania robić za pomocą odpowiednich znaczków. Jednak to jest tylko moja opinia, w dodatku dość kontrowersyjna i łatwo można się z nią nie zgodzić. Oto przykładowy fragment takiego pliku tekstowego z projektem: Projekt gry == ________________ Założenia główne - ma być ładna - grywalna - dynamiczna - dużo krwi Nie są też moim zdaniem potrzebne edytory graficzne czy nawet specjalnie dedykowane do sporządzania schematów i diagramów edytory grafiki wektorowej. Do takich celów wystarczy kartka papieru, a narysowanie takiego schematu na kartce będzie dużo prostsze, niż za pomocą specjalnego programu. Formy tekstowe To obojętne, czy chodzi o tekst w jakimś edytorze tekstu na komputerze, czy o notatki na kartce papieru. Chciałbym przedstawić tu najważniejsze ze stosowanych przeze mnie podczas projektowania form, które mają charakter tekstowy (nie graficzny). ListyPraktycznie zawsze są to listy - czyli kolejne punkty, a każdy z punktów zawiera trochę tekstu (zwykle nie więcej, niż jedną linijkę). Listy to dużo lepszy, bo bardziej systematyczny i ustrukturalizowany sposób reprezentacji informacji, niż zwykły, ciągły tekst.Listy nie muszą być płaskie - mogą tworzyć hierarchię. Każdy punkt może zawierać swoje podpunkty, a te kolejne podpunkty itd. - od zagadnień najbardziej ogólnych do najbardziej szczegółowych. Punkty warto grupować - dzielić na kategorie według jakiś kryteriów. Można to robić pisząc je razem, obok siebie i oddzielając od innych grup linijką odstępu, a można stosować różne rodzaje wypunktowania (do punktowania nadają się znaczki takie jak: - * > ] = o).
Listy warto też sortować. Punkty na liście można porządkować według
różnych kryteriów, zależnie od tego, jakie informacje się w niej znajdują.
Czego mogą dotyczyć listy? W zasadzie znajdują zastosowanie wszędzie. O różnych etapach projektowania i notatkach, które powstają w ich wyniku przeczytasz w następnym podrozdziale. Oto przykład projektu gry w postaci listy: - intro
Jakiś fajny, cząsteczkowy efekt graficzny
- menu
> Wróć do gry (tylko jeśli gra trwa)
> Nowa gra
> Otwórz grę
> Zapisz grę (tylko jeśli gra trwa)
> Credits
> Opcje
> Wyjście
Tutaj pytanie: Czy na pewno chcesz wyjść?
- ekran zapisywania i otwierania gry
> lista zapisanych gier
> pole do wpisania nazwy
> przycisk OK
> przycisk Anuluj
- Opcje
> wybór rozdzielczości
> wybór częstotliwości odświeżaniaListy TODONajważniejszym rodzajem listy, wymagającym osobnego omówienia jest lista TODO, czyli lista rzeczy do zrobienia. Takie listy pojawiają się i są intensywnie wykorzystywane przez cały czas powstawania i realizacji projektu, dlatego warto dobrze nauczyć się nimi zarządzać.Na liście TODO zapisuje się rzeczy, które trzeba będzie jeszcze zrobić w ramach projektu - te bliższe i te dalsze, te większe i te mniejsze, a nawet najmniejsze drobiazgi. Chodzi o to, żeby o niczym nie zapomnieć. Kiedy tylko przypomni ci się coś, co trzeba albo można do projektu dodać i nie masz możliwości zająć się tym natychmiast (choćby przemyśleć to), dopisz nowy punkt do listy TODO. Co można powiedzieć o listach TODO? Przede wszystkim lista taka ulega ciągłym zmianom. Stale pojawiają się nowe punkty, nowe rzeczy do zrobienia, a stare są realizowane i trzeba je jakoś wykreślić. Zależnie od tego, czy daną listę TODO prowadzisz na kartce czy na komputerze oraz czy chcesz, żeby zrealizowane już pozycje pozostały na niej, możesz to robić na różne sposoby:
Listy TODO szybko się rozrastają i warto grupować ich punkty według zagadnienia, którego dotyczą. Na przykład możesz oddzielić rzeczy "do zrobienia" związane z pisaniem samego programu, jego dokumentacji, instalatora itd. Poszczególne pozycje listy warto też sortować, a kryteriami tego sortowania mogą być np.:
Warto mieć taką listę również na kartce, ponieważ podczas projektowania (albo kiedykolwiek, jeśli tylko twój umysł razem z podświadomością jest naprawdę zaangażowany w projekt) - choćby w środku nocy - może ci przyjść do głowy jakiś pomysł albo jakaś rzecz do zrobienia, o której nie powinieneś zapomnieć i warto mieć przygotowane miejsce, w którym można będzie ją zapisać. Można też zorganizować sobie taką ogólną listę TODO (niezwiązaną z żadnym konkretnym projektem), na której będziesz zapisywał wszystko, co ci się przypomni - nowe pomysły, drobiazgi i wszystko to, co musi poczekać z przemyśleniem, realizacją albo przynajmniej przepisaniem w docelowe miejsce do chwili włączenia komputera. Tak wygląda w pomniejszeniu moja aktualna lista TODO, która leży na moim biurku:
Schematy i diagramyCzasami zdarza się, że istota konkretnych informacji, które chcesz w sposób systematyczny zapisać, nie pozwala na ich dobre zorganizowanie za pomocą listy z punktami i podpunktami. Wtedy narysuj poszczególne hasła rozmieszczając je przestrzennie na powierzchni kartki, biorąc je w owalne albo prostokątne ramki i połącz strzałkami.Czasami też lepszym pomysłem na przedstawienie hierarchicznie zorganizowanych informacji jest narysowanie drzewa, w którym każda informacja może mieć swoje podinformacje - może się rozgałęziać. Pewne rodzaje schematów i diagramów - diagramy klas i diagramy przejść stanów - poznasz w następnym podrozdziale. Tam też znajdują się odpowiednie przykłady. Oto przykład ogólnego schematu organizacji gry - moduły i relacje między nimi:
TabeleKiedy zapisywane informacje nie tylko składają się z kolejnych punktów (wierszy), ale każdy z nich ma takie same pola (kolumny), wtedy można pomyśleć o użyciu tabeli.Do zrobienia tabeli nadaje się kartka w kratkę, ale czasami pomocny może być też arkusz kalkulacyjny (szczególnie jeśli przy okazji trzeba coś policzyć). Przykłady mogłeś zobaczyć w podrozdziale poświęconym podejmowaniu decyzji. Okno główneKiedy masz już w miarę klarowną koncepcję swojego pomysłu i chcesz rozpocząć jego dokładne projektowanie (między fazą określania wymagań, a fazą analizy), na pierwszej stronie swojego stosu kartek z projektami narysuj schematycznie okno główne swojego programu albo ekran główny swojej gry (w czasie normalnego działania). Zaznacz na nim ważniejsze przyciski i inne kontrolki, narysuj jakieś przykładowe informacje wewnątrz okna czy jakąś przykładową sytuację z gry.Taki rysunek to dobry punkt wyjścia do rozpoczęcia projektowania poszczególnych części programu czy nawet do samego dzielenia go na części i moduły. Pomaga wyobrazić sobie, jak program będzie wyglądał po ukończeniu i przywodzi na myśl marzenia o chwili, kiedy będzie już gotowy. Kiedy zagłębiasz się w szczegóły projektowania, a potem pisania jakiś elementów swojego programu, rzuć czasami okiem na ten główny rysunek. To pomaga wrócić na chwilę myślami do całościowego spojrzenia na projekt, o którym można czasami łatwo zapomnieć, a to może mieć złe konsekwencje (np. kiedy poświęcasz za dużo uwagi czemuś, co nie jest zbyt istotne albo nawet robisz coś, co potem okazuje się zupełnie niepotrzebne i musisz to skasować). Mapy myśliMapa myśli (ang. "mind map") to sposób organizacji notatek na kartce, który zdaniem wielu najlepiej odzwierciedla pracę ludzkiego mózgu. Mnie on zupełnie nie odpowiada - wolę listy z punktami i podpunktami. Widocznie mój umysł właśnie takich struktur danych używa do wewnętrznej reprezentacji informacji. Tym nie mniej niektórzy twierdzą, że mapy myśli nadają się do wszystkiego - od lepszego rozumienia przeczytanych książek, poprzez naukę do sprawdzianów, aż po zapisywanie efektów burzy mózgów i dlatego czuje się w obowiązku napisać o nich kilka słów.Mapa myśli operuje słowami-kluczami. Na środku jest jakieś główne hasło lub symbol, a wokół należy pisać poszczególne zagadnienia w postaci odgałęzień. Odgałęzienia mogą się łączyć, w każdej chwili można znaleźć miejsce na dodawanie nowych itd. - panuje tu pełna swoboda. Zaleca się przy tym stosowanie różnych formatowań - różnych rodzajów linii, tekstu, różnych kolorów, stawianie symboli (jak wykrzykniki "!" czy
znaki zapytania "?") przy niektórych hasłach itp.
Oto pomniejszona mapa myśli, którą sporządziłem kiedyś w trakcie czytania książki na temat biblioteki standardowej C++. Do robienia takich map należy używać kartek nie mniejszych, niż w formacie A4.
PseudokodJeżeli sam kod w języku programowania, którego używasz będzie skomplikowany, czasem warto pomóc sobie projektując go "na sucho" - na kartce albo lepiej w Notatniku, bez kompilacji, w pseudokodzie. Pseudokod to jakiś język programowania, który nie istnieje, a którego używasz, żeby jak najprościej opisać jakieś instrukcje. Może być podobny do C++, do Pascala, do czego tylko chcesz. Nie musisz się martwić szczegółami, obsługą błędów, możesz używać nieprecyzyjnych czy niezdefiniowanych jeszcze funkcji - byle opisać samą ideę.W niektórych sytuacjach ta metoda jest naprawdę niezastąpiona. Za jej pomocą możesz zaprojektować sam interfejs jakiegoś modułu czy klasy - jakie funkcje będzie udostępniał na zewnątrz. Możesz też opisać w pseudokodzie jakiś skomplikowany algorytm. To duży komfort psychiczny pisać kod bez kompilacji, nie obawiając się, że gdzieś wkradnie się mniej czy bardziej poważny błąd. Możesz go dopracowywać "na luzie". Kiedy już będzie dostatecznie precyzyjny, przepiszesz go na swój język programowania i jeśli o niczym nie zapomniałeś, doświadczysz czegoś doprawdy wspaniałego - ten skomplikowany kod skompiluje się i zadziała poprawnie od razu, za pierwszym razem! Pierwszym przykładem niech będzie zdefiniowanie interfejsu, czyli funkcji, które pewien moduł (weźmy dla przykładu moduł dźwiękowy projektowanej gry) będzie udostępniał na zewnątrz. Nie ważne, czy to będzie klasa, czy funkcje globalne. System dźwiękowy - interfejs = - void Create() - inicjalizacja systemu - void Destroy() - finalizacja systemu - void Load(int ID, string FileName) - wczytanie dźwięku WAVE - int Play(int ID, bool Loop) - odgrywa dźwięk, zwraca kanał - void Stop(int Channel) - zatrzymuje dźwięk z podanego kanału - void SetVolume(int Channel, byte Volume) - ustawia głośność dźwięku Jako przykład projektowania algorytmu przytoczę tym razem autentyczny fragment mojego projektu pewnej gry - algorytm wykrywania kolizji. Jak widać, używany pseudojęzyk może wyglądać dowolnie - tak, jak w danej chwili jest wygodnie. Chodzi o opisanie samego algorytmu. foreach(obj na liście)
if obj się porusza
Circle = obj.Ruch
CircleBackup = Circle
Stop = false
foreach (kolidująca ściana, Circle)
if obj.Collision(ściana)
Stop = true
foreach (kolidujący obiekt, Circle)
if obj.Collision(obiekt)
Stop = true
if obiekt.Collision(obj)
Stop = true
if Stop
Circle.x = obj.x
if JestKolizja(Circle)
Circle = CircleBackup
Circle.y = obj.y
if JestKolizja(Circle)
obj.vx = 0
obj.vy = 0
else
obj.pos = Circle
obj.vx = 0
else
obj.pos = Circle
obj.vy = 0
else
obj.pos = CircleDiagramy klasNiezależnie czy piszesz obiektowo czy strukturalnie, w budowie swojego programu z pewnością możesz wyróżnić jakieś "obiekty". Jedne przechowują w sobie drugie obiekty albo całe ich kolekcje, odpowiadają za ich tworzenie, usuwanie, odwołują się do nich itd. - są ich właścicielami. Tak powstaje pewna hierarchia. Inną hierarchią są dziedziczone klasy. Czasami po prostu jakiś obiekt potrzebuje dostępu do innego, przechowuje wskaźnik do niego itp. Tak powstają różne relacje. To nie muszą być tylko klasy, to mogą być np. moduły.Takie wzajemne zależności między elementami muszą być dobrze zaprojektowane. Nie zawsze da się przewidzieć już podczas projektowania, jak to wszystko będzie najlepiej napisać. Dlatego prezentowane tu metody nadają się bardziej do stosowania już podczas implementacji - by pomóc sobie w trudnych momentach. Kiedy samo pisanie nie wystarcza i sytuacja jest skomplikowana, możesz narysować sobie diagram klas. Są do tego specjalne formalne notacje (jak UML), ale nie musisz się ich uczyć. Ważne tylko, żeby jakoś rozrysować, a przez to pomóc wyobrazić sobie sytuację w kodzie. Jako przykład możemy rozrysować organizację elementów projektowanej platformówki. Po lewej stronie jest hierarchia klas. Obiekt jest
abstrakcyjną klasą bazową i z niej dziedziczą inne klasy wprowadzając pola i metody
charakterystyczne dla swojego rodzaju. Po prawej stronie są relacje między
działającymi już obiektami - GRA przechowuje kolekcję obiektów
oraz mapę, która składa się z kolei ze zbioru pól.
Diagramy przejść stanówNie przydają się zbyt często, ale to jeszcze jeden sposób na rozpisanie sobie działania programu, jeśli jego projektowanie czy kodowanie staje się zbyt skomplikowane - po prostu kiedy już nie mieści się w głowie. Taki diagram składa się ze zbioru stanów i połączeń między nimi. W każdej chwili program (czy jakiś jego obiekt, którego diagram dotyczy) jest w jednym z tych stanów. Strzałki pokazują, jak można między tymi stanami przechodzić, a przy każdej można napisać, jaka akcja powoduje przejście do tego stanu i jakie dodatkowe czynności są przy tym przejściu podejmowane.Poniższy przykład to diagram stanów, w jakich może być gra i przejść między nimi. Zakładamy przy tym, że wyjście do menu nie jest równoznacznie z usunięciem z pamięci samej gry, a więc można wyjść do menu w celu zapisania gry, a potem do niej wrócić.
ProgramowaniePo zaprojektowaniu przychodzi czas na programowanie (inaczej kodowanie, pisanie, implementację). Jednak te dwie rzeczy nie są zupełnie odrębne. Czasami można zacząć pisanie bez dokładnego zaprojektowania całości, a czasami trzeba już podczas pisania wrócić do projektu albo jakiś szczegół zaplanować.Programowanie a projektowanie Przez ostatnie pół roku miałem zajęcia z probabilistyki i statystyki z pewnym profesorem, którego z pewnością długo nie zapomnę. Jedną z rzeczy, które powiedział i które na dobre zapadły mi w pamięci jest takie oto stwierdzenie: Otóż powiedział on, że to, co my - studenci - nazywamy rozwiązywaniem zadania, to jest tylko formalność - najmniej ważna część rozwiązania. To nie podstawienie do wzoru i policzenie stanowi sedno sprawy. Tak naprawdę w zadaniu chodzi o to, żeby przeczytać i zrozumieć jego treść, rozpoznać problem, wybrać odpowiednie metody, wzory i zaplanować, w jaki sposób trzeba będzie go rozwiązać. Myślę, że z programowaniem jest trochę podobnie. Im jesteś bardziej zaawansowany i im większe rzeczy piszesz, tym lepiej i dokładniej musisz projektować, a im lepiej i dokładniej projektujesz, tym bardziej samo kodowanie staje się tylko formalnością. Ale na szczęście w naszym przypadku nie musi to wyglądać aż tak radykalnie. Programowanie nadal pozostaje czynnością najważniejszą, sednem sprawy i istotną naszej pracy. Jeżeli boisz się, że przez porządne projektowanie programowanie straci swój urok, przypomnij sobie chwile, kiedy wpadasz na jakiś nowy pomysł i obmyślasz go, marzysz o nim. Projektowanie i wszystkie inne etapy tworzenia programu mogą być i są równie przyjemne. Co trzeba umieć? Żeby w ogóle mógł myśleć o programowaniu jako o realizacji pewnych projektów, musisz umieć kilka rzeczy w stopniu przynajmniej podstawowym. W przeciwnym razie ten artykuł nie jest dla ciebie, a jedyne, co mogę ci polecić, to dalszą naukę samego programowania i nabywanie doświadczenia. Żeby móc dobrze programować, musisz umieć:
Nawet kiedy doskonale znasz język programowania i wszystkie potrzebne biblioteki, możesz mieć problem z napisaniem wymarzonego programu. Przyczyną jest zwykle jego wielkość. Zaprojektowanie bardzo pomaga poradzić sobie z tym problemem, ale to nie koniec. Dalsze krotki trzeba podjąć także podczas kodowania. Dlatego również w tej dziedzinie chciałbym udzielić ci kilku wskazówek. Zawsze najtrudniej jest zacząć. Dlatego zamiast rozmyślać i zamartwiać się wielkością i złożonością tego, co czeka cię do napisania lepiej natychmiast włącz IDE, stwórz nowy moduł, napisz komentarz z nagłówkiem (data, wersja, twoje nazwisko) i zacznij coś pisać - choćby nagłówki funkcji, szkielet klasy itp. - po prostu zacznij. Dopiero potem pomyślisz co dalej, napiszesz treść wszystkich funkcji itp. a przekonasz się, że dalej już jakoś pójdzie. Dokładnie tak jak podczas projektowania. Wiele lat temu jakiś programista mądrze powiedział, że wszystko wydaje się trudne, dopóki nie stanie się proste. Tą magiczną receptą na uproszczenie wszystkiego co trudne jest podział każdego zadania na mniejsze podzadania i skupienie się na nim najpierw tylko ogólnie, a potem na szczegółach każdego z tych podzadań osobno. W przypadku programowania lepiej byłoby chyba powiedzieć, że należy pisać kolejne warstwy - otoczki (ang. "wrappers"). Właściwie to jest istota całego programowania i jej zrozumienie wydaje się być kluczem do stania się bardziej zaawansowanym programistą. Na przykład początkujący mógłby bać się pisania gry z atrakcyjną grafiką myśląc, że to trudne, bo w czasie jej pisania ciągle będzie musiał się tą grafiką zajmować. Zaawansowany wie, że to totalna bzdura - trzeba się nauczyć jakiejś biblioteki graficznej i raz napisać własny moduł graficzny - otoczkę na nią, która udostępni funkcje potrzebne w tej grze (jak rysowanie bitmap, tekstu i cząsteczek). W pozostałej części gry będzie już potem mógł tylko używać funkcji tego swojego modułu. Nie będę w tym artykule wdawał się w rozważania na temat organizacji kodu w poszczególnych językach programowania. Chciałbym natomiast zachęcić cię do poznania, zrozumienia i stosowania zasad programowania zorientowanego obiektowo. To nie jest tylko zbiór reguł składniowych języka - to sposób myślenia, który może naprawdę dużo pomóc, chociaż osobiście uważam, że czasami bywa przeceniany i sam mieszam kod obiektowy ze strukturalnym. Na temat programowania obiektowego są całe grube książki, dlatego tutaj napiszę o tym jedynie kilka słów. Po przejściu od asemblera i programowania liniowego (z wszędobylskimi instrukcjami skoku) na wyższy poziom piewcy programowania strukturalnego (jak Niklaus Wirth) głosili konieczność oddzielenia danych (zmienne) kod kodu (funkcje). Programowanie obiektowe natomiast łączy dane z kodem, który na nich operuje i zamyka je razem w logiczne jednostki zwane klasami. W ten sposób obiekty należące do klasy Samochód (klasa jest typem, można definiować
zmienne tego typu i tworzyć jednocześnie wiele takich obiektów) przechowują
swoje dane (jak Pozycja, Prędkość, Kolor)
i mają funkcje do operowania na nich (jak Ruszaj(),
Zatrzymaj()).
Programowanie obiektowe najlepiej nadaje się tam, gdzie zachodzi potrzeba przechowywania kolekcji wielu niebanalnych obiektów (tzn. każdy z nich jest czymś więcej niż pojedynczą liczbą czy znakiem). Oto przykład w C++ dotyczący omawianej, fikcyjnej platformówki: // Abstrakcyjna klasa bazowa dla obiektów w grze
class Obiekt {
// Pozycja na mapie
int x, y;
public:
// Klasa jest polimorficzna - wirtualny destruktor
virtual ~Obiekt();
// Przesunięcie obiektu (sprawdza kolizje)
virtual void Move(int NewX, int NewY);
// Odczytanie pozycji
int GetX() { return x; }
int GetY() { return y; }
// Rysuje obiekt na ekranie
virtual void Draw() = 0;
};
// Jakiś leżący nieruchomo przedmiot, ozdoba itp.
class Rzecz : public Obiekt {
private:
int Rodzaj;
public:
// Konstruktor
Rzecz(int a_Rodzaj);
// Rysowanie
virtual void Draw();
};
// Postać w grze - jakieś żyjątko, które może zginąć
class Postac : public Obiekt {
private:
int Zycie; // 0..ZYCIE_MAX
public:
// Konstruktor
Postac() : Zycie(ZYCIE_MAX) { }
// Zwraca życie
int GetZycie() { return Zycie; }
// Obrazenia dla tej postaci
// (zwraca true, jeśli nie żyje)
bool Damage(int x);
};
// Bohater gry - to też rodzaj postaci
class Bohater : public Postac
{
private:
// Amunicja do broni różnego rodzaju
int Amunicja[BRONIE_COUNT];
public:
// Konstruktor
Bohater();
// Rysowanie bohatera
virtual void Draw();
};
// Potworek, czyli wróg gracza
class Potworek : public Postac
{
private:
int Rodzaj;
public:
// Konstruktor
Potworek(int a_Rodzaj) : Rodzaj(a_Rodzaj) { }
// Rysowanie potworka
virtual void Draw();
};
// Klasa menadżera obiektów przechowuje kolekcję obiektów,
// zarządza nimi i zwalnia je z pamięci
class ObiektManager {
private:
std::list<Obiekt*> Lista;
public:
// Zwalnia wszystkie obiekty z pamięci
~ObiektManager();
// Dodaje do listy utworzony wcześniej obiekt
void Add(Obiekt *obj);
// Rysuje wszystkie obiekty
void Draw();
// itd...
};Na koniec chciałbym dodać, że zależności między klasami i modułami powinny tworzyć hierarchię, czyli drzewo. W praktyce trudno tego tak ściśle przestrzegać i powstaje lepszy albo gorszy graf. Tym nie mniej należy dla każdego modułu jasno określić, jaki interfejs (zbiór klas, funkcji itp.) udostępnia na zewnątrz i z jakich innych modułów korzysta. Przykładowo: jeśli masz w swoim programie moduł z różnymi ogólnymi funkcjami, jak konwersje różnych typów na łańcuchy, szybki generator liczb pseudolosowych itp. i zakładasz, że wszystkie inne moduły twojego programu mogą się do niego odwoływać, to on sam nie powinien używać żadnego z nich, a jedynie nagłówków zewnętrznych (jak nagłówki używanych bibliotek). Innym przykładem może być moduł dźwiękowy. Skoro ma stanowić otoczkę na używaną bibliotekę dźwiękową (jak FMOD czy DirectAudio), tylko on powinien tej biblioteki używać. Pozostałe moduły gry powinny implementować dźwięk wyłącznie z użyciem funkcji tego modułu, a nie bezpośrednio. Moduł główny gry, podobnie jak inne moduły może odwoływać się do tego modułu dźwiękowego, ale on nie powinien odwoływać się do nich. Podobnie jest z klasami i obiektami. Każdy obiekt powinien mieć jasno zdefiniowanego właściciela, który go przechowuje (albo wskaźnik do niego), używa go i jest odpowiedzialny za jego tworzenie i usuwanie z pamięci. Każdy obiekt, wskaźnik i w ogóle każda zmienna powinna mieć jasno określony i zdefiniowany (przynajmniej w twojej głowie, a najlepiej w komentarzu przy jej deklaracji) czas życia - kiedy istnieje, kto i kiedy ją tworzy a kiedy usuwa. Dotyczy to nie tylko obiektów własnych klas czy zasobów alokowanych dynamicznie. Nawet najzwyklejsza globalna zmienna liczbowa w pewnym czasie (w niektórych stanach) nie jest używana, ma wartość niezdefiniowaną, w pewnych sytuacjach i przez pewne obiekty czy funkcje jest zapisywana, a przez inne odczytywana - im jaśniej to wszystko zdefiniujesz, tym lepiej. W filmie "Matrix" bardzo mądrze powiedziano, że wszystko co ma początek, ma też i swój koniec. Ta zasada powinna ci towarzyszyć ciągle podczas programowania. Kiedy otwierasz nawias, napisz od razu jego zamknięcie, a dopiero potem wypełniaj go zawartością. To znacznie uprości pisanie instrukcji i wyrażeń. Kiedy piszesz kod alokujący pamięć czy tworzący jakieś inne zasoby, od razu napisz komplementarny kod, który je zwalnia. Niezwalnianie zasobów nie spowoduje błędu i w ogóle nie będzie widoczne, ale powoduje wycieki i dlatego jest najbardziej zdradliwym z błędów. Kod Być może zdarzyło ci się, że pisałeś coś i nagle dostrzegłeś, że to jest bez sensu, zupełnie źle zorganizowane, nieprzemyślane i nie da się tego dalej ciągnąć - trzeba skasować i napisać od nowa, zupełnie inaczej. Takie sytuacje są częste i wynikają z niedostatecznie solidnego zaprojektowania. Dlatego pamiętaj: pisz w porządku od początku. To nie znaczy, że musisz zaprojektować cały program dokładnie, zanim w ogóle rozpoczniesz kodowanie. Najważniejsze to zaprojektować ogólnie, czyli odpowiednio podzielić na moduły. Potem już możesz spokojnie zagłębiać się w szczegóły każdego z tych elementów osobno projektując je i pisząc. To, co teraz napiszę, jest bardzo ważne dlatego proszę, abyś zwrócił na to szczególną uwagę. Otóż musisz cały czas dbać o swój kod. Mimo usilnych starań zawsze będą się zdarzały sytuacje, kiedy projekt okazuje się niedoskonały i w wyniku tego coś się w kodzie nie zgadza, coś trzeba zmienić. Czasami trzeba nawet zmienić bardzo dużo. Jeżeli taka sytuacja spotka cię w środku czy nawet już pod koniec pisania jakiegoś modułu, musisz natychmiast to zrobić. Nawet, jeśli trzeba w tym celu dużo skasować, dużo napisać od nowa, żmudnie przerabiać różne elementy składniowe kodu czy porządnie przemyśleć i zaprojektować nową organizację tego wszystkiego. Nie możesz pozwolić sobie na żadne prowizorki ani odkładać na potem żadnych poważnych zmian w kodzie. Takie sytuacje są normalne, nieuniknione i trzeba się do nich przyzwyczaić. Mają też swoje dobre strony - każda zmiana i każda nowa wersja kodu jest lepsza od poprzedniej, a podczas takich przeróbek zdobywasz wiele doświadczenia. Jeśli zostawisz wszystko tak jak jest i postawisz na rozwiązania tymczasowe, bardzo szybko pogubisz się we własnym kodzie i twój projekt upadnie. Komentarze Kiedy piszesz kod, przelewasz do pliku swoje myśli, ale ktoś inny albo ty sam za kilka dni, tygodni czy miesięcy nie będziesz już pamiętał, o czym wtedy myślałeś - zostanie tylko kod. Dlatego tak ważne jest pisanie komentarzy. Mike powiedział kiedyś, że komentarze są przydatne głównie dlatego, że są zielone. To prawda, mogą pełnić w kodzie także funkcję estetyczną, np. jako separator: // = // Klasy z obiektami gry To oczywiste, że nie ma sensu komentować wszędzie i wszystkiego. Szczególnie nie warto opisywać tego, co i tak widać w kodzie, np.: // ŹLE!!!
// Dla każdego wiersza...
for (y = 0; y < MAP_HEIGHT; y++) {
// Dla każdej kolumny...
for (x = 0; x < MAP_WIDTH; x++) {
// Odrysowanie pola mapy
DrawMapCell(x, y, Map[y*MAP_WIDTH+x]);
}
}Komentarz powinien opisywać to, czego z kodu bezpośrednio nie da się odczytać - rzeczy bardziej abstrakcyjne i ogólne, użyte sztuczki itp. // Odrysowanie całej mapy
for (y = 0; y < MAP_HEIGHT; y++) {
for (x = 0; x < MAP_WIDTH; x++) {
DrawMapCell(x, y, Map[y*MAP_WIDTH+x]);
}
}Komentarze mają także wiele innych zastosowań. Jednym z nich jest opisywanie interfejsu. Przykładowo, dobrym pomysłem jest opisywanie deklaracji każdej funkcji w pliku nagłówkowym komentarzem z informacją co ona robi, jakie parametry pobiera, co zwraca i jak reagują na błędy. // Wczytuje dźwięk z pliku WAVE do pamięci // Dźwięk zostanie sam zwolniony podczas finalizacji systemu dźwiękowego // - ID - identyfikator dla wczytywanego dźwięku // - FileName - nazwa pliku dźwięku wraz ze ścieżka // Jeśli się uda, zwraca true // Jeśli się nie uda, zwraca false i wpisuje komunikat do zmiennej g_ErrMsg bool Load(int ID, const std::string &FileName); Za pomocą komentarzy można zrobić swego rodzaju nagłówek na początku każdego pliku źródła: //---------------------------------------------------------- // Nazwa : sound // Wersja : 1.0 // Opis : Moduł dźwiękowy // Autor : Adam Sawicki "Regedit" // Data : 2004-09-21 // URL : mailto:regedit@risp.pl // URL : http://www.programex.prv.pl/ //---------------------------------------------------------- Można też umieszczać w komentarzach dłuższe, ogólne opisy sposobu działania danego modułu, klasy, funkcji itp., by ułatwić ich zrozumienie i używanie. Można opisać (razem z przykładowym kodem), jak należy prawidłowo używać danego modułu. Można nawet rysować sobie ASCII Art. /* Rysowanie obramowania przebiega w takich 4 etapach:
_________________
| |____3____| |
| | | |
| 1 | | 2 |
| |_________| |
|___|____4____|___|
*/Dokumentacja Inżynieria oprogramowania wymienia dokumentowanie jako osobną fazę produkcji programu, która odbywa się równolegle ze wszystkimi pozostałymi. Nie chodzi tu jednak o napisaniu pomocy do swojego programu - to można zrobić na końcu, kiedy program będzie już napisany. Chodzi o sporządzanie opisów z informacjami technicznymi. Część takich informacji możesz zawrzeć w plikach z kodem, w komentarzach - np. listę udostępnianych przez moduł funkcji wraz z opisem ich działania, parametrów, zwracanej wartości i reakcji na błędy. Czasami jednak warto sporządzić osobny dokument tekstowy opisujący np. używany w programie format pliku czy protokół sieciowy. To powinna być możliwie formalna, systematyczna i w pełni kompletna dokumentacja danego zagadnienia. Warto pisać dokumentacje, bo kiedyś możesz chcieć wrócić do swojego projektu, a dedukowanie formatu pliku na podstawie kodu funkcji do odczytywania i zapisywania to nic dobrego. Format pliku z poziomem do platformówki Lokalizacja: Platformówka\Maps\*.map Pliki binarne Budowa ------ - nagłowek - mapa - liczba obiektów [DWORD] - obiekt, obiekt, obiekt... Nagłówek -------- - łańcuch "Platformówka" (12 B) Mapa ---- - Szerokość [WORD] - Wysokość [WORD] - Rodzaj pola, Rodzaj pola, Rodzaj pola, ... [BYTE] Obiekt ------ - Typ [BYTE] 0 = rzecz 1 = potworek - x [WORD] - y [WORD] Jeśli rzecz: - Rodzaj rzeczy [BYTE] Jeśli potworek: - Życie [int] - Rodzaj potworka [BYTE] Kopie bezpieczeństwa Jeśli nigdy nie straciłeś bezpowrotnie efektów swojej pracy, to bardzo dobrze, ale to nie znaczy, że nigdy nic złego ci się nie przydarzy. Różne wypadki się zdarzają - od wirusów i trojanów, poprzez awarię dysku czy niefortunne repartycjonowanie aż po działalność młodszej siostry. Dlatego żeby nikt nigdy nie mógł powiedzieć ci mądry Polak po szkodzie, rób kopie bezpieczeństwa. Kopie można podzielić na dwa rodzaje. Pierwszy pomaga uchronić się przed utratą efektów pracy w razie awarii. W tym celu powinieneś kopiować swoje pliki na jakiś inny nośnik, poza dysk twardy - np. na swoje konto FTP/WWW/shell, na dyskietkę czy pamięć flash. Ja robie taką kopię codziennie wieczorem, jeśli tylko coś napiszę, a raz na miesiąc nagrywam wszystkie swoje dokumenty na CD-RW. Drugi rodzaj kopii powinieneś robić przed poważnymi zmianami w kodzie czy wykasowaniem jakiegoś fragmentu, bo stara wersja tego kodu może jeszcze kiedyś okazać się potrzebna. W tym celu wystarczy objąć pewien fragment źródła w komentarz zamiast go usuwać. Czasem jednak wygodniej jest skopiować starą wersję pliku do osobnego katalogu. Warto wspomnieć też o systemach kontroli wersji, które w sposób wygodny i funkcjonalny zapewniają archiwizowanie kolejnych wersji plików. W niektórych zastosowaniach są niezastąpione. Osobiście jednak uważam, że w projektach indywidualnych nie ma sensu wytaczać armaty na wróbla i najczęściej wystarcza katalog z podkatalogami z datami, np. Platformówka\Backup\2004-09-22\.
TestowanieW zatytułowany w ten sposób rozdziale chciałbym tak naprawdę przedstawić kilka różnych tematów.Szukanie błędów Pierwszym z nich jest szukanie błędów. Rzadko zdarza się, żeby wszystko działało od razu poprawnie. Mówią nawet, że jeśli program od razu działa dobrze, to coś jest nie tak. Po napisaniu nieuchronnie czeka cię znajdowanie błędów w kodzie zwane też debugowaniem, a w inżynierii oprogramowania - uruchamianiem. Chociaż nie należy to do przyjemności, to jednak nie musi to być aż tak znienawidzony etap realizacji projektu. Czasami błędu szuka się całymi godzinami, a nawet dłużej niż dzień, ale radość po znalezieniu i usunięciu takiego błędu jest naprawdę ogromna. Poniżej przedstawiam moje metody na znajdowanie błędów w kodzie. Wcześniej jednak chciałbym wprowadzić pewien podział błędów. Najprostsze z nich to z pewnością błędy składniowe języka programowania. Każdemu zdarza się czasami zapomnieć średnika czy zrobić jakąś literówkę. W nauce bezbłędnego pisania bardzo pomaga programowanie w językach skryptowych takich, jak PHP, ale to nie jest przyjemna lekcja. W językach tych bowiem nie ma deklarowania zmiennych, a więc popełnienie literówki w nazwie zmiennej nie powoduje błędu kompilacji, tylko odwołanie do nowej zmiennej o pustej wartości, a w efekcie dużo trudniejsze do znalezienia błędy w działaniu. Kompilatory (szczególnie do C++) mają nieprzyjemny zwyczaj wyrzucania ogromnej ilości komunikatów o błędach, w dodatku wszystkich nie tam i nie na ten temat, gdzie naprawdę jest błąd. Dlatego zawsze warto najpierw przyjrzeć się linijkom kodu wokół miejsca, w którym jest pierwszy błąd (szczególnie linijce poprzedniej), a dopiero potem przeczytać komunikat błędu. Błędów składniowych nie należy bagatelizować nawet w językach takich, jak C++. Na przykład napisanie operatora przypisania = zamiast
porównania == wewnątrz warunku if kompiluje się
zwykle bez problemu, a powoduje błędne działanie programu.
Dużo gorsze są jednak tzw. błędy logiczne. Takie błędy to po prostu niedoprojektowany, nie przemyślany albo źle napisany kod, chociaż poprawny składniowo. Czasami powodują one wywłaszczenie znane też jako błąd ochrony albo wykonanie niedozwolonej operacji (niedopilnowany wskaźnik), zawieszenie się programu (nieskończona pętla), przepełnienie stosu (nieskończona rekurencja), a czasami po prostu dziwne i błędne zachowanie programu. W dalszej części tego podrozdziału opiszę metody znajdowania takich błędów. Szukanie błędów w kodzie jest sztuką i wymaga przede wszystkim dużego doświadczenia. Jedną z najważniejszych metod na szukanie błędów jest kontrola przepływu sterowania, czyli co, kiedy i w jakiej kolejności się wykonuje. Czasami Wystarczy wiedzieć, czy jakiś fragment kodu albo jakaś funkcja w ogóle jest wykonywana, a czasami potrzebne są dokładniejsze informacje. Beep Najprostszą, ale zaskakująco skuteczną techniką jest wstawianie do kodu wywołań funkcji Beep().
Ta funkcja z nagłówka <windows.h> wydaje dźwięk z systemowego
głośniczka zwanego też PC Speaker. Jeśli więc nie podłączałeś go kiedy
składałeś swojego peceta, czym prędzej napraw ten błąd.
Nad możliwościami tej funkcji długo możnaby się rozwodzić. Przyjmuje ona dwa parametry - częstotliwość dźwięku (w hercach) i czas jego trwania (w milisekundach). Wstawienie do kodu takiego wywołania, jak pokazane poniżej, pozwala na stwierdzenie, czy dany fragment w ogóle się wykonuje i kiedy się wykonuje. Beep(1000, 100); W różnych miejscach kodu można wstawić takie wywołania z różnymi częstotliwościami (zwykle ok. 500-2000 Hz) i czasami (zwykle ok. 50-2000 ms), co pozwoli je rozróżniać. Parametrami tymi mogą nawet sterować aktualne wartości zmiennych. Dwa następujące po sobie, takie same wywołania Beep() nie brzmią jak jedno, ale wyraźnie słychać przerwę między
nimi.
Logi Innym sposobem są drukowania kontrolne (logi). Możesz zorganizować sobie jakąś możliwość zapisywania tekstu, a do różnych miejsc w kodzie wstawiać instrukcje zapisujące takie drukowania. Dzięki temu dowiesz się, co i w jakiej kolejności było wykonywane. Możesz także zapisywać wartości zmiennych po przetworzeniu ich na łańcuchy. Takim miejscem do zapisywania logów mogą być:
Żeby znaleść błąd, czasami wystarczy przejrzeć swój kod - otworzyć w odpowiednich miejscach te pliki, w których twoim zdaniem ukrył się błąd i poczytać własny kod myśląc, jak będzie się wykonywał - wykonując go w wirtualnej maszynie programisty. Warto też skonfrontować użycie niektórych funkcji bibliotecznych z ich dokumentacją, żeby upewnić się, że podawane są do nich właściwe parametry. Bardzo ciekawa jest też metoda pluszowego misia - próbujesz wytłumaczyć misiowi, jak działa twój kod, a miś znajdzie błąd. Z pewnością domyślasz się, jak to możliwe - próbując precyzyjnie i dokładnie opisać zasadę działania swojego kodu masz okazję jeszcze raz go przemyśleć i uchwycić błędy, sprzeczności i niedorzeczności, które mogły się do niego wkraść w czasie jego pisania. Praca krokowa Każde poważne zintegrowane środowisko programistyczne (IDE) posiada wbudowany debugger, a w nim możliwość pracy krokowej. Warto umieć używać tego potężnego narzędzia, bo dzięki niemu dużo łatwiej jest znajdować błędy. Stawia się w kodzie pułapki (ang. "breakpoint"), uruchamia się program, a w chwili wykonywania kodu z tych wybranych miejsc wykonania zostają wstrzymane. Można wtedy śledzić wartości zmiennych i robić różne inne rzeczy, ale przede wszystkim można posuwać się w wykonywaniu kodu po jednej linijce przeskakując je (polecenie "Step Over") lub wchodząc do wnętrza wywoływanych funkcji (polecenie "Step Into"). Dużą pomocą jest też podglądanie stosu wywołań (ang. "Call Stack"). Widać na nim aktualny stan zagnieżdżonych wywołań funkcji - jedna funkcja wywołała drugą, ta następną itd. aż do miejsca aktualnie wykonywanego kodu. Komentowanie kodu Kiedy wszystko inne zawodzi, trzeba odwołać się do najbardziej żmudnych i nieprzyjemnych metod. Prezentowana tu technika to ostateczność, a jej używanie jest konieczne właściwie tylko w jednym przypadku - kiedy debugowanie (praca krokowa) jest niemożliwe. Np. dzieje się tak, kiedy program pisany w Visual C++ działa poprawnie w wersji "Debug", a wysypuje się w wersji "Release" (w której debugowanie jest niemożliwe). Wspomniana technika to obejmowanie w komentarz pewnych fragmentów kodu czy wywołań funkcji, a tym samym ograniczanie funkcjonalności programu. Możesz wykomentować te wywołania, które twoim zdaniem mogą powodować błąd, skompilować program i sprawdzić, czy bez nich faktycznie błąd nie występuje. Z drugiej strony konieczne może się okazać wykomentowanie prawie całego kodu, a następnie stopniowe wprowadzanie do niego kolejnych części jego pełnej funkcjonalności. Tak postępując można zawężać zakres poszukiwań aż do znalezienia miejsca w kodzie, które powodowało problemy. Podsumowanie Przy okazji warto wspomnieć o jeszcze jednej rzeczy, która odróżnia początkujących od zaawansowanych programistów. Początkujący napotkawszy jakiś większy błąd często stają w kropce i rezygnują z dalszego pisania. Dla zaawansowanych praktycznie nie ma błędów nie do znalezienia - wszystko jest kwestią odpowiedniego czasu i pracy oraz użycia odpowiednich metod. Jednak są błędy jeszcze gorsze, niż wszystkie opisane powyżej. To błędy, które dają o sobie znać tylko czasami i są praktycznie nieuchwytne - objawiają się w różnych chwilach i w różnych miejscach, zupełnie ze sobą niezwiązanych. Najgorsze jest to, że czasami nie wiadomo, jak można je spowodować, a tym samym nie sposób ustalić ich przyczyny i miejsca w kodzie. Co robić z takim błędem? Można pozostawić (może u innych go nie będzie :), można dalej go szukać (tylko ile czasu i pracy to jeszcze zajmie?), a można napisać dany moduł zupełnie od nowa. Najważniejsze jednak, by takich okropnych błędów po prostu nie robić. Ich przyczyną najczęściej są niedopilnowane wskaźniki. Dlatego zawsze jasno definiuj, w jakich chwilach obiekty alokowane dynamicznie istnieją, kto jest odpowiedzialny za ich tworzenie i usuwanie z pamięci. Na przykład jeśli w każdej chwili obiekt może nie istnieć (wskaźnik do niego jest równy 0 - NULL) albo może istnieć jeden
(wtedy wskaźnik pokazuje na niego), to podczas usuwania tego obiektu nie
zapomnij wyzerować wskaźnika.
Testy Drugim zagadnieniem, które chciałbym poruszyć w tym rozdziale jest testowanie napisanego już (przynajmniej do pewnego stopnia) i działającego poprawnie (przynajmniej na pozór) programu. Każdy program i każda gra po napisaniu przechodzi etap testów, w którym różni ludzie (nie tylko programiści) używają go znajdując przy tym błędy i zgłaszając swoje uwagi. Testy Alfa Testowanie każdego napisanego fragmentu programu, który już działa i nadaje się do uruchomienia to czynność, którą my - programiści - przeprowadzamy instynktownie. Nie ma sensu robić z tego żadnej wielkiej i systematycznej nauki, chociaż taką się robi. Jako autor programu, który zna jego kod, możesz odruchowo dobierać testy, które najlepiej sprawdzą jego poprawność - wprowadzając wartości graniczne albo nawet spoza zakresu, wpisując litery w miejsce na cyfry itp. - to są tylko najprostsze przykłady. Z pewnością wiesz, o czym mówię. Takie testowanie programu przez autora i inne osoby zaangażowane w projekt to alfa-testy. Wersja Alfa to wersja programu przeznaczona do testów wewnętrznych. Testy Beta Wersja Beta to wersja testowa programu przeznaczona do testów zewnętrznych - dla wszystkich lub tylko wybranych i upoważnionych osób spoza kręgu jego twórców. To bardzo ważne, by pokazać swoją produkcję innym osobom, które nie brały udziały w jego pisaniu. Warto, żeby niektóre z nich w ogóle nie były programistami. Osoby niezaangażowane mogą po prostu spojrzeć na projekt z boku, obiektywnie, całościowo i zgłosić uwagi według swoich indywidualnych cech. Dlatego warto zebrać opinie od wielu osób. Można udostępnić wersję Beta swojego programu i poprosić, aby zainteresowane osoby pobrały go, przetestowały i wysłały do ciebie raport z testu. Należy ich zachęcić, żeby pisali w nim:
Bardzo niepokojące jest, że takie z pozoru drobne błędy projektowe wychwytywane przez niezależnych testerów na tym etapie testowania są często niezwykle trudne do poprawienia. Tak jest ze wszystkimi konsekwencjami złego zaprojektowania. Im wcześniej popełniony błąd (etap projektowania, analizy czy już określania wymagań), tym gorsze są jego konsekwencje i tym trudniej go naprawić. Jeden punkt w notatkach projektu może przekładać się na tysiące linii kodu w programie. Błędna decyzja na pierwszym etapie - w fazie strategicznej - dyskredytuje cały projekt. Dlatego najlepiej, abyś projekty programu jeszcze przed rozpoczęciem pisania pokazał i przedyskutował z jakimś kolegą-programistą. Dzięki temu unikniesz wielu dalszych kłopotów. Pamiętaj, że zagłębiając się w szczegóły i myśląc dużo o danym programie tracisz możliwość obiektywnego spojrzenia na niego "z boku". Optymalizacja Kiedy już program działa i to działa poprawnie, można pomyśleć o zwiększeniu jego wydajności. W dziedzinie pisania gier ma to szczególne znaczenie i warto walczyć o każdą dodatkową klatkę na sekundę :) Mówi się, że przez 90% czasu wykonuje się tylko 10% kodu. Oznacza to, że nie warto przerabiać wszystkiego, co skomplikowane, ważne i co mogłoby się wydawać przeznaczone do optymalizacji. Trzeba znaleźć te miejsca w kodzie, które z racji częstego uruchamiania czy wykonywania się bardzo wiele razy (w pętli) mają decydujący wpływ na wydajność i właśnie im przyjrzeć się bliżej patrząc, co można zrobić w celu ich przyspieszenia. Do znajdowania tych tzw. wąskich gardeł są specjalne programy zwane profilerami. W praktyce często wystarcza jednak wstawienie do kodu odpowiednich wywołań, które pomierzą, ile czasu zajmuje wykonanie instrukcji zawartych między nimi. ojProfiler.Begin(); // ... MojProfiler.End();[ Wystarczy odczytać czas na początku, czas na końcu i odjąć je od siebie. Możesz napisać własny moduł profilujący z takimi funkcjami i wyposażyć go w możliwości zapisywania wyników pomiarów do jakiegoś loga, np. do pliku tekstowego. ZakończenieZ zamiarem napisania tego artykułu nosiłem się już od dłuższego czasu, ale trudno mi było zebrać się na odwagę. W końcu to jest jeden z takich tekstów, który zamiast skupiać się na suchych zagadnieniach technicznych, opisuje sprawy bardziej ogólne, a także zawiera osobiste doświadczenia i opinie autora.Zdążyłem już dwa razy rozpocząć i porzucić pisanie tego tekstu. Wreszcie jednak udało mi się go skończyć. Mówiąc szczerze, umotywowała mnie zachęta i oczekiwanie na ten tekst ze strony ludzi z Warsztatu oraz przyjacielska atmosfera, jaka mimo licznych sporów panuje na kanale #warsztat i na Game Design Forum. Tak więc teraz, w przededniu I Ogólnopolskiej Konferencji Twórców Gier Komputerowych - pierwszego w historii zlotu Warsztatu - oddaje w wasze ręce ten tekst i mam nadzieję, że opisane w nim moje doświadczenia pomogą wam wypracować własne metody na solidne, systematyczne podejście do programowania, a tym samym zwiększyć ilość ukończonych produkcji, którymi możecie się pochwalić. Na koniec pragnę podziękować wszystkim tym, którzy na ten artykuł czekali i pokładali w nim pewne nadzieje - mam nadzieję, że się nie zawiedliście. Szczególnie podziękowania należą się temu, który zawsze niestrudzenie recenzuje moje teksty zgłaszając cenne uwagi - a jest nim Xion. Literatura
| ||||||||||||||||||||||||||||||||||||
|