W zeszłym tygodniu pisałem o tym, jak przejść perfekcyjnie rozmowę techniczną na Junior Programistę. W tym tygodniu pokażę Ci, na co zwracam uwagę podczas oceniania zadania rekrutacyjnego w czasie jej trwania. Zatem, zadanie rekrutacyjne Junior Programista, czas zacząć.
Lecimy!
Kontekst
Wróćmy na chwilę do procesu rekrutacyjnego. Być może zacząłeś go od zadania domowego, potem była rozmowa z HR, a teraz przyszedł czas na rozmowę techniczną (kolejność może być odmienna).
Podczas rozmowy technicznej sprawdzam umiejętności kandydata poprzez rozwiązanie krótkiego zadania z programowania.
Tak, jak pisałem we wcześniejszym artykule, podczas tej rozmowy na żywo stosuję dużo prostsze zadanie, niż te, które np. w czasie rekrutacji kandydat może rozwiązywać w domu. Zadanie jest prostsze, ale stanowi wyzwanie ze względu na to, że jest ono jednak na żywo i jest oceniane. I ma też ograniczony czas.
W kwestiach technicznych. Zainspirowany sposobem przeprowadzania rozmów online, które realizuje Clément Mihailescu, zacząłem też stosować Google Docs i czasem Jamboard w czasie rozmowy rekrutacyjnej. Generalnie Clément prowadzi kanał, który jest bardzo ciekawy i ma mnóstwo świetnych porad dla osób początkujących. Dlatego go polecam wszystkim, którzy chcą rozwinąć swoje umiejętności.
Pomysł na zadanie rekrutacyjne przyszedł mi do głowy również dzięki jego nagraniom. W jednym z nich problem do rozwiązania również zaczynał się od klawiatury telefonicznej, choć zadanie było o wiele bardziej skomplikowane. Nie zmienia to faktu, że klawiatura telefoniczna stała się głównym tematem mojego zadania rekrutacyjnego dla przyszłego junior programisty.
Problem do rozwiązania
Zadanie, które stosowałem podczas ostatniej rekrutacji wyglądało następująco:
Zapewne znasz klawiaturę w starych telefonach. Np. taką w Nokii 3310 lub Sony Ericsson K750i:
W tych starych modelach, żeby napisać jakąś literę należało kilka razy wcisnąć dany przycisk. I tak np. żeby uzyskać A należało wcisnąć przycisk 2 jeden raz, a żeby uzyskać Z to przycisk 9 cztery razy.
Dlatego np. słowo KOT oznaczało wciśnięcie klawiszy 556668.Twoim zadaniem jest napisanie metody, która dowolne słowo zamieni właśnie na taki ciąg cyfr.
Ciekawy jestem, czy Twoim zdaniem to zadanie rekrutacyjne jest proste, czy trudne na stanowisko Junior Programista. Tylko … weź pod uwagę, że jest ono robione na żywo, w Google Docs i z niejawnym ograniczeniem czasowym.
Nie muszę chyba dodawać, że kolejne rekrutacje, gdy będę miał okazję je prowadzić od strony technicznej, będą z zupełnie innym zadaniem 😊
Co oceniam w tym zadaniu?
Przede wszystkim zależy mi na poznaniu tego, w jaki sposób dana osoba dochodzi do rozwiązania. W tym procesie wg mnie najważniejsza jest umiejętność dekompozycji problemu.
W tym przykładzie może wydawać się to na wyrost. Ale nawet tutaj obserwator może zwrócić uwagę, że jednym z elementów rozwiązania może być poszukiwanie możliwości dalszego rozszerzenia modelu np. o inne klawiatury z innymi zestawami znaków.
Dlatego warto zwrócić uwagę, żeby już na początku nie zaczynać pisać linijek kodu, a poświęcić minutę, czy dwie, na spokojne zastanowienie się, jak podejść do zadania. A także zadać dodatkowe pytania odnośnie szczegółów.
Drugi element, jaki oceniam, to sposób opowiadania o kodzie, który właśnie się tworzy na żywo. Dzięki temu mogę stwierdzić, czy dany kandydat ułożył jakiś plan, czy raczej działa po omacku (i może się uda).
Zadanie rozwiązywane jest w Google Docs. Także nie zależy mi w tym miejscu na tym, żeby kod był w wersji „kompiluje się bez problemów”. W tym momencie oceniam ogólne podejście do kodowania. Mogę tu stwierdzić m. in. czy dana osoba już orientuje się w podstawach JDK, jakich konstrukcji używa, być może wzorców i czy samo pisanie kodu nie sprawia jej problemu.
Na końcu oczywiście istotne jest też to, czy rozwiązanie ma potencjał do poprawnego działania. Czyli w tym miejscu jest to dosyć zero-jedynkowa odpowiedź: tak, ten kod będzie działał po oszlifowaniu w IDE lub nie, nie będzie działał, nie ma szans.
Dekompozycja
Generalnie problem nie jest skomplikowany. Także nie należy się w nim doszukiwać jakichś ukrytych zawiłości.
Najłatwiej zacząć od zdekomponowania problemu na mniejsze fragmenty. Tutaj wydaje się, że tych małych bloczków będzie kilka:
- Po pierwsze należy określić, jak ma wyglądać nasza metoda, czyli jej typy argumentów wejściowych i wyjściowych.
- Następnie widzimy, że należy rozwiązać kwestię relacji między literami a przyciskami.
- W dalszej kolejności warto zastanowić się, jak połączyć wszystkie dane, aby uzyskać wynik.
Wejście i wyjście
Na podstawie danych w zadaniu można bardzo szybko dojść do metody o takiej postaci:
public String encode(String text) {
...
}
Bardzo prosta. Przyjmuje jeden argument, nasz wejściowy text. Na wyjściu zwraca również wynikowy napis.
Co tutaj można zrobić lepiej?
Przede wszystkim warto zastanowić się, co by było, gdyby klawiatura była inna (inny alfabet, fragment alfabetu) lub gdybyśmy chcieli te układy klawiszy wymieniać. Wówczas elementem rozwiązania mogłoby być dodanie drugiego argumentu do metody, tak żeby uwzględniać taką możliwość. Argumentem tym mogłoby być właśnie mapowanie liter i przycisków.
Co może tu pójść nie tak?
Z moich obserwacji wynika, że w tym momencie najgorszą rzeczą, jak może się stać jest pominięcie tego elementu. Co to właściwie znaczy?
Chodzi o sytuację, gdy od razu rozpoczyna się pisanie kodu bez wydzielenia metody. Przede wszystkim prośba o napisanie metody jest w poleceniu. Jest to element zadania. Z drugiej strony od razu rozpoczęcie kodowania bez wydzielenia metody np. pętli for(…) w tym przykładzie prowadzi do rozwiązania niekompletnego, które nie jest podatne na dalszą rozbudowę.
Jak przechowywać relację liter i przycisków?
Sposobów jest kilka, ale … zwykle rozwiązanie najprostsze jest najlepsze. Ewidentnie narzuca się, że relacja między literami i ciągami cyfr jest rodzajem mapowania. Dlatego bardzo naturalnym rozwiązaniem jest tu wykorzystanie Map:
private static final Map<String, String> mapper = new HashMap<>() {{
put("A", "2");
put("B", "22");
put("C", "222");
put("D", "3");
...
}};
Rozwiązanie wydaje się proste. Z powyższej mapy od razu widzimy potrzebną relację: litera A to 2, B to 22, itd.
Co można tu zrobić lepiej?
W tym miejscu można pokusić się o próbę uogólnienia i wprowadzenie interfejsu, którego implementacje zwracałyby poszczególne ciągu cyfr dla danej litery. Jedna z takich implementacji mogłaby korzystać z przedstawionej powyżej mapy, inna być może z innych elementów.
W każdym razie w rekrutacji na stanowisko Junior Programista nie oczekuję takiego uogólnienia. Ale bardzo chętnie zobaczyłbym w tym miejscu wydzielenie dostępu do tej mapy poprzez jakąś metodę. Dlaczego? Żeby na przykład obsłużyć sytuację braku litery w naszej mapie i zwrócenie null.
Co może pójść tutaj jednak nie tak?
Na tym etapie najtrudniejszym elementem jest dobór struktury danych. Im prostsza tym lepsza. Dlatego utrudnienie sobie tego zadania poprzez specyficzną strukturę danych będzie prawdopodobnie prowadziło do licznych błędów w kodzie. Tak może być np. z tablicami dwuwymiarowymi, zbiorami (Set), listami par litera – przycisk oraz klasami reprezentującymi przyciski (choć to podejście może iść w stronę obiektowości). Innym podejściem, które niechybnie prowadzi do błędów, będzie próba wyliczania liczby cyfr dla danej litery na podstawie kodów ASCII.
Łączymy wszystko
Teraz kiedy mamy już dwa elementy nadszedł czas na połączenie ich w całość. Można to zrobić na wiele sposobów. Ja przygotowując to zadanie podzieliłem je sobie na trzy grupy rozwiązań:
- podstawowe,
- średniozaawansowane,
- podejście z rekurencją.
Dlaczego tak?
Przede wszystkim chcę sprawdzić podstawowe umiejętności kodowania. Średniozaawansowane rozwiązanie będzie na pewno miłym zaskoczeniem. Tak samo, gdy ktoś wpadnie na rozwiązanie z rekurencją.
Podstawy są ważne
Rozwiązanie podstawowe. Używamy tutaj prostych konstrukcji, kod nie zawiera streamów i specjalnych fajerwerków. Zabezpieczam sobie przy okazji dostęp do mapy. Przykład poniżej:
private String fromMapper(String letter) {
return mapper.getOrDefault(letter, "");
}
public String encodeSimple(String word) {
String result = "";
if (Objects.nonNull(word)) {
for (int i = 0; i < word.length(); i++) {
String letter = String.valueOf(word.charAt(i));
result += fromMapper(letter);
}
}
return result;
}
I tutaj moja dodatkowa uwaga. Na stanowisko Junior Programista tak rozwiązane zadanie rekrutacyjne podczas rozmowy online jest tym, które jest przeze mnie oczekiwanym. Bez komplikacji, podstawowe umiejętności kodowania przedstawione. Krótki czas rozwiązania, po prostu działa.
Co można tu pójść nie tak?
Przede wszystkim na tym etapie zdarzają się jakieś drobne pomyłki. Jedną z nich jest wpisanie indeksów na sztywno, np. text.charAt(0). To proste rzeczy, które w rozmowie o tym kodzie szybko można skorygować (dlatego tak ważne jest mówienie o swoim kodzie w trakcie pisania).
Inne rzeczy, na które rzadko zwracana jest uwaga to typy danych. Warto pamiętać, że char to nie to samo co String.
Jeszcze inny przykład to założenie, że w danych wejściowych nie będzie null. To nie jest duży problem, bo zadanie nie polega na sprawdzaniu null, ale prawie nikt na to nie zwraca uwagi podczas pisania (dlatego ja czasem pytam, a co by było gdyby …).
Rozwiązanie z wykorzystaniem reduce
Jeśli popatrzymy na ten problem z innej perspektywy to możemy zaobserwować, że łączenie danych możemy zrealizować nieco inaczej. Nasz wynik jest tak naprawdę połączeniem pustego łańcucha znaków z kolejnymi łańcuchami znaków dla każdej litery w naszym wyrazie będącym argumentem metody. Dlatego średniozaawansowane rozwiązanie mogłoby wyglądać następująco:
public String encodeWithReduce(String word) {
if (Objects.nonNull(word)) {
String result = word.chars()
.mapToObj(c -> String.valueOf((char) c))
.reduce("", (accumulator, element) -> accumulator + fromMapper(element));
return result;
}
return "";
}
Co można tu pójść nie tak?
Nie miałem jeszcze takiego rozwiązania na stanowisko Junior Developer. Nie trudno tu jednak wskazać, że w tym rozwiązaniu trzeba już trochę więcej wiedzieć na temat strumieni, trzeba dobrze zmapować typ wchodzący do reduce i poprawnie przeprowadzić operację.
Rozwiązanie rekurencyjne
O to, czy kandydat widzi możliwość rozwiązania problemu rekurencyjnie staram się pytać prawie zawsze. Jestem ciekawy, czy wpadnie na takie rozwiązanie. Podobnie, jak przy reduce, w tym przypadku pomaga uzmysłownie sobie, że tak naprawdę przetwarzanie argumentu wejściowego jest przetwarzaniem głowy łańcucha znakowego i rekurencyjnie jego ogona. W ten sposób można dojść do takiego rozwiązania:
public String encodeWithRecursion(String word) {
if (Objects.nonNull(word) && word.length() > 0) { // sorry, nie wiem czemu tutaj higlighter uparł się na wstawienie encji HTML
var mapped = fromMapper(String.valueOf(word.charAt(0)));
return mapped + encodeWithRecursion(word.substring(1));
}
return "";
}
Co można tu pójść nie tak?
Z dotychczasowych rekrutacji wiem, że rekurencja stanowi duży problem dla kandydatów. Wyodrębnienie odpowiednich fragmentów przetwarzania jest ważnym elementem, który tak naprawdę przychodzi z praktyką. Do tej pory nie miałem jeszcze przypadku, żeby ktoś rozwiązał to zadanie w ten sposób. Ale tak, jak i pisałem wcześniej, na stanowisko Junior Developer to rozwiązanie nie jest wymagane. Ale byłoby fajnym zaskoczeniem.
Tu przy okazji trzeba wiedzieć, jak zachowa się substring, żeby nie generować dodatkowych if’ów.
Podsumowanie
Mam nadzieję, że tym tekstem pomogę Tobie w lepszym przygotowaniu się do Twojej rozmowy kwalifikacyjnej na stanowisko Junior Programista.
Pewnie sam widzisz, że przykładowe zadanie nie jest nadmiernie trudne. Ale nawet w nim można znaleźć kilka ciekawych możliwości i miejsc, gdzie można wyraźnie pokazać swoje umiejętności.
Także daj znać, jeśli ten tekst jest dla Ciebie wartościowy. A tak z ciekawości, czy takie zadanie rekrutacyjne na stanowisko Junior Programista jest wg Ciebie łatwe czy trudne?
Jeśli masz jakieś pytanie, chcesz się czegoś dowiedzieć? Śmiało, wrzucaj komentarz. A może masz jakieś swoje przemyślenia na temat rekrutacji na Junior Programistę?
Witam,
dzięki za tekst – podziwiam ludzi, którzy mają czas, ochotę i (tak podejrzewam) czerpią radość z dzielenia się wiedzą.
Tekst przeczytałem z dużym zainteresowaniem i ze smutkiem przyznaję przed samym sobą, że jestem jakże typowym juniorem – nie tylko podczas stresującej rozmowy rekrutacyjnej, ale i w zaciszu domowego biurka nie wpadłbym na rozwiązanie rekurencyjne…
Ale ja nie w tym temacie. Zastanawia mnie, jak zaprezentowana metoda, zdekoduje na przykład napis: DEMON, który zakodowany będzie wyglądał tak:
333666666
3(D)33(E)6(M)66(O)666(N).
Pozdrawiam serdecznie,
Robert
Cześć, dzięki za komentarz. W tym zadaniu dekodowanie nie jest istotne, choć samo w sobie stanowiłoby ciekawy problem na inną rekrutację. Chodzi tylko o zakodowanie tekstu.
Całe życie powtarzam sobie – musisz być uważniejszy. Nie przeczytałem z uwagą treści zadania i założyłem, że proces powinien przebiegać w dwie strony 😉 (ech nigdy nie przestanę być pre-juniorem).
I jeszcze będę się teraz zamęczał, jak przeprowadzić takie dekodowanie – wydaje mi się, że nie jest to wcale takie trywialne.
Moim zdaniem jest to niemożliwe, jeśli wejściowy ciąg nie jest podzielony na poszczególne litery 🙂
Ale swoją drogą ciekawym zadaniem byłoby wypisanie wszystkich możliwych słów dla danego ciągu cyfr. Słów poprawnych lub nie 😉
Super artykul i ogolnie z calym cyklem z nieba mi spadasz 🙂 obecnie szukam pracy jako junior developer, mialem przyjemnosc rozwiazywania zadan rekrutacyjnych, ale brakowalo mi wlasnie takich porad jak do tego podejsc.
Dzięki. Znajdziesz tu więcej takich treści. Polecam również cykl z początku roku: https://bartlomiejchmielewski.pl/junior-developer-2020-podsumowanie/
Czy rekurencję traktujesz jako coś pozytywnego czy jako niepotrzebne komplikowanie prostego zadania?
Podczas takiego zadania live przede wszystkim chciałbym zobaczyć proste rozwiązanie. Na etapie juniorskim myślenie w kategoriach rozwiązań rekurencyjnych nie jest zwykle tym pierwszym, które przychodzi do głowy. I jest w mojej opinii bardzo zależne od ścieżki kształcenia. Natomiast zadanie można rozwiązać w prosty sposób i zastanowić się jeszcze, czy da się je zrobić inaczej. Rekurencja jest w tym przypadku fajnym elementem, którym można pokazać, że wie się więcej. I to bym traktował na plus.
Witam, Mógłbym prosić o informację, jak oceniłbyś rozwiązanie przedstawione poniżej. Porównując to do przedstawionych rozwiązań podejrzewam, że przeglądaniem mapy po wartościach i wyciąganie klucza nie jest dobrze widziane. Czy przedstawienie takiego rozwiązania dyskwalifikuje kandydata? private static final Map mapper = new HashMap() {{ put(1, „”); put(2, „ABC”); put(3, „DEF”); put(4, „GHI”); put(5, „JKL”); put(6, „MNO”); put(7, „PQRS”); put(8, „TUV”); put(9, „WXYZ”); }}; public static String encode(String tekst) { if (tekst == null || tekst.length() == 0) { return „”; } tekst = tekst.toUpperCase(); String wynik = „”; for (int a = 0; a < tekst.length(); a++) { String c =… Czytaj więcej »
Cześć Mirek. To rozwiązanie zdecydowanie nie dyskwalifikuje kandydata. Najważniejsze w takim zadaniu jest pokazanie pomysłu. Tego, jak chcesz podejść do problemu. Nie chodzi tu o znalezienie jakiegoś najlepszego rozwiązania. Najważniejsze w tym przypadku to odpowiedzenie sobie na pytanie, jak wygląda dane zadanie i jak je zdekomponować. To jest w gruncie rzeczy najtrudniejsze podczas technicznych rozmów kwalifikacyjnych.
Dzień dobry chciałbym się zapytać jak bardzo krzywo rekruterzy by się patrzyli gdybym zrobił metodę która by wykorzystywała if else zamiast map? czy przykładowo tak
for(int x = 0; x <= letters.Length; x++)
{
if (letters[x] == 'A’)
{
numbers += „2”;
}
else if (letters[x] == 'B’)
{
numbers += „22”;
}
Hey. W tym przykładzie chodzi między innymi o dobór elastycznej metody mapowania liter na ciąg cyfr. Dlatego im mniej elastyczne rozwiązanie, a takim jest opcja z if’ami, tym takie rozwiązanie jest mniej korzystne.
Ciężko mi odpowiedzieć za inne osoby, które prowadzą rekrutacje od strony technicznej. Mogę tutaj podzielić się swoją oceną. Wersja z if’ami powinna w trakcie rozmowy zostać zrefactorowana – ja bym zapytał, czy to jest wg kandydata najlepsze podejście do mapowania.
Witam, Dopiero zaczynam przygodę z javą, stąd takie prymitywne rozwiązanie. Mimo wszystko, gdyby doszło modyfikacji klawiatury to wystarczy skorygować tablicę alfabetyczną. Co o tym sądzisz? public Long change(String str) { int[] alphArray = {0, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 9}; String lcStr = str.toLowerCase(); String wordToNumber = ""; int pressCounter = 0; if (str == null || str.length() == 0) { return 0000l;} for (int i = 0; i < str.length(); i++) { if ((alphArray[(int) (lcStr.charAt(i)) - 96]) != (alphArray[(int)… Czytaj więcej »
Cześć, dzięki za komentarz 👍o Popatrzyłbym na to rozwiązanie z kilku perspektyw. Pierwsza – czy kod będzie zrozumiały dla większej liczby osób. Moim zdaniem wiele osób będzie miało problem ze zrozumieniem działania tego kawałka. Dlatego nie byłbym fanem takiego podejścia, bo bez głębszego zastanowienia nie będziemy wiedzieć, dlaczego daje taki wynik. Druga perspektywa to przypadku brzegowe. Tutaj na pewno warto zastanowić się, czy pusty tekst na wejściu powinien zwracać 0 na wyjściu. Trzecia perspektywa to potencjalna rozbudowa. Czy można zmienić układ klawiatury, układ liter. Czy implementacja będzie to wspierać? Pamiętaj, że zawsze warto uproszczać kod i zadbać o jego czytelność… Czytaj więcej »