•  
  •  
  •  
  •  
  •  
  •  
  •  

Zadanie rekrutacyjne Java w 60 minut

Jeśli wybierasz się na rozmowę rekrutacyjna to na pewno zainteresuje Cię zadanie rekrutacyjne Java jakie dostałem do rozwiązania. Czas na zaimplementowanie zadania to 60 minut. Frameworki i biblioteki do wykorzystania dowolne. Opiszę tutaj jak to zrobiłem w trakcie interview oraz jakbym to zrobił w sytuacji mniej stresującej.

Treść zadania rekrutacyjnego Java:

Korzystając z publicznego REST API: https://jsonplaceholder.typicode.com napisz aplikację klienta, która będzie pobierać listę osób (users) wraz ich zadaniami do wykonania (todos). Aplikacja powinna pobierać i wypisywać pobrane dane cyklicznie co 5 sekund.

Aplikacja powinna być odporna na błędy połączenia z API.

Pobrane dane powinny być wydrukowane w następującej postaci:

User #{id} ({userName})
	[*]	task: {title}
	[ ]	task: {title}
...

Przykład:

User #7 (Elwyn.Skiles)
	[*]	task: inventore aut nihil minima laudantium hic qui omnis
	[*]	task: provident aut nobis culpa
	[ ]	task: esse et quis iste est earum aut impedit
	[ ]	task: qui consectetur id
	[ ]	task: aut quasi autem iste tempore illum possimus

Wytyczne

Brak. Niestety pomimo tego, że dopytywałem rekruterów czy lepiej aby zadanie było zrobione jak najszybciej czy jak najładniej to niestety nie uzyskałem konkretnej odpowiedzi. Dla rekruterów, w teorii, najważniejsze było aby zadanie było kompletne (skończone) czyli żeby działało.

Strategia

Ponieważ najważniejsze było stworzenie działającego prototypu stwierdziłem, że spróbuję stworzyć coś co działa i realizuje założenia a potem (jeśli będę miał na to czas) będę się martwił jak upiękrzyć poprzez refactoring.

Wstępna analiza

Stwierdziłem, że najlepiej będzie zabrać się za zadanie etapami:

  1. Klient REST
  2. Mapping obiektów domenowych
  3. Wydrukowanie danych w odpowiednim formacie
  4. Cykliczne wykonywanie zadania

Rozwiązanie:

Zadanie rozpocząłem od wygenerowania projektu z generatora Spring Initializer. Dzięki temu miałem od razu szkielet projektu wraz z wymaganymi zależnościami. Stwierdziłem jednak, że dopóki nie będę wyraźnie potrzebował framework-a Spring to nie będę go używał. Okazało się, że Spring jednak nie był potrzebny.

1 . Klient REST

Pierwsze co przyszło mi do głowy to pytanie jakiej biblioteki najlepiej użyć aby połączyć się po REST. I tutaj pomyślałem od razu o nowości w JDK 11 i nowym kliencie HTTP.

Przy okazji jeśli jesteś zainteresowany zmianami licencyjnymi Java od wersji 11 przeczytaj mój post na temat tego czy Java jest nadal darmowa?

Po chwili jednak namysłu stwierdziłem, że ponieważ nie używałem tego API wcześniej wiec lepiej będzie użyć coś sprawdzonego o czym będzie łatwo znaleźć informacje więc zdecydowałem się na Apache HTTP Client.

HttpHost target = new HttpHost("jsonplaceholder.typicode.com", 80, "http");
HttpGet getRequest = new HttpGet("/users");
HttpResponse httpResponse = httpclient.execute(target, getRequest);

2. Mapping obiektów domenowych

Do zmapowanie danych JSON zwróconych przez REST API użyłem biblioteki GSON.

String jsonStations = EntityUtils.toString(entity);
Type listType = new TypeToken<ArrayList<User>>() {}.getType();
return new Gson().fromJson(jsonStations, listType);

3. Wydrukowanie danych w odpowiednim formacie

Wymaganie co do tego jak ma wyglądać output jest na tyle proste, że do wydrukowania zwróconych danych użyłem najprostszej metody czyli System.out.println oraz metody String.format.

for (User user : users) {
  System.out.println("User #" + user.getId() + " (" + user.getUsername() + ")");
  List<Todo> todos = getTodosByUserId(user.getId());
  for (Todo todo : todos) {
    System.out.println(String.format("\t[%s] task: %s",
                                     todo.getCompleted() ? "*" : " ",
                                     todo.getTitle()));
  }
}

Dodatkowo w podsumowaniu zadania stwierdziłem, że można by tutaj użyć jakiegoś systemu szablonów typu Velocity lub Freemarker.

Myślę, że kolejną alternatywą było by tu wykorzystanie loggera (np. Logback).

4. Cykliczne wykonywanie zadania

Jak do tej pory w trakcie rozwiązywania zadanie nigdzie nie potrzebowałem używać framework-a Spring. Uznałem, że skoro do tej pory nie był potrzebny to bez sensu go dodawać tylko po to aby użyć adnotacji @Scheduled. Stworzyłem więc w prymitywny sposób nowy wątek z pętlą while:

Thread t = new Thread(() -> {

  while (!Thread.currentThread().isInterrupted()) {
    printUsersWithTasks();
    try {
      Thread.sleep(TimeUnit.SECONDS.toMillis(10));
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

});
t.start();

Podsumowanie rozwiązania

Jak się okazało do rozwiązania całego zadania niepotrzebnie założyłem, że będzie potrzebny Spring. Zadanie rekrutacyjne z Java było na tyle proste, że obeszło się bez niego. Czy jednak było to rozwiązanie oczekiwane?

Rozwiązanie zadania zajęło mi około 30 minut. Po tym czasie omówiłem co bym ulepszył i zapytałem czy wykonać zaproponowane zmiany:

  1. Wspomniałem o brakujących testach i stwierdziłem, że jeśli zadanie nie było by ograniczone czasowo i priorytet byłby ustawiony na jakość a nie finalne działanie kodu to rozpoczął bym od napisania testów.
  2. Fallback oraz dodanie error handling-u w przypadku niepowodzenia wykonania call-a REST-owego i zwracanie rezultatu np. z lokalnego cache-a.
  3. Zamiana ręcznego startowania wątku na inny mechanizm np. adnotację Spring @Scheduled

Jak bym to zrobił gdybym miał to zrobić ponownie? Czyli jak rozwiązać zadanie rekrutacyjne Java po rekrutacji.

1. Generowanie szkieletu

Rozwiązując podane zadanie rekrutacyjne z Java drugi raz, również rozpoczął bym od wygenerowanie szkieletu z udziałem generatora Spring Initializr.

Spring Initializr

Z dependencji na początek dodam tylko:

  • Lombok
  • OpenFeign

Rozplanowanie komponentów

Czyli stworzenie struktury nie tylko pakietów ale również klas, które będą stanowić rozwiązanie zadanego problemu. To właśnie coś czego zabrakło podczas tworzenia mojego rozwiązania na rozmowie kwalifikacyjnej. W trakcie kodowania skupiłem się za bardzo na celu a nie na drodze do jego osiągnięcia.

Warstwę logiki aplikacji podzielmy na poniższe komponenty:

zadanie rekrutacyjne java - diagram komponentów
  • Scheduler – Klasa odpowiadająca tylko i wyłącznie za cykliczne wykonywanie zadania
  • FallbackSupportService – Klasa w której dzieje się najwięcej. Jest tu obsłużenie sytuacji wyjątkowej gdzie w przypadku braku połączenia z serwisem REST zwracamy dane trzymane w kopii zapasowej.
  • UsersProvider – Klasa odpowiedzialna za dostarczanie danych o użytkownikach i ich zadaniach ToDo.
  • Printer – Klasa pomocnicza służąca do wydrukowania pobranych danych przy użyciu Loggera.

2. Deklaratywny klient REST

Jedną z najbardziej kontrowersyjnych decyzji w tym zadaniu jest wybór klienta REST. Opcji jest dość sporo, i każda znajdzie swoich zwolenników jak i przeciwników. Do wyboru mamy między innymi:

Po rozpatrzeniu wszystkich opcji zdecydował bym się na użycie Feign zamiast Apache HTTP Client. Kod dzięki użyciu tej biblioteki stanie się bardziej czytelny i odrazu będziemy mięć podział na komunikację REST i logikę biznesową.

@FeignClient(name = "usersClient", url = "https://jsonplaceholder.typicode.com")
public interface UsersClient {

    @GetMapping("/users")
    List<User> users();
}

@FeignClient(name = "tasksClient", url = "https://jsonplaceholder.typicode.com")
public interface TasksClient {
    @GetMapping("/todos")
    List<Task> getTasksBy(@RequestParam("userId") Integer userId);

}

2. Domena bez zmian (prawie)

Jeśli chodzi o klasy domenowe odpowiadające za przechowywanie danych z otrzymanego JSON-a to pozostały by takie jak w oryginalnym rozwiązaniu. Z jedną zmianą: W klasie User dodał bym listę zadań, które należą do danego użytkownika.

@Data
public class User {
    Integer id;
    String name;
    String username;
    String email;

    List<Task> tasks; // here is the change
}
      
@Data
public class Task {
    Integer id;
    Integer userId;
    String title;
    Boolean completed;
}

3. Printer jako osobny komponent

Po zastanowieniu uznałem, że wydrukowanie danych na ekran, choć bardzo prymitywne, powinno być jednak odseparowane do osobnej klasy. Dodatkowo zrezygnowałem z systemowego println na rzecz loggera.

@Service
@Slf4j
public class Printer {
    void printUsersWithTasks(List<User> users) {
        users.stream().forEach(user -> {
            log.info(MessageFormat.format("User #{0} ({1})", user.getId(), user.getUsername()));
            printTasksFor(user);
        });
    }
    private void printTasksFor(User user) {
        user.getTasks().stream().map(task -> MessageFormat.format("\t[{0}] task: {1}])",
                task.getCompleted() ? "*" : " ",
                task.getTitle())).forEach(log::info);
    }
}

4. Prawdziwy Scheduler

Skoro i tak w rozwiązaniu alternatywnym wszędzie korzystam z dobrodziejstw frameworka spring, więc tym razem oczywistością jest użycie adnotacji @Scheduled do ustawienia cyklicznego wykonywania zadania.

@Component
public class Scheduler {

    private final FallbackSupportService fallbackSupportService;

    public Scheduler(FallbackSupportService fallbackSupportService) {
        this.fallbackSupportService = fallbackSupportService;
    }

    @Scheduled(fixedDelay = 10000)
    public void logAllUsersTasks() {
        fallbackSupportService.getUsersAndPrint();
    }
}

5. Wsparcie dla niepowodzenia

I na koniec coś czego nie zakodowałem w oryginalnym rozwiązaniu – Fallback support. Po napisaniu zadania i stwierdzeniu, że działa ono według wytycznych wspomniałem jedynie, jak można by dodać obsługę takich sytuacji niespodziewanych gdzie nasz serwis od którego jesteśmy zależni przestaje działać. Teraz natomiast pokażę jak bym to obsłużył:

@Slf4j
@Component
public class FallbackSupportService {

    private final UsersProvider usersProvider;
    private final Printer printResultService;

    public FallbackSupportService(UsersProvider usersProvider, Printer printResultService) {
        this.usersProvider = usersProvider;
        this.printResultService = printResultService;
    }

    private List<User> backupUsers = null;

    @HystrixCommand(fallbackMethod = "printCachedResults",
            commandProperties = {
                    @HystrixProperty(name = "execution.timeout.enabled", value = "false"),
                    @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "1"),
                    @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "10")
            })
    void getUsersAndPrint() {
        backupUsers = usersProvider.getUsersWithTasks();
        printResultService.printUsersWithTasks(backupUsers);
    }

    @SuppressWarnings("unused")
    public void printCachedResults() {
        if (null != backupUsers) {
            log.warn("Current data are from backup!");
            printResultService.printUsersWithTasks(backupUsers);
        } else {
            log.error("Ups... we have some issues. No results... try again later...");
        }
    }
}

FallbackSupportService jest odpowiedzialny za wykonanie całego zadania a jeśli coś pójdzie nie tak (brak połączenia REST) to niech serwis sobie radzi i wydrukuje dane poprzednio pobrane.

Użyłem tutaj bardzo wygodnego rozwiązania jakim jest Circut Braker – Hystrix. Gdzie możemy podać w adnotacji metodę fallback-ową, która będzie wołana w przypadku gdy danych obwód zostanie otwarty z powodu zbyt dużej ilość niepowodzeń.

Hystrix udostępnia również dashboard na którym możemy monitorować status obwodu:

Hystrix Dashboard

Hystrix dashboard możemy aktywować poprzez dodanie odpowiedniej adnotacji:

@SpringBootApplication
@EnableScheduling
@EnableFeignClients
@EnableCircuitBreaker
@EnableHystrixDashboard
public class RestclientApplication {

	public static void main(String[] args) {
		SpringApplication.run(RestclientApplication.class, args);
	}

}

Podsumowanie

Jak widać stres i ograniczony czas bardzo wpływa na to jakie rozwiązanie możemy dostarczyć gdy mamy przed sobą takie czy inne zadanie rekrutacyjne z Java.

Druga kwestia to wyczucie czego oczekuje się od takiego rozwiązania. Dla jednej osoby najważniejsze będzie aby zadanie po prostu działało a dla drugiej zadanie nie musi być skończone ale powinno być perfekcyjnie napisane. Kwestia preferencji.

Trzecia rzecz to jakich technologii użyjemy. Jednym może się podobać zrealizować tak prostego zadania przy użyciu tylko i wyłącznie core Java i nie używanie żadnych bibliotek pomocniczych. Ktoś inny będzie oczekiwał użycia najnowszych framework-ów aby kod był zwięzły i czytelny.

  •  
  •  
  •  
  •  
  •  
  •  
  •  

3 komentarze na temat “Zadanie rekrutacyjne Java w 60 minut

  1. Świetny wpis. Fajna konstrukcja artykułu z wnioskami i dwoma rozwiązaniami. Brakuje mi w polskiej blogosferze javy takich moco inspirujących wpisów. Też jutro siadam do tego zadania tylko z innymi bibliotekami, których do tej pory nie znałem a tutaj wymieniłeś. Może warto abyś wrzucał podobne wpisy częściej? Co tydzień tekst zadania i rozwiązanie na szybko. A za kilka dni rozwiązanie po Bożemu. To byłoby super, szeroki rozwój w ramach jednego wpisu na blogu!

  2. Bardzo fajny wpis, możesz dodać link do GH? Według mnie nie wziąłeś pod uwagę trzech rzeczy, klient wspierający http 2.0, klient z nieblokującym IO, a także timeoutów socketa, reada, czy też ustawień trzymania połączenia. Bardzo fajną prezentację na tej temat miał Adam Dubiel ze trzy lata temu na Confiturze.

  3. Dzieki za wpis! Bardzo ciekawie wiedziec czego chca na rozmowach – oby takich wiecej 🙂
    Ja swoje rozwiazanie w jednej klasie w ciagu 45 minut podaje – troche sie meczylem, bo zapomnialem o @EnableScheduling i \n w printf 😀


    @Service
    public class ApiService {

    private final WebClient webClient;

    public ApiService(WebClient.Builder webClientBuilder) {
    this.webClient = WebClient.builder().build();
    }

    @Scheduled(fixedDelay = 5000)
    public void perform() {

    List users = getUsers();

    List tasks = getTasks();

    tasks.forEach(task -> {
    users.get(task.getUserId() - 1).getTasks().add(task);
    });

    printUsersTaskStatus(users);
    }

    private List getUsers() {
    User[] usersArray = webClient.get().uri("https://jsonplaceholder.typicode.com/users")
    .retrieve()
    .onStatus(HttpStatus::isError, response -> {
    System.out.println("Error");
    return Mono.error(new RuntimeException("Error"));
    })
    .bodyToMono(User[].class).block();

    return Arrays.asList(usersArray);
    }

    private List getTasks() {
    Task[] tasksArray = webClient.get().uri("https://jsonplaceholder.typicode.com/todos")
    .retrieve()
    .onStatus(HttpStatus::isError, response -> {
    System.out.println("Error");
    return Mono.error(new RuntimeException("Error"));
    })
    .bodyToMono(Task[].class)
    .block();
    return Arrays.asList(tasksArray);
    }

    private void printUsersTaskStatus(List users) {
    users.forEach(user -> {
    System.out.printf("User #%d (%s) \n", user.getId(), user.getUsername());
    user.getTasks().forEach(task -> {
    String status = Boolean.parseBoolean(task.getCompleted()) ? "*" : " ";
    System.out.printf("\t[%s] task: %s \n", status, task.getTitle());
    });
    });
    }
    }

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *