Dołączenie do nowej firmy – z ustaloną kulturą i praktykami programistycznymi – może być zniechęcającym doświadczeniem. Kiedy dołączyłem do zespołu Ansible, postanowiłem spisać praktyki i zasady inżynierii oprogramowania, których nauczyłem się przez lata i które staram się stosować w swojej pracy. Jest to niedefiniowalna, niewyczerpująca lista zasad, które powinny być stosowane z rozsądkiem i elastycznością.
Moją pasją jest testowanie, ponieważ wierzę, że dobre praktyki testowania mogą zarówno zapewnić minimalny standard jakości (którego niestety brakuje w wielu produktach programistycznych), jak i kierować i kształtować sam rozwój. Wiele z tych zasad odnosi się do praktyk i ideałów testowania. Niektóre z tych zasad są specyficzne dla Pythona, ale większość nie. (Dla programistów Pythona, PEP 8 powinien być pierwszym przystankiem w poszukiwaniu stylu programowania i wytycznych.)
Ogólnie, my programiści mamy swoje zdanie, a silne opinie są często oznaką wielkiej pasji. Mając to na uwadze, nie krępuj się nie zgadzać z tymi punktami, a my możemy je omówić i przedyskutować w komentarzach.
Najlepsze praktyki rozwoju i testowania
1. YAGNI: „You Aint Gonna Need It”. Nie pisz kodu, który myślisz, że może być potrzebny w przyszłości, ale jeszcze go nie potrzebujesz. Jest to kodowanie dla wyimaginowanych przyszłych przypadków użycia, i nieuchronnie kod stanie się martwym kodem lub będzie wymagał przepisania, ponieważ przyszły przypadek użycia zawsze okazuje się działać nieco inaczej niż to sobie wyobrażałeś.
Jeśli umieścisz kod dla przyszłego przypadku użycia, zakwestionuję go podczas przeglądu kodu. (Możesz i musisz zaprojektować API, na przykład, aby umożliwić przyszłe przypadki użycia, ale to inna sprawa.)
Tak samo jest z komentowaniem kodu; jeśli blok skomentowanego kodu jest przeznaczony do wydania, nie powinien istnieć. Jeśli jest to kod, który może zostać przywrócony, należy zrobić ticket i odnieść się do hash commit’u usuwającego kod. YAGNI jest kluczowym elementem zwinnego programowania. Najlepszą referencją na ten temat jest Extreme Programming Explained, autorstwa Kenta Becka.
2. Testy nie potrzebują testów. Infrastruktura, frameworki i biblioteki do testowania potrzebują testów. Nie testuj przeglądarki ani zewnętrznych bibliotek, chyba że naprawdę tego potrzebujesz. Testuj kod, który piszesz, a nie kod innych ludzi.
3. Trzeci raz, kiedy piszesz ten sam kawałek kodu, jest właściwym momentem, aby wyodrębnić go do postaci pomocnika ogólnego przeznaczenia (i napisać dla niego testy). Funkcje pomocnicze w ramach testu nie potrzebują testów; kiedy je wyłamujesz i ponownie używasz, potrzebują testów. Do trzeciego napisania podobnego kodu, masz tendencję do posiadania jasnego pomysłu, jaki kształt ma problem ogólnego przeznaczenia, który rozwiązujesz.
4. Jeśli chodzi o projektowanie API (zewnętrzny interfejs API i obiektowy interfejs API): Proste rzeczy powinny być proste; złożone rzeczy powinny być możliwe. Zaprojektuj najpierw dla prostego przypadku, najlepiej z zerową konfiguracją lub parametryzacją, jeśli jest to możliwe. Dodaj opcje lub dodatkowe metody API dla bardziej złożonych i elastycznych przypadków użycia (jeśli są potrzebne).
5. Fail fast. Sprawdzaj dane wejściowe i zawiedź na bezsensownych danych wejściowych lub nieprawidłowym stanie tak wcześnie, jak to możliwe, najlepiej z wyjątkiem lub odpowiedzią na błąd, która sprawi, że dokładny problem będzie jasny dla twojego rozmówcy. Dopuszczaj jednak „innowacyjne” przypadki użycia twojego kodu (np. nie rób sprawdzania typu dla walidacji danych wejściowych, chyba że naprawdę tego potrzebujesz).
6. Testy jednostkowe testują jednostkę zachowania, nie jednostkę implementacji. Zmiana implementacji, bez zmiany zachowania lub konieczności zmiany któregokolwiek z twoich testów jest celem, choć nie zawsze możliwym. Więc tam gdzie to możliwe, traktuj swoje obiekty testowe jak czarne skrzynki, testując poprzez publiczne API bez wywoływania prywatnych metod lub majstrowania przy stanie.
W przypadku niektórych złożonych scenariuszy – takich jak testowanie zachowania na konkretnym złożonym stanie w celu znalezienia niejasnego błędu – może to nie być możliwe. Pisanie testów jako pierwsze naprawdę pomaga w tym, ponieważ zmusza cię do myślenia o zachowaniu twojego kodu i o tym jak zamierzasz go przetestować zanim go napiszesz. Testowanie w pierwszej kolejności zachęca do tworzenia mniejszych, bardziej modułowych jednostek kodu, co generalnie oznacza lepszy kod. Dobrą referencją do rozpoczęcia podejścia „testuj najpierw” jest Test Driven Development by Example, autorstwa Kenta Becka.
7. Dla testów jednostkowych (włączając w to testy infrastruktury testowej) wszystkie ścieżki kodu powinny być przetestowane. 100% pokrycie jest dobrym miejscem do rozpoczęcia. Nie można pokryć wszystkich możliwych permutacji/kombinacji stanu (eksplozja kombinatoryczna), więc to wymaga rozważenia. Tylko jeśli istnieje bardzo dobry powód, ścieżki kodu powinny pozostać nieprzetestowane. Brak czasu nie jest dobrym powodem i kończy się kosztowaniem więcej czasu. Możliwe dobre powody to: autentycznie nietestowalne (w jakikolwiek znaczący sposób), niemożliwe do trafienia w praktyce, lub ujęte w innym miejscu w teście. Kod bez testów to odpowiedzialność. Mierzenie pokrycia i odrzucanie PR-ów, które zmniejszają procent pokrycia jest jednym ze sposobów na zapewnienie stopniowego postępu w dobrym kierunku.
8. Kod jest wrogiem: Może się zepsuć i wymaga konserwacji. Pisz mniej kodu. Usuń kod. Nie pisz kodu, którego nie potrzebujesz.
9. Nieuchronnie, komentarze do kodu stają się z czasem kłamliwe. W praktyce niewiele osób aktualizuje komentarze, gdy coś się zmienia. Dąż do tego, aby twój kod był czytelny i samodokumentujący się poprzez dobre praktyki nazewnictwa i znany styl programowania.
Kod, który nie może być oczywisty – obejście jakiegoś niejasnego błędu lub nieprawdopodobnego warunku, albo konieczna optymalizacja – wymaga komentarza. Komentuj intencje kodu, i dlaczego robi coś, a nie co robi. (Ten szczególny punkt dotyczący komentarzy jako kłamstw jest kontrowersyjny, tak przy okazji. Ja nadal uważam, że jest poprawny, a Kernighan i Pike, autorzy książki The Practice of Programming, zgadzają się ze mną.)
10. Pisz defensywnie. Zawsze myśl o tym, co może pójść źle, co się stanie na niepoprawnych danych wejściowych i co może się nie udać, co pomoże ci złapać wiele błędów zanim się wydarzą.
11. Logika jest łatwa do testowania jednostkowego, jeśli jest bezstanowa i wolna od efektów ubocznych. Rozbij logikę na osobne funkcje, zamiast mieszać logikę z kodem bezpaństwowym i wypełnionym efektami ubocznymi. Rozdzielenie państwowego kodu i kodu z efektami ubocznymi na mniejsze funkcje sprawia, że są one łatwiejsze do wyśmiewania i testowania jednostkowego bez efektów ubocznych. (Mniejszy narzut na testy oznacza szybsze testy.) Efekty uboczne wymagają testowania, ale testowanie ich raz i wyśmiewanie wszędzie indziej jest ogólnie dobrym wzorcem.
12. Globale są złe. Funkcje są lepsze niż typy. Obiekty są prawdopodobnie lepsze niż złożone struktury danych.
13. Używanie wbudowanych typów Pythona i ich metod będzie szybsze niż pisanie własnych typów (chyba że piszesz w C). Jeśli wydajność jest istotna, spróbuj wymyślić, jak używać standardowych typów wbudowanych zamiast niestandardowych obiektów.
14. Wstrzykiwanie zależności jest użytecznym wzorcem kodowania, który pozwala jasno określić, jakie są twoje zależności i skąd pochodzą. (Obiekty, metody i tak dalej otrzymują swoje zależności jako parametry, zamiast same instantiować nowe obiekty). To sprawia, że sygnatury API są bardziej złożone, więc jest to kompromis. Kończąc z metodą, która potrzebuje 10 parametrów dla wszystkich swoich zależności jest dobrym znakiem, że twój kod robi zbyt wiele, tak czy inaczej. Ostatecznym artykułem na temat wstrzykiwania zależności jest „Inversion of Control Containers and the Dependency Injection Pattern,” autorstwa Martina Fowlera.
15. Im więcej musisz wyśmiewać, aby przetestować swój kod, tym gorszy jest twój kod. Im więcej kodu musisz zainicjować i umieścić w miejscu, aby móc przetestować konkretny fragment zachowania, tym gorszy jest twój kod. Celem są małe, testowalne jednostki, wraz z testami integracyjnymi i funkcjonalnymi wyższego rzędu, aby sprawdzić, czy jednostki te poprawnie współpracują.
16. Interfejsy API skierowane na zewnątrz są miejscem, gdzie „projektowanie z góry” – i rozważanie przyszłych przypadków użycia – naprawdę ma znaczenie. Zmiana API jest bolesna dla nas i dla naszych użytkowników, a tworzenie wstecznej niekompatybilności jest okropne (choć czasem niemożliwe do uniknięcia). Projektuj zewnętrzne interfejsy API ostrożnie, wciąż trzymając się zasady „proste rzeczy powinny być proste”.
17. Jeśli funkcja lub metoda przekracza 30 linii kodu, rozważ jej rozbicie. Dobrym maksymalnym rozmiarem modułu jest około 500 linii. Pliki testowe mają tendencję do bycia dłuższymi niż to.
18. Nie wykonuj pracy w konstruktorach obiektów, które są trudne do przetestowania i zaskakujące. Nie umieszczaj kodu w __init__.py (z wyjątkiem importu dla wyrównania nazw). __init__.py nie jest miejscem, w którym programiści spodziewają się znaleźć kod, więc jest to „zaskakujące”.”
19. DRY (Don’t Repeat Yourself) ma znacznie mniejsze znaczenie w testach niż w kodzie produkcyjnym. Czytelność pojedynczego pliku testowego jest ważniejsza niż utrzymanie (wyłamywanie kawałków nadających się do ponownego użycia). Dzieje się tak dlatego, że testy są wykonywane i czytane indywidualnie, a nie są częścią większego systemu. Oczywiście nadmierne powtarzanie oznacza, że komponenty wielokrotnego użytku mogą być tworzone dla wygody, ale jest to o wiele mniejszy problem niż w przypadku produkcji.
20. Refaktoryzuj, gdy tylko widzisz taką potrzebę i masz szansę. Programowanie polega na tworzeniu abstrakcji, a im bliżej abstrakcji do domeny problemu, tym łatwiejszy do zrozumienia i utrzymania jest kod. W miarę jak systemy rosną organicznie, muszą zmieniać strukturę dla swoich rozszerzających się przypadków użycia. Systemy przewyższają swoje abstrakcje i strukturę, a nie zmienianie ich staje się długiem technicznym, który jest bardziej bolesny (i wolniejszy i bardziej zabugowany) do obejścia. Uwzględnij koszt usunięcia długu technicznego (refaktoryzacji) w szacunkach dotyczących pracy nad funkcjonalnością. Im dłużej pozostawiasz dług, tym większe odsetki się od niego kumulują. Świetną książką na temat refaktoryzacji i testowania jest Working Effectively with Legacy Code, autorstwa Michaela Feathersa.
21. Spraw, aby kod był najpierw poprawny, a dopiero potem szybki. Podczas pracy nad problemami z wydajnością, zawsze profiluj przed wprowadzeniem poprawek. Zazwyczaj wąskie gardło nie znajduje się tam, gdzie myślałeś, że się znajduje. Pisanie niejasnego kodu, ponieważ jest on szybszy, jest warte zachodu tylko wtedy, gdy go sprofilowałeś i udowodniłeś, że faktycznie jest tego wart. Pisanie testów, które ćwiczą kod, który profilujesz z timingiem wokół niego sprawia, że wiesz, kiedy skończyłeś, jest łatwiejsze i może być pozostawione w zestawie testów, aby zapobiec regresji wydajności. (Z typową uwagą, że dodanie kodu mierzącego czas zawsze zmienia charakterystykę wydajnościową kodu, czyniąc pracę nad wydajnością jednym z bardziej frustrujących zadań.)
22. Mniejsze, ściślej ukierunkowane testy jednostkowe dają więcej cennych informacji, gdy zawiodą – mówią konkretnie, co jest nie tak. Test, który zajmuje połowę systemu, aby przetestować zachowanie, wymaga więcej badań, aby określić, co jest nie tak. Ogólnie rzecz biorąc, test, którego wykonanie zajmuje więcej niż 0.1 sekundy nie jest testem jednostkowym. Nie ma czegoś takiego jak powolny test jednostkowy. Dzięki ściśle ukierunkowanym testom jednostkowym testującym zachowanie, twoje testy działają jak de facto specyfikacja twojego kodu. Idealnie, jeśli ktoś chce zrozumieć twój kod, powinien być w stanie zwrócić się do zestawu testów jako „dokumentacji” zachowania. Świetną prezentacją na temat praktyk testowania jednostkowego jest Fast Test, Slow Test, autorstwa Gary’ego Bernhardta:
23. „Not Invented Here” nie jest tak złe, jak ludzie mówią. Jeśli piszemy kod, to wiemy, co on robi, wiemy, jak go utrzymywać i możemy go swobodnie rozszerzać i modyfikować według własnego uznania. Jest to zgodne z zasadą YAGNI: mamy specyficzny kod dla przypadków użycia, których potrzebujemy, a nie kod ogólnego przeznaczenia, który jest skomplikowany do rzeczy, których nie potrzebujemy. Z drugiej strony, kod jest wrogiem, a posiadanie większej ilości kodu niż jest to konieczne jest złe. Należy rozważyć kompromis podczas wprowadzania nowej zależności.
24. Wspólne posiadanie kodu jest celem; silosowa wiedza jest zła. Oznacza to co najmniej omawianie lub dokumentowanie decyzji projektowych i ważnych decyzji implementacyjnych. Przegląd kodu jest najgorszym momentem na rozpoczęcie dyskusji o decyzjach projektowych, ponieważ trudno jest przezwyciężyć inercję, aby dokonać gruntownych zmian po napisaniu kodu. (Oczywiście nadal lepiej jest wskazać i zmienić błędy projektowe w czasie przeglądu niż nigdy.)
25. Generatory dają czadu! Są one ogólnie krótsze i łatwiejsze do zrozumienia niż obiekty stanowe do iteracji lub wielokrotnego wykonywania. Dobrym wprowadzeniem do generatorów jest „Generator Tricks for Systems Programmers,” autorstwa Davida Beazley’a.
26. Bądźmy inżynierami! Myślmy o projektowaniu i budowaniu solidnych i dobrze zaimplementowanych systemów, zamiast hodować organiczne potwory. Programowanie to jednak sztuka balansowania. Nie zawsze budujemy statek rakietowy. Nadmiar inżynierii (architektura cebuli) jest równie bolesny w pracy, jak niedopracowany kod. Prawie wszystko, co napisał Robert Martin jest warte przeczytania, a Clean Architecture: A Craftsman’s Guide to Software Structure and Design jest dobrym źródłem wiedzy na ten temat. Design Patterns to klasyczna książka programistyczna, którą każdy inżynier powinien przeczytać.
27. Przerywane, nieudane testy obniżają wartość zestawu testów, do tego stopnia, że w końcu wszyscy ignorują wyniki testów, ponieważ zawsze coś się nie udaje. Naprawianie lub usuwanie testów, które zawodzą sporadycznie, jest bolesne, ale warte wysiłku.
28. Generalnie, szczególnie w testach, czekaj na konkretną zmianę, a nie śpij przez dowolną ilość czasu. Voodoo sleeps są trudne do zrozumienia i spowalniają Twój zestaw testów.
29. Zawsze sprawdź, czy Twój test przynajmniej raz się nie powiedzie. Umieść w nim celowy błąd i upewnij się, że się nie powiedzie, lub uruchom test zanim testowane zachowanie zostanie ukończone. W przeciwnym razie nie będziesz wiedział, że naprawdę coś testujesz. Przypadkowe pisanie testów, które w rzeczywistości niczego nie testują lub które nigdy nie mogą zawieść, jest łatwe.
30. I na koniec, punkt dla kierownictwa: Ciągłe mielenie funkcji jest okropnym sposobem na rozwijanie oprogramowania. Nie pozwalając programistom być dumnymi ze swojej pracy, nie wydobędziesz z nich tego, co najlepsze. Nierozwiązanie problemu długu technicznego spowalnia rozwój i skutkuje gorszym, bardziej zabugowanym produktem.