Programowanie to sport zespołowy. Duże programy są tworzone przez zespoły, lub wiele współpracujących zespołów. Bywa i tak, że nikt nie ma wiedzy o całym systemie, a każdy zna tylko jego fragmenty. Jak sensownie funkcjonować w takich realiach? Oto kilka zasad dobrego warsztatu programisty.
Dla kogo piszemy kod? Dla komputera? Ależ skąd! Piszemy dla siebie i swoich współpracowników. Inaczej, po skompilowaniu pierwszej wersji programu, możnaby kod źródłowy wyrzucić. Tymczasem program, który jest używany, będzie wielokrotnie modyfikowany, przez nas i naszych kolegów. Modyfikacje same są zwykle niewielkie, ale wymagają przeczytania większego fragmentu kodu. Wniosek: kod źródłowy jest więcej razy czytany, niż pisany. Powinniśmy więc go tak pisać, żeby się łatwo czytało i modyfikowało, a nie żeby jak najszybciej stworzyć pierwszą wersję.
Wcięcia muszą być! Kod bez wcięć nie będzie oceniany. Wszytkie szczegóły dotyczące wcięć wynikają z tego po co one są: wcięcia mają ułatwić szybkie dopasowanie początku i końca konstrukcji składniowej.
Wielkość wcięć: od 2 do 4, w zależności od smaku, ale konsekwentnie stała. Minimalna wielkość wcięcia to 2, bo inaczej "schodki" nie są widoczne. Maksymalna to 4, bo inaczej kod robi się strasznie szeroki.
W pozostałych szczegółach, możecie dobrać Państwo styl, który najbardziej Wam odpowiada, byle konsekwentnie. Tutaj można znaleć przykładowy dobry styl.
Robimy odstępy wokół operatorów binarnych i po znakach interpunkcyjnych. Nie robimy odstępów po wewnętrznej stronie nawiasów. Na przykład: 2 + 2 * 4, [2; 3; 4], (2, 3 - 4).
Odstępy w pionie (puste wiersze) też są ważne. Między definicjami procedur robimy wiersz lub dwa odstępu. Wewnątrz dłuższych procedur możemy też robić pojedyncze wiersze odstępu dla podkreślenia struktury kodu.
W podejściu do komentarzy można spotkać dwie skrajności:
Oba podejścia są złe. Owszem, kod ma być przejrzysty i ma być widać co się w nim dzieje, ale zawsze są pewne aspekty, które z niego nie wynikają, np. co jest celem? po co coś się dzieje? co zakładamy? Z drugiej strony, umieszczanie w komentarzach "relacji na żywo" z obliczeń mija się z celem i tylko utrudnia czytanie kodu. Podstawowa zasada brzmi: Komentarze mają ułatwić czytanie i zrozumienie kodu. Odnosi się to tak samo do innych programistów, z którymi współpracujemy, jak i do nas samych tylko za jakiś czas. Oto kilka typowych miejsc, w których warto umieszczać komentarze:
Identyfikatory powinny być znaczące. Jeśli z nazwy identyfikatora wynika czym jest argument, to często nie trzeba nic więcej o nim pisać w komentarzach.
Identyfikatory (nazwy stałych i procedur) powinny zaczynać się z małej litery. Wieloczłonowe nazwy proponuję łączyć podkreśleniem: taki_sobie_identyfikator.
Program, który jest rozwijany podlega ciągłym zmianom. Jak sprawdzić, czy kolejne zmiany nie wprowadzają nowych błędów do programu? Testować! Warto utrzymywać bogaty zestaw testów, które po każdej zmianie w programie możemy łatwo uruchomić. Testy nie zagwarantują nam poprawności kodu, ale odpowiedni zestaw testów potreafi wykryć większość błędów.
Pisząc kod, powinniśmy odrazu tworzyć dla niego testy. Dla każdej funkcjonalności / procedury piszemy testy. Mamy jakąś elementarną procedurę wykorzystywaną w naszym kodzie -- piszemy dla niej testy. Mamy procedurę integrującą kilka procedur pomocniczych w coś większego -- piszemy dla niej testy. Tak jak procedury tworzą hierarchię, tak testy tworzą swoistą piramidę. Powiniśmy mieć wiele prostych testów dla elementarnych procedur (unit testy), oraz mniejszą liczbę testów-scenariuszy symulujących sposób użycia całego systemu.
Jak pisać testy w Ocamlu? Najlepiej wykorzystać operację assert <warunek>;. (Uwaga na ";". Narazie nie wnikamy w imperatywność.) Sprawdza ona, czy warunek jest spełniony i jeśli nie, przerywa obliczenia. Gdy uruchamiamy kod "dla użytkownika", możemy wyłączyć sprawdzanie asercji opcją -noassert. Testy mogą być po prostu asercjami na poziomie definicji (tj. nie zagnieżdżonymi wewnątrz definicji). Umieszczanie asercji wewnątrz definicji procedur też ma sens, ale to już coś innego niż testowanie.
Dwie najpopularniejsze techniki pisania kodu, mające na celu wyeliminowanie błędów, to code reviews i pair-coding. Pair-coding polega na pisaniu razem w parach. Jedna osoba pisze kod, a druga śledzi i komentuje ten proces. Obie muszą rozumieć co się dzieje i być przekonane, że powstający kod jest poprawny. Code review polega na tym, że kod, lub zmiana w kodzie, napisana przez jednego programistę jest przeglądana przez innego programistę. Przeglądający może zgłaszać zastrzeżenia i wskazywać co należy poprawić. Zasadniczo reviewer nie wprowadza poprawek sam, z wyjątkiem drobiazgów. Tę właśnie technikę zastosujemy na Laboratorium.
Jeśli pracujemy w zespole wieloosobowym nad programem składającym się z wielu plików, to jak synchroizować wyniki swojej pracy? Do tego właśnie służą systemy kontroli wersji. System kontroli wersji pamięta:
Starsze systemy kontroli wersji (RCS) wymagały blokowania plików w celu wprowadzania zmian w nich. Nowsze systemy kontroli wersji (Subversion, Git) pozwalają na równoczesne wprowadzanie zmian w tych samych plikach przez wielu programistów. W praktyce dobrze się to sprawdza. Jednak zmiany takie nie zawsze można automatycznie scalić. Czasami konieczna jest interwencja człowieka.
Git jest aktualnie chyba najpopularniejszym systemem kontroli wersji. Git jest systemem rozproszonym. Na każdym komputerze biorącym udział w tworzeniu programu znajduje się repozytorium zawierające kompletną (choć niekoniecznie aktualną) informację o historii wersji programu. Repozytoria mogą wymieniać między sobą informacje o wprowadzanych zmianach. Zwykle każdy programista ma jedno repozytorium lokalne na swoim komputerze, oraz istnieje jedno zdalne repozytorium, służące synchronizowaniu zmian.
W Git'ie konkretna wersja programu to commit. Commit ma poprzedni commit i wiadomo jakie zmiany względem niego wprowadza. Jeden poprzedni commit może mieć wiele następnych commitów (fork). Możliwy jest też commit o dwóch poprzednich commitach (merge), scalający zmiany wprowadzone od ich wspólnego poprzednika.
Zwykle myślimy w kategoriach gałęzi (branch), a nie commit'ów. Branch to wskaźnik na konkretny commit. W miarę wprowadzania zmian do kodu, wskaźnik ten jest przesuwany na kolejne commity. Zwykle mamy główną gałąź (trunk/master) oraz wiele gałęzi pobocznych, tworzonych na potrzeby opracowania poszczególnych zmian i łączonych potem z główną gałęzią.
Git bardzo dobrze wspiera przeglądy kodu. Typowy cykl opracowania zmiany w kodzie wygląda następująco:
Istnieje wiele serwisów oferujących przechowywanie zdalnego repozytorium, np. GitHub, BitBucket. Można też do tego wykorzystać współdzielony system plików, choć właściwa kontrola praw dostępu wymaga pewnie utworzenia mnóstwa grup. Jeżeli będziecie Państwo przechowywać swoje programy zaliczeniowe w repozytoriach, to proszę zadbać, żeby nie były one publicznie dostępne (przynajmniej dopóki nie minie ostateczny termin oddawania programów).