Najlepsze praktyki programistyczne
„Swój kod zawsze pisz w taki sposób, jakby osoba, która będzie go potem utrzymywać, była brutalnym psychopatą, który wie, gdzie mieszkasz”.
— John Woods
Kod produkcyjny to kod stosowany w produkcie używanym przez innych. Kiedy udostępniamy oprogramowanie jako produkcyjne, oznacza to, że oddajemy je światu. W tym rozdziale przedstawię kilka ogólnych zasad programistycznych, które mogą pomóc w pisaniu kodu nadającego się na kod produkcyjny. Wiele z tych zasad pochodzi z książki The Pragmatic Programmer Andy’ego Hunta i Dave’a Thomasa, książki która w zdecydowany sposób poprawiła jakość pisanego przeze mnie kodu.
Pisz kod w ostateczności
Twoim zadaniem jako programisty jest pisanie możliwie jak najmniejszej ilości kodu. Kiedy staniesz przed koniecznością rozwiązania problemu, Twoją pierwszą myślą nie powinna być: „Jak mam rozwiązać ten problem?”. Powinieneś raczej zadać sobie pytanie: „Czy ktoś inny rozwiązał już ten problem wcześniej i czy mogę użyć jego rozwiązania?”. Jeśli próbujesz rozwiązać problem, istnieje spore prawdopodobieństwo, że ktoś zrobił to już wcześniej. Zacznij zatem od poszukania rozwiązania problemu w internecie. Dopiero wtedy, kiedy upewnisz się, że nikt inny nie rozwiązał tego problemu, zacznij go rozwiązywać samodzielnie.
Zasada DRY
DRY to zasada programistyczna, której nazwa stanowi skrót angielskich słów Don’t Repeat Yourself — nie powtarzaj się. Chodzi w niej o to, by nie powtarzać w programie tego samego lub bardzo podobnego kodu. Taki kod należy umieścić w funkcji, której będzie można używać w wielu sytuacjach.
Prostopadłość
Prostopadłość (ang. orthogonality) to zasada spopularyzowana przez autorów książki Pragmatyczny programista. Jak wyjaśniają Hunt i Thomas: „W kontekście programowania termin ten zaczął
oznaczać pewną niezależność lub separację. Dwie lub większa liczba rzeczy są prostopadłe, jeśli zmiany w jednej z nich nie wywołują zmian w pozostałych. W dobrze zaprojektowanym systemie zmiany bazy danych będą prostopadłe do interfejsu użytkownika: będzie można zmienić interfejs użytkownika bez wpływu na bazę danych, jak również zmienić używaną bazę danych bez wpływu na interfejs użytkownika”. Aby przełożyć to na praktykę, zapamiętaj, że „a nie powinno wpływać na b”. Jeśli dysponujemy dwoma modułami, takimi jak moduł a oraz moduł b, to moduł a nie powinien zmieniać elementów modułu b i na odwrót. Jeśli zaprojektujemy system, w którym moduł a zmienia b, który z kolei zmienia moduł c, a ten zmienia moduł d, to sytuacja szybko wymknie się spod kontroli, a system stanie się niemożliwy lub trudny do zarządzania. Każdy element danych powinien mieć jedną reprezentację Jeśli używamy jakiegoś elementu danych, powinien on być przechowywany tylko w jednym miejscu. W ramach przykładu załóżmy, że piszemy oprogramowanie korzystające z numerów telefonów. Jeśli w naszym programie będziemy używać dwóch funkcji operujących na liście numerów kierunkowych, musimy się upewnić, że w programie będzie istnieć tylko jedna taka lista. Nie powinny się w nim pojawić dwie listy numerów kierunkowych, każda używana przez inną funkcję. Wszystkie numery kierunkowe powinny być zapisane w jednej zmiennej globalnej. Albo jeszcze lepiej: można je zapisać w pliku lub bazie danych. Problem z powielanymi danymi pojawia się w przypadkach, kiedy trzeba będzie je zmienić — gdyż będziemy musieli pamiętać, aby odpowiednio zmienić je w każdym miejscu, w jakim zostały powielone. Jeśli zmienimy listę numerów kierunkowych w jednej funkcji, lecz zapomnimy zrobić to w drugiej, która także używa tych numerów, nasz program przestanie działać prawidłowo. Problemu tego można łatwo uniknąć, dbając o to, by każdy element danych miał tylko jedną reprezentację.
Funkcje powinny robić tylko jedną rzecz
Każda funkcja powinna robić jedną i tylko jedną rzecz. Jeśli zauważymy, że nasze funkcje stają się zbyt długie, warto zadać sobie pytanie, czy realizują one tylko jedno zadanie. Ograniczenie funkcji do wykonywania tylko jednego zadania daje kilka korzyści. Kod stanie się łatwiejszy do czytania i analizy, gdyż nazwa funkcji będzie dokładnie opisywać jej przeznaczenie. W razie problemów z działaniem kod będzie łatwiejszy do testowania, gdyż każda funkcja będzie odpowiedzialna za konkretne zadanie, dzięki czemu będziemy mogli szybciej odszukać i zdiagnozować
problematyczną funkcję. Już wielu sławnych programistów stwierdzało: „Tak wiele złożoności w oprogramowaniu jest efektem tego, że jeden element kodu realizuje dwie różne rzeczy”.
Jeśli to trwa zbyt długo, zapewne robimy coś źle
Próbujemy zrobić coś, co w oczywisty sposób jest złożone, na przykład chcemy operować na dużych zbiorach danych. Jeśli wtedy uruchamianie programu trwa zbyt długo, prawdopodobnie robimy coś źle. Wykonuj operacje w optymalny sposób już od samego początku Jeśli podczas pisania kodu pomyślisz: „Wiem, że można to zrobić lepiej, ale jestem już w połowie pracy, więc nie chce mi się przerywać i zastanawiać się, jak to poprawić”, przerwij pracę! Nie kontynuuj pisania tego kodu — napisz go od początku lepiej.
Zachowaj zgodność z konwencjami
Poświęcenie czasu na poznanie konwencji stosowanych w nowym języku programowania ułatwi Ci czytanie i analizę kodu pisanego w tym języku. PEP 8 to grupa wytycznych dotyczących pisania kodu w języku Python, które warto przeczytać. Obejmuje ona także zasady kontynuowania pisania kodu w kolejnych wierszach. Zasady te są dostępne na stronie https://www.python.org/dev/peps/pep-0008/.
Rejestracja
Rejestracja (ang. logging) to praktyka polegająca na zapisywaniu danych podczas wykonywania programu. Z rejestracji można korzystać, by ułatwić sobie debugowanie programu oraz zyskać
dodatkowy wgląd w to, co się dzieje podczas jego wykonywania. Python udostępnia specjalny moduł do rejestracji o nazwie logging; pozwala on na wyświetlanie komunikatów konsoli
i zapisywanie ich w pliku. Czasami pisząc programy, robimy błędy, nikt jednak nie chce, by przeszły one niezauważone — dlatego warto zapisywać informacje o tym, co się dzieje w programie, tak by później można je było przeglądać. Rejestracja jest także przydatna do celów gromadzenia i analizy danych. Można by na przykład skonfigurować serwer w taki sposób, by rejestrował dane — w tym datę i godzinę — za każdym razem gdy zostanie odebrane żądanie. Wszystkie te informacje można zapisywać w bazie danych i napisać dodatkowy program, który będzie je analizować i wyświetlać wykres prezentujący na przykład liczbę odwiedzin w poszczególnych godzinach. Bloger Henrik Warne pisze: „Jedną z różnic pomiędzy dobrym oraz złym programistą jest to, że dobry programista dodaje do kodu mechanizmy rejestracji oraz tworzy narzędzia ułatwiające debugowanie programu, kiedy coś pójdzie w nim źle”. Więcej informacji na temat modułu logging można znaleźć na stronie https://docs.python.org/3/howto/logging.html.
Testowanie
Testowanie programu oznacza sprawdzenie, czy dany program spełnia wymagania, które stanowiły podstawy jego projektu i utworzenia, reaguje prawidłowo na wszelkiego rodzaju dane wejściowe, wykonuje swoje działania dostatecznie szybko, jest odpowiednio użyteczny, można go zainstalować w docelowych środowiskach oraz odpowiada ogólnym efektom oczekiwanym przez zleceniodawców lub zainteresowanych. W celu testowania programów programiści piszą kolejne programy. W środowisku produkcyjnym testowanie nie jest czynnością opcjonalną. Każdy program, który ma trafić do fazy produkcyjnej, należy uznać za niekompletny, jeśli nie zostaną do niego napisane odpowiednie testy. Jeśli jednak piszemy program na bieżące potrzeby, którego nie planujemy więcej używać, przygotowywanie dla niego testów może być stratą czasu. Jeśli tworzymy program, który będzie używany przez innych, to testy należy pisać. Jak powiedziało kilku sławnych programistów: „Kod nieprzetestowany jest kodem niedziałającym”. Informacje na temat modułu unittest dostępnego w języku Python można znaleźć na stronie https://docs.python.org/ 3/library/unittest.html.
Przeglądanie kodu
Przeglądanie kodu następuje wtedy, gdy ktoś czyta nasz kod i przekazuje nam swoje uwagi. Warto to robić jak najczęściej zwłaszcza wtedy, kiedy samodzielnie uczymy się programowania. Jeśli
nawet będziesz się starał stosować do wszystkich najlepszych praktyk opisanych w tym rozdziale, to i tak znajdą się rzeczy, które będziesz robić nieprawidłowo. Dlatego przyda Ci się ktoś
doświadczony, kto przeglądnie Twój kod i zwróci uwagę na popełniane błędy, byś mógł je poprawić. Code Review to witryna, na której można opublikować swój kod, by został skomentowany przez społeczność programistów. Każdy może z niej skorzystać i opublikować swój kod. Taki kod przeglądają członkowie społeczności Stack Exchange, a następnie przekazują informacje o tym, co zostało w nim napisane prawidłowo, oraz sugestie dotyczące tego, co można by w nim poprawić. Witryna ta jest dostępna pod adresem http://codereview.stackexchange.com/.
Bezpieczeństwo
Bezpieczeństwo to zagadnienie, które programista samouk może łatwo przeoczyć. Najprawdopodobniej na rozmowach kwalifikacyjnych nie padną żadne pytania dotyczące zagadnień bezpieczeństwa, które również nie odgrywają ważnego znaczenia w przypadku programów pisanych podczas nauki programowania. Niemniej jednak, kiedy już znajdziemy pierwszą pracę, staniemy się bezpośrednio odpowiedzialni za bezpieczeństwo pisanego kodu. W tym podrozdziale zamieszczę kilka wskazówek, które pozwolą o nie zadbać. Wcześniej w książce przedstawione zostało polecenie sudo pozwalające na wykonywanie programów z uprawnieniami administratora. Nigdy nie należy go używać do wykonywania programów, jeśli nie jest to absolutnie konieczne; kiedy hacker włamie się do programu, zyska uprawnienia administratora. Oprócz tego, jeśli zarządzamy serwerem, należy wyłączyć możliwość logowania się na konto root. Każdy hacker wie, że takie konto istnieje, dlatego jest ono jednym z podstawowych celów podczas ataków na system. Zawsze należy zakładać, że dane wejściowe podawane przez użytkownika są niebezpieczne. Istnieje kilka rodzajów ataków wykorzystujących błędy w programach pobierających dane od użytkowników — dlatego zawsze należy zakładać, że dane te są niebezpieczne i odpowiednio je traktować. Innym sposobem zabezpieczania programów jest minimalizacja powierzchni ataku — różnych obszarów programu, które napastnik może wykorzystać do gromadzenia danych lub przeprowadzenia ataku na system. Zapewniając, że powierzchnia ataku będzie możliwie jak najmniejsza, redukujemy także prawdopodobieństwo, że nasz program będzie podatny na atak. Oto kilka strategii minimalizacji powierzchni ataku: o ile to możliwe, należy unikać przechowywania poufnych informacji, należy przydzielać użytkownikom możliwie najniższy poziom dostępu, należy używać jak najmniej bibliotek pisanych przez innych twórców (im mniej kodu, tym mniej potencjalnych luk w zabezpieczeniach) i w końcu należy usuwać z kodu możliwości, które już nie są potrzebne (im mniej kodu, tym mniej potencjalnych zagrożeń). Unikanie logowania na konto administratora, traktowanie danych wpisywanych przez użytkowników jako niebezpieczne oraz minimalizacja powierzchni ataku to trzy ważne kroki pozwalające poprawić bezpieczeństwo programów. Jednak stanowią one jedynie punkt wyjścia. Zawsze należy starać się myśleć jak hacker. W jaki sposób hacker mógłby wykorzystać ten kod? Takie podejście pozwala odkrywać słabe punkty, które inaczej mogłyby zostać przegapione. Zagadnienie bezpieczeństwa jest zbyt obszerne, bym mógł przedstawić je dokładniej w tej książce, dlatego zawsze należy o nim myśleć i zdobywać informacje na jego temat. Najlepiej wyraził to Bruce Schneier: „Bezpieczeństwo to stan umysłu”.