Jak dobrze napisać programistyczne zadanie rekrutacyjne
Ostatnio zajmuję się oceną zadań rekrutacyjnych w firmie, w której pracuję. Z tych doświadczeń, a także z feedbacku, jaki sam otrzymywałem przy różnych okazjach, wyłania mi się koncepcja tego, jak chciałbym, żeby wyglądało dobrze napisane zadanie. Będę pisał z perspektywy backendowca, ale postaram się tak sformułować myśli, żeby były możliwie najbardziej uniwersalne.
Zdaję sobie sprawę z tego, że moje doświadczenie jest dość wąskie i na pewno nie będzie uniwersalne. Niektóre rady mogą się spotkać ze sprzeciwem. Takie potencjalnie kontrowersyjne zaznaczę i liczę na dyskusję z Wami w komentarzach.
Jedna złota rada
Gdybym skondensować wszystko, co zaraz napiszę, w jednym zdaniu, byłoby to:
„Pisz aplikację tak, jakbyś miał ją faktycznie utrzymywać”.
Wiem, że na studiach często liczy się odbębnienie funkcjonalności, ale w pracy będziesz się zajmować poważnymi rzeczami.
Jakie zatem grzechy popełniają kandydaci (w tym i ja)?
Po pierwsze, chyba najważniejsze, to mieszanie wysokopoziomowych abstrakcji z niskopoziomowymi szczegółami. Przykładem czegoś takiego może być wsadzanie całego kodu do części odpowiedzialnej za obsługę zapytania HTTP. Funkcja albo metoda, która dostaje zapytanie, powinna jedynie wykorzystać zależności do przeprowadzenia operacji parsowania danych, sprawdzania uprawnień i przetworzenia danych. Rozważmy takie dwa rozwiązania (kod pisany bez związku z jakimkolwiek frameworkiem). Który handler jest łatwiejszy do zrozumienia?
def handler(request):
if not request.user or not request.user.had_permission("update_model"):
return Response(401)
try:
data = json.loads(request.data)
if data["some_value"] < 0:
return Response(400, detail="some_value can't be less than 0")
except JsonException:
return Response(400, detail="Invalid JSON")
OrmModel.query.where(user=request.user).update(data)
return Response(200)
def handler(request, permission_checker, serializer, updater):
permission_checker(request.user)
data = serializer(request)
updater(data)
return Response(200)
W pierwszym przypadku wszystkie szczegóły są wywleczone na wierzch. Jakakolwiek zmiana tak naprawdę dotyka wszystkiego. Zmiana sposobu robienia zapytań do bazy danych może mieć wpływ na sprawdzanie uprawnień. W drugim rozwiązaniu wstrzykujemy zależności i w nich zarządzamy. Kod jest dużo bardziej czytelny, zmiany wprowadzamy w miejscach, które mają jasno zdefiniowaną odpowiedzialność.
Kolejna sprawa to obsługa gita. Powszechne podejście traktowania gita jako narzędzia do zipowania kodu jest bolączką projektów. Absolutnym minimum dla mnie jest klarowny commit message i squash commitów robiących to samo. Widok pięciu commitów pod rząd z wiadomością „fixes”, albo „asdf” nie świadczy dobrze o podejściu kandydata.
Podobnie commity, które zawierają kod z jakimiś chronionymi danymi. Jeśli przypadkiem wrzuciliśmy do repozytorium jakiś klucz prywatny, to trzeba usunąć commita. Dobrze byłoby stworzyć branche na poszczególne funkcje. Idealnie byłoby mieć jeden chroniony branch dopuszczający kod tylko jeśli przejdzie testy automatyczne.
Jeśli przy tym jesteśmy - CI zawsze będzie miło widziane. GitHub i GitLab umożliwiają za darmo odpalenie testów i warto z tego skorzystać.
Do tego potrzebne są jednak testy. Czasami jest to sztywne wymaganie, czasami nie. Jeśli jest, to należy je napisać. Jeśli nie, to… I tak należy je napisać. Nawet jeśli nie robimy TDD, to testy w dłuższej perspektywie ułatwiają życie a w końcu piszemy aplikację tak, jakbyśmy mieli ją utrzymywać.
Może się zdarzyć tak (co zresztą ma odbicie w prawdziwej pracy), że czas nas goni tak bardzo, że nie możemy testować wszystkiego. Co wówczas należy zrobić? Testujemy najważniejsze funkcjonalności. Jeśli dostęp do jakiegoś zasobu ma być tylko dla zalogowanych użytkowników, to sprawdźmy przynajmniej, czy zablokujemy dostęp dla kogoś, kto nie jest zalogowany. Jeśli mamy przyznać zniżkę, to sprawdźmy chociaż poprawność algorytmu w oderwaniu od kontekstu - zapytań HTTP, bazy danych itd.
No dobra, ale nie tylko na testy może zabraknąć czasu. Co wtedy? Napisać wszystko byle jak, czy napisać część rzeczy dobrze? Ja preferuję drugą opcję. Może się zdarzyć tak, że jakaś firma będzie wolała pierwsze podejście (tu zaznaczam potencjalny konflikt, o którym pisałem na początku), ale w tym momencie warto się zastanowić, czy chcemy pracować gdzieś, gdzie standardem jest robota robiona na „odwal się”. Moim zdaniem normalnym jest, że pisząc zadanie rekrutacyjne będziemy je robić w czasie wolnym, nie zawsze będąc w stanie poświęcić dostatecznie dużo godzin, żeby zrobić wszystko i jeszcze zrobić to dobrze.
Sprawdzając zadania, staram się ocenić to, jak myśli kandydat, a nie to, czy aplikacja daje „wartość biznesową”. Czasami można wynegocjować dodatkowe kilka dni na zakończenie zadania, czasami może się okazać, że to, co zaimplementowaliśmy, może wystarczyć. Warto tu porozmawiać.
To wszystko?
Nie, ale te rady pokrywają jakieś 80-90% moich uwag do kodu, który przeglądam. Te bolączki widziałem w kodzie kandydatów na wszystkie stanowiska - od juniorów po seniorów, więc śmiem przypuszczać, że nie jest to wiedza powszechna. Nie gwarantuję, że trzymając się tych wytycznych, dostaniesz pracę, ale gwarantuję, że zwiększysz swoje szanse. Powodzenia.