• Zadanie rekrutacyjne Java w 2 tygodnie

  • --> --> -->
  •  
  •  
  •  
  •  
  •  
  •  
  •  

Zadanie rekrutacyjne Java w 2 tygodnie

W poprzednim moim wpisie: Zadanie rekrutacyjne Java w 60 minut opisałem jak poradziłem sobie z rozwiązaniem zadania programistycznego podczas rozmowy rekrutacyjnej i jak bym to zrobił ponownie – już po rozmowie. Natomiast w tym artykule przedstawię zadanie domowe, które dostałem do rozwiązania jako wstęp przed interview. Na rozwiązanie zadania dostałem dwa tygodnie.

Oto zbiór wymagań jakie otrzymałem:

Serwis do oceniania filmów:


1. Docker
    – Do przygotowania kontenery dla uruchomienia całego środowiska 
    – Skrypt .sh do uruchomienia projektu
    – Readme.md jak uruchomić projekt (w języku angielskim)

2. Backend
    Do zamodelowania dane:
    – Lista filmów. Każdy film ma swój tytuł, datę produkcji i gatunek
    – Lista ocen przypisanych do danego filmu. Oceny w skali od 1-10
    
    Do stworzenia API Restowe z metodami:
    – Pobranie listy filmów oraz ocen dla filmów
    – Dodanie nowej oceny dla filmu

    Wybrane technologie:
    – Spring
    – Spring Data MongoDb
    – API Restowe dowolny moduł dla Springa

3. Baza danych
    – mongoDB
    – skrypt do zasilania bazy danymi

4. Frontend
    – Wyświetlenie listy filmów (Nazwa, data produkcji w formacie: DD-MM-RRRR, gatunek  wyświetlony jako ikonka, sama ikonka nie ma znaczenia), średnia ocen za film, zaokrąglona do 2 miejsc po przecinku. Jeśli film nie został oceniony to treść ‚Brak ocen’
    – po kliknięciu w wybrany tytuł włącza się komponent do dodawania nowej oceny za film (może to być np modal, nowa strona, lub rozwija się wiersz i pojawia się możliwość dodania oceny)
    – Komponent dodawania nowej oceny. Tu dowolność, może to być np 10 gwiazdek i po kliknięciu konkretnej dodaje się ocena.
    – Button Save zapisuje odpowiedź i wraca do listy z odświeżoną średnią oceną

    Wybrane technologie:
    – React
    – Redux

5. Testy
    – Unit testy dla API Restowego
    – Unit testy dla React w Jest + Enzyme

Wymagania i co o nich myślę

Zadania domowe to były w szkole a teraz to ja to p…

Zanim odniosę się do samych wymagań krótko o tego typu zadaniach domowych. Słyszałem różne opinie jeśli chodzi o takie zadania. Jak: „Czemu mam spędzać swój wolny czas na robieniu czegoś za co nikt mi nie zapłaci?” lub „Po co mam tworzyć coś co zostanie i tak wyrzucone do kosza i nikt z tego nie skorzysta?”.

Spotkałem się również z sytuacją, w której pracodawca umożliwia kandydatowi przedstawienia swojego własnego projekt, który gdzieś sobie tworzy, i nie chce marnować czasu na pisanie zadania rekrutacyjnego. Myślę, że w jakimś stopniu jest to dobry sposób na rozwiązanie problemu gdy kandydat rzeczywiście nie chce tracić czasu na robienie setny raz tego samego.

Do sedna

Przejdźmy jednak do samych wymagań. To co może się podobać lub nie to to, że technologie w jakich zadanie rekrutacyjne ma być dostarczone są precyzyjnie określone. Mamy tutaj oczywiście Spring-a jako główny framework aplikacji, Dockera do konteneryzacji, MongoDB do persystencji oraz React do wykorzystania we front-endzie. Jedyne o czym możemy zdecydować sami to moduł REST po stronie back-endu oraz jak ma wyglądać komponent oceniania filmu po stronie front-endu.

Powyższe precyzyjne wymagania można uznać za duży plus gdyż nie musimy się głowić w jakiej technologii będzie nam stworzyć taki serwis najlepiej. Jednak z drugiej strony jesteśmy zmuszeni do wykorzystania niektórych technologii, z którymi wcześniej nie mieliśmy do czynienia i na pewno po napisaniu takiej aplikacji będzie widać, które frameworki znamy z doświadczenia a które używamy po raz pierwszy.

Znajomość technologii

I tak oto w moim przypadku największym problemem było w tym zadaniu brak wcześniejszego doświadczenia z bazą dokumentową MongoDB jak i brak znajomości React i Redux. Z drugiej strony to właśnie brak doświadczenia z tymi technologiami były jedyną moją motywacją do zrobienia tego zadania po godzinach.

Co mnie boli w tym zadaniu rekrutacyjnym

Dodatkowo co mnie uderzyło w tych wymaganiach to brak wymagania odnośnie testów integracyjnych dla modułu REST. Wydaje mi się, że takie testy powinny być integralną częścią takiego zadania. Opis jak ma działać sama funkcja oceniania filmu z mojej perspektywy wydaje się mało ergonomiczna, dlatego w moim rozwiązaniu pozwoliłem sobie na lekką modyfikację ale o tym póżniej.

Rozwiązanie

1. Docker

Potrzebujemy dwóch kontenerów, pierwszy to baza danych: mongo-db oraz drugi to nasza aplikacja do oceniania filmów: movies-app. Ponieżej plik docker-compose.yml zawierający definicję tych serwisów:

version: "3"
services:
  mongo-db:
    image: mongo:4
    container_name: "mongo-db"
    restart: always
    environment:
      - MONGO_DATA_DIR=/data/db
      - MONGO_LOG_DIR=/dev/null
      - MONGO_INITDB_DATABASE=moviesDb
    volumes:
      - ./data/db:/data/db
    ports:
      - 27017:27017
    command: mongod
    networks:
      - default
  movies-app:
    depends_on:
      - mongo-db
    container_name: "movies-app"
    environment:
      - SPRING_DATA_MONGODB_URI=mongodb://mongo:27017/moviesDb
      - MOVIESDB_POPULATEBYINITDATA=true
    links:
      - mongo-db:mongo
    build: .
    ports:
      - "8080:8080"
    networks:
      - default
  • mongo-db – kontener zbudowany z obrazu mongo w wersji 4. Definiujemy tutaj katalog, w którym mongo będzie zapisywał dane oraz port na który się będziemy łączyć: 27017.
  • movies-app – kontener, który zbudujemy z pliku Dockerfile (treść poniżej) odpowiada za to linia 27. Definiujemy też tutaj to że nasza aplikacja będzie dostępna na porcie 8080 oraz to, że jest zależna od serwisu mongo-db (linia 19,20) i też w takiej kolejności Docker ma uruchomić nasze serwisy.

Poniżej wspomniany wyżej Dockerfile:

FROM openjdk:12
VOLUME /tmp
ARG DEPENDENCY=build/dependency
COPY ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY ${DEPENDENCY}/META-INF /app/META-INF
COPY ${DEPENDENCY}/BOOT-INF/classes /app
EXPOSE 8080
ENTRYPOINT ["java","-cp","app:app/lib/*","pl.devrev.ratingservice.Main"]

Do projektu dołączone są skrypty odpowiadające za uruchomienia, zatrzymanie jaki posprzątanie po sobie:

  • start.sh
  • stop.sh
  • clean.sh

2. Backend

Domena

Mając narzuconą dokumentową bazę MongoDB na pierwszy rzut oka wydaje się, że całość można by zamodelować jedną klasą: „Movie”. Klasa ta zawierała by takie pola jak:

  • title,
  • releaseDate,
  • genre,
  • genreIconUri,
  • avgRate.

Tak na prawdę to takiego modelu oczekujemy po stronie widoku. Czyli to chcielibyśmy

aby back-end zwracał nam do front-endu. Dlatego utworzymy sobie podobną klasę DTO w tym celu:

@Data
@Builder
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@ToString
@NoArgsConstructor
@AllArgsConstructor
public class MovieDto {
    @EqualsAndHashCode.Include
    private String id;
    @EqualsAndHashCode.Include
    private String title;
    @JsonSerialize(using = ReleaseDateSerializer.class)
    @JsonDeserialize(using = ReleaseDateDeserializer.class)
    private ZonedDateTime releaseDate;
    private String genreName;
    private String genreIconUri;
    private List<Integer> ratings;
}

Przejdźmy jednak do klas domenowych, które będziemy przechowywać w naszej bazie. Pomimo, że korzystamy z bazy dokumentowej zdecydowałem się zamodelować domenę relacyjnie z podziałem na trzy klasy:

@Document
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Movie {
    @Id
    @EqualsAndHashCode.Include
    private String id;
    private String title;
    @JsonSerialize(using = ReleaseDateSerializer.class)
    @JsonDeserialize(using = ReleaseDateDeserializer.class)
    private ZonedDateTime releaseDate;
    @DBRef
    @JsonDeserialize(using = GenreDeserializer.class)
    private Genre genre;

    public Movie(String id) {
        this.id = id;
    }
}
@Document
@Data
@NoArgsConstructor
public class Genre {
    @Id
    @EqualsAndHashCode.Include
    private String id;
    private String name;
    private String iconUri;

    public Genre(String id) {
        this.id = id;
    }
}
@Document
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
    public class MovieRate {
    @Id
    @EqualsAndHashCode.Include
    private String id;
    private Integer value;
    @JsonFormat(shape = JsonFormat.Shape.STRING, 
                pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX")
    private ZonedDateTime timestamp;
    @DBRef
    @JsonDeserialize(using = MovieDeserializer.class)
    private Movie movie;
}

Dlaczego Genre jest osobno?

Dlatego, że Genre ma link do ikony, która teoretycznie może się zmienić, więc jeśli będziemy mieć bardzo bardzo dużo filmów w naszej bazie nie chcę modyfikować milionów rekordów z powodu zmiany ikony danego gatunku.

Dlaczego nie avgRate tylko osobna klasa MovieRate?

Między innymi po to aby uzyskać niemutowalność. Jeśli ocena będzie częścią dokumentu Movie to możemy skończyć z bardzo dużą ilością konkurencyjnych call-i do dodania nowej oceny (bądź modyfikacji wyliczonej średniej oceny (avgRate)). Czyli czekała by nas synchronizacja. W przypadku gdy wydzielimy ocenę jako osobny byt to ocena będzie obiektem niemutowalnym, który będziemy tylko i wyłącznie tworzyć. Nie przewidujemy przecież, że ktoś będzie chciał zmienić swoją ocenę lub ją usunąć. Przynajmniej nie mamy takich wymagań na chwilę obecną.

Przy takim modelu możemy również dodać sobie do klasy MovieRate pole: timestamp, które będzie nam pokazywało kiedy dokładnie dana ocena została dodana do naszego filmu.

Mapowanie

W celu mapowania domeny na klase DTO użyje generatora kodu MapStruct.

@Mapper(componentModel = "spring")
public interface MovieMapper {

    @Mapping(source = "genre.name", target = "genreName")
    @Mapping(source = "genre.iconUri", target = "genreIconUri")
    @Mapping(target = "ratings", ignore = true)
    MovieDto mapToMovieDto(Movie movie);
}
...
public class MovieEndpoint {
...
	private List<MovieDto> convertToDto(List<Movie> movies) {
        return movies.stream()
          .map(movieMapper::mapToMovieDto)
          .collect(Collectors.toList());
    }
...

Pomimo tego, że w tym przypadku zwykłe kopiowanie „property-by-property” nie było by takie złe (z uwagi na małą ilość pól) to jednak staram się unikać pisania tego wprost. Dodatkowo w przypadku, gdy chcielibyśmy dodać jakieś pole do klasy Movie to wystarczy, że takie same pole dodamy do klasy MovieDto i nie musimy już nic więcej aktualizować. W przypadku tradycyjnego kopiowania musielibyśmy dodać dodatkową linijkę kodu odpowiadającą za kopiowanie nowej właściwości.

REST Service

Czas przejść do serca naszej aplikacji czyli serwisu, który będzie zwracał listę filmów. Dla uproszczenia pomijam tutaj takie rzeczy jak paginacja czy implementację HATEOSa.

Opcja 1

  • GET /movies
  • GET /movie/{movieId}/ratings
  • POST /movie/{movieId}/ratings

W pierwszej opcji oceny traktujemy jako osobny byt i linkujemy je w obiekcie Movie:

// GET /movies
[
  {
    "movieId": "1",
    "title": "The Shawshank Redemption",
    "releaseDate": "1994-01-16",
    "genreName": "Drama",
    "genreIconUri": "Drama.png",
    "links": [
      {
        "rel": "ratings",
        "href": "http://localhost:8080/api/movie/1/ratings"
      }
    ]
  },
...

// GET /movie/1/ratings
[
  {
    "value": 6
  },
  {
    "value": 10
  },
  {
    "value": 2
  },
  {
    "value": 5
  },
  {
...

Dzięki temu, że traktujemy oceny filmu (ratings) jako osobny byt możemy użyć metody POST i w łatwy sposób dodać nową ocenę do filmu. Niestety w tym podejściu przy pobraniu listy filmów musimy dodatkowo wysłać osobne żądanie po listę ocen dla danego filmu. Czyli z listy n-filmów pobranych po stronie GUI mamy n-dodatkowych HTTP requestów potrzebnych do wyświetlenia średniej oceny dla filmu.

Jeśli chodzi o zalety takiego podejścia to na plus zaliczył bym to, że przy ewentualnej zmianie wymagań (na przykład jeśli chcielibyśmy do widoku zwracać czas kiedy ocena została dodana) modyfikujemy tylko jeden endpoint: ratings. Kształt endpointu movies pozostaje bez zmian.

Kolejną zaletą jest to, że jeśli najpierw w GUI chcemy wyświetlić listę samych filmów (bez ich ocen) a dopiero po kliknięciu w dany film pokazujemy średnią ocenę, to przy takim podejściu uderzamy w endpint ratings tylko wtedy kiedy naprawdę musimy (Lazy loading).

Opcja 2

  • GET /movies
  • PATCH /movie/{movieId}/{newRate}

W moim rozwiązaniu zdecydowałem się jednak na opcję numer dwa. Gdzie w endpoint movies zwracam listę filmów wraz z ocenami jako jedna integralna część.

// GET /movies
[
  {
    "id": "1",
    "title": "The Shawshank Redemption",
    "releaseDate": "1994-01-16",
    "genreName": "Drama",
    "genreIconUri": "Drama.png",
    "ratings": [6, 10, 2, 5, 11, 11, 13, 4, 8]
  }
...

Dzięki takiemu rozwiązaniu po stronie widoku otrzymuję wszystko za pomocą jednego żądania HTTP. Liczenie średniej oceny pozostawiam jednak dla warstwy prezentacji dzięki temu mam pewną elastyczność w wyborze formy wyświetlenia.

@Log
@RestController
@AllArgsConstructor
@Validated
@RequestMapping("/api")
public class MovieEndpoint {

    private final MovieRepository movieRepository;
    private final MovieRateRepository movieRateRepository;
    private final MovieMapper movieMapper;

    @RequestMapping(value = "/movies")
    public List<MovieDto> movies() {
        List<Movie> movies = movieRepository.findAll();
        log.info(
          String.format("Found %s movies in database", movies.size()));
        List<MovieDto> movieDtos = convertToDto(movies);
        attachRatesToMovies(movieDtos);
        return movieDtos;
    }

    private List<MovieDto> convertToDto(List<Movie> movies) {
        return movies.stream()
          .map(movieMapper::mapToMovieDto)
          .collect(Collectors.toList());
    }

    private void attachRatesToMovies(List<MovieDto> movieDtos) {
        for (MovieDto movieDto : movieDtos) {
            List<MovieRate> movieRates = 
              movieRateRepository.findByMovieId(movieDto.getId());
            movieDto.setRatings(
              movieRates.stream()
              .map(MovieRate::getValue)
              .collect(Collectors.toList())
            );
        }
    }

Jeśli natomiast chodzi o dodanie nowej oceny to w tym przypadku będziemy jednak musieli modyfikować sam obiekty Movie jako dodanie nowego elementu do tablicy ratings. Dlatego zdecydowałem się użyć w tym celu metody PATCH:

    // PATCH /movie/{movieId}/{newRate}
    @RequestMapping(value = "/movie/{movieId}/{newRate}",
            method = RequestMethod.PATCH)
    public void rateMovie(@PathVariable("movieId") String movieId,
                     @Min(1) @Max(10)
                     @PathVariable("newRate") Integer value) {
        Optional<Movie> movie = movieRepository.findById(movieId);
        if (movie.isPresent()) {
            log.info(String.format("Adding new rate[%s] for movie: %s", 
                                   value, movieId));
            addNewRate(movie.get(), value);
        } else {
            throw new MovieNotFoundException(
              "There is no movie with id:" + movieId);
        }
    }

    private void addNewRate(Movie movie, int value) {
        MovieRate movieRate = MovieRate.builder()
                .timestamp(ZonedDateTime.now(ZoneOffset.UTC))
                .value(value)
                .movie(movie)
                .build();
        movieRateRepository.save(movieRate);
    }

3. Inicjalizacja MongoDB danymi

Do zasilenia bazy MongoDB danymi użyje prostego i gotowego mechanizmu a mianowicie klasy: Jackson2RepositoryPopulatorFactoryBean z pakietu: org.springframework.data.repository.init. Plik JSON z danymi umieszczamy w katalogu resources gdzie ClassLoader będzie mógł go znaleźć bez problemu przy uruchamianiu aplikacji.

Klasa odpowiadająca za załadowanie danych do MongoDB:

@Configuration
@ConditionalOnProperty(
        value = "moviesdb.populatebyinitdata",
        havingValue = "true")
public class MongoDbInitializerConfig {

    @Bean
    public Jackson2RepositoryPopulatorFactoryBean init(
            @Qualifier("objectMapper") ObjectMapper mapper) {
        
        Jackson2RepositoryPopulatorFactoryBean factory = 
                new Jackson2RepositoryPopulatorFactoryBean();
        factory.setMapper(mapper);

        Resource sourceData = new ClassPathResource("data.json");
        factory.setResources(new Resource[]{sourceData});

        return factory;
    }

}

Dzięki zastosowaniu adnotacji @ConitionalOnProperty w łatwy sposób możemy wyłączyć inicjalizację bazy danymi zmieniając property:

moviesdb:
  populatebyinitdata: false

Możemy również nadpisać ta właściwość z poziomu docker-compose.yml:

    environment:
      - SPRING_DATA_MONGODB_URI=mongodb://mongo:27017/moviesDb
      - MOVIESDB_POPULATEBYINITDATA=true

4. Front-End

Jeśli chodzi o front-end to z powyższych wymagań postanowiłem uprościć mechanizm wystawiania oceny tak aby nie wyskakiwały żadne pop-upy. Również nie chciałem męczyć użytkownika ciągłym klikaniem w przycisk „Zapisz”. Z punktu widzenia użyteczności (ustability) jest to mało wydajne i przyjazne dla użytkowników (not user friendly).

Czyli to co zobaczymy z „przodu” naszej aplikacji to tabelka z listą filmów i dodatkowa kolumna z dziesięcioma gwiazdkami. Użytkownik wybiera jaką ocenę chce dać po kliknięciu w wybraną gwiazdkę.

Spójność ostateczna

Zaraz po kliknięciu zakładamy, że wszytko pójdzie dobrze i średnia ocena jest aktualizowana bezpośrednio po stronie front-endu. To znaczy, że nie stosujemy tutaj standardowego podejścia polegającego na tym, że przesyłamy nową ocenę dla danego filmu, następnie robimy call do serwera po to aby pobrać zmieniony obiekt filmu i podmienić go po stronie widoku.

Dzięki takiemu podejściu aktualizacja jest natychmiastowa, użytkownik nie czeka na komunikację klient-serwer oraz od razu widzi zmieniony stan po dodaniu swojej oceny (średnia ocena filmu jest zaktualizowana).

Nie zapominając o innych użytkownikach

Niestety nic za darmo. W tym podejściu istnieje ryzyko, że w międzyczasie jakiś inny użytkownik również doda swoją ocenę dla tego filmu. I tutaj niestety obecny użytkownik będzie widział obliczoną średnią ocenę bez uwzględnienia innych równoczesnych modyfikacji. Do czasu odświeżenia całej strony z listą filmów. Zakładamy jednak, że jest to stosunkowo mało prawdopodobne. A nawet jeśli się zdarzy to i tak akceptujemy taki stan gdyż docelowo wszystkie oceny i tak zostaną dodane. Zatem ostatecznie średnia ocena filmu będzie spójna.

W przypadku niepowodzenia dodania oceny wyświetlamy alert i zmieniamy stan naszego komponentu do wystawiania oceny na ponownie aktywny (editable: true).

    onStarClick(nextValue, prevValue, name) {
        this.setState({rating: nextValue, editable: false});
        const movieId = this.props.movie.id;
        axios.patch(RATE_MOVIE_URL + movieId + '/ratings/' + nextValue)
            .then(res => this.props.onRate(movieId, nextValue))
            .catch(onRejected => {
                this.setState({rating: 0, editable: true});
                alert(onRejected);
            });
    }

    render() {
        const {rating} = this.state;
        return (
            <StarRatingComponent
                editing={this.state.editable}
                ref={this.props.movie.id}
                name={this.props.movie.id}
                starCount={10}
                value={rating}
                onStarClick={this.onStarClick.bind(this)}
            />
        )
    }

Podsumowanie

Realizacja tego zadania sprawiła mi sporo satysfakcji. Poznałem (w minimalnym stopniu ale jednak) podstawy technologii React jak i bazy dokumentowej MongoDB.

Choć zadanie wydawało się z początku bardzo proste do realizacji to jednak sporo czasu spędziłem zastanawiając się jak zaprojektować model. Również endpoint REST (dodawania oceny dla filmu) nie był sprawą oczywistą. Tak aby było to jak najbardziej zoptymalizowane i jednocześnie poprawne. Można to zrobić na kilka sposobów. W prawdziwej produkcyjnej realizacji podobnego systemu mielibyśmy zapewne zdefiniowany większy obszar do zaprojektowania. Dzięki temu nie trzeba by się zastanawiać jakie inne funkcjonalności mogą wpłynąć na nasz model w teorii tylko było by to z góry zdefiniowane. I paradoksalnie prostsze.

Git repo

Link do repozytorium kodu: https://github.com/devrevpl/devrev-rating-service

  •  
  •  
  •  
  •  
  •  
  •  
  •  

15 komentarze na temat “Zadanie rekrutacyjne Java w 2 tygodnie

    1. Niestety nie zostałem zaproszony na rozmowę z powodu:

      „60-70% tasków FE i reszta po stronie BE. Dlatego szukamy Fullstacka z naciskiem na Front End. Twoje doświadczenie i sposób wykonania zadania wskazują na szerszą wiedzę z zakresu Back End’u.”

      Co oczywiscie jest prawdą, gdyż dopiero realizując to zadanie uczyłem się React i Redux.
      Niestety w opisie nie było tego wskazania 60-70% nacisku na FrontEnd.

      Jesli interesuje cię co było nie tak po stronie FE:
      „FE
      – braki w JS
      – widoczne wykorzystanie tutoriali
      – brak podziału na jakąkolwiek strukturę projektu (componenty / containery itp.)
      – brak odseperowania logiki od componentu

      1. Jakie jest twoje podejście do tak przeprowadzanego procesu rekrutacyjnego?
        Mianowicie chodzi mi o to, że poświęciłeś dość sporo czasu na realizację zadania wydawałoby się bardzo ściśle określonego i koniec końców nie dostałeś możliwości „obrony” swojej pracy pomijając kwestie tego czy była ona dobra czy zła.

        Ze swojej strony zawsze staram się zaprosić na rozmowę osobę, która zrobiła zadanie rekrutacyjne nawet jak jestem praktycznie przekonany, że nic z tego nie będzie ale zawsze jest jakaś szansa, że dana osoba będzie w stanie obronić swoją pracę i jednak przekonać osoby przeprowadzające rekrutacje do swoich umiejętności. Jest to oczywiście związane z koniecznością „poświęcenia” dodatkowego czasu przez obie strony z dużym prawdopodobieństwem, że i tak „nic z tego nie będzie” w większości przypadków ale osobiście uważam, że jak ktoś poświęcił swój prywatny czas na zadanie to i tak powinien mieć możliwość obrony danej pracy. Jakie jest twoje zdanie na ten temat?

        1. Częściowo się z Tobą zgadam. Dobrze by było mieć możliwość obrony i ewentualnego przekonania rekruterów do swojego podejścia w rozwiązaniu zadania i wytłumaczenia czemu tak a nie inaczej.
          Jeśli jednak chodzi o czas poświęcony to patrząc od strony rekrutera to on też musiał poświęcić swój czas na ocenę mojego zadania oraz wypunktowanie dobrych i złych stron rozwiązania.
          W tym przypadku jeśli mowa o straconym czasie to z innej strony jeśli miałbym zostać zaproszony na rozmowę tylko z grzeczności i z takich czy innych powodów nie dostać oferty na pracę to czy nie było by to marnowanie jeszcze większej ilośći mojego (oraz rekruterów) czasu?

    1. Dzięki za komentarz. Możesz jednak rozwinąć z jakiego powodu rekomendujesz nie używać @Data?

      1. Moim zdaniem mieszanie kilku sposób tworzenia obiektu jest conajmniej niepotrzebne. Jeżeli stosujesz builder to robisz zmienne final i zaczyna być immutable. Jeżeli masz Buildera, zmienne niefinalne i jeszcze @Data, to masz kilka sposobów na tworzenie obiektu. Obiekt traci hermetyzację.

  1. Ja bym cie przyjął 🙂

    BTW. Nie zrobiłeś stronicowania przy pobieraniu listy filmów, to tez jest błąd. Jeśli chodzi o testy integracyjne – dobrze, ze tego nie wpisali w wymagania, doświadczony developer sam o nie zadba.

    1. Jeśli chodzi o stronicowanie to nie nazwał bym tego błądem (bardziej Nice-to-Have), jednak:
      1. Nie było tego w wymaganiach,
      2. Jeśli byłoby to uznane za bład to dostał bym taką informację w feedback od rekrutera,
      3. Moim zdaniem robienie czegoś ponad to co jest w wymaganiach (w takich zadaniach) może zostać uznane za niezrozumienie wymagań.

      Jeśli chodzi o testy integracyjne:
      W wymaganiach jest punkt 5. gdzie dokładnie są określone oczekiwane testy. Jeśli tego punktu by nie było to całkowicie się z Tobą zgadzam i wtedy developer powinien napisać testy unitowe+integracyjne.

      1. Rozumiem Twój punkt widzenia jednocześnie uważam, że zwracanie z API zawartości całej tabeli to jest problem, który powinien zaadresować programista nawet, jeśli nie ma tego wskazanego explicite w wymaganiach. Podobnie jak z obsługą błędów – trzeba to robić natomiast nikt nie będzie tego wpisywał w wymagania. Tak czy siak, bardzo fajny wpis, oby tak dalej 🙂

  2. Zmieniłeś wymagania na froncie, bo inny sposób prezentacji wydawał Ci sie lepszy. Jakbym był upierdliwym rekruterem to dałbym pogrubiony minus za cos takiego 😀

    1. Tak. Zrobiłem to. I zrobił bym to jeszcze raz w podobnym przypadku. Z tym, że w prawdziwym projekcie przedstawił bym moją wizję rozwiązania stakeholder-om przed implementacją, pokazał bym wszystkie za i przeciw i po przedyskutowaniu wszyscy zainteresowani powinni podjąć decyzję, w którą stronę idziemy. Ponieważ jednak było to zadanie rekrutacyjne i nie miałem kontaktu z osobą, która definiowała wymagania to zaryzykowałem. Szczerze mówiąc uważam to za jedną z najpiękniejszych rzeczy w naszej dziedzinie bycia deweloperem, że właśnie na tak zdefiniowane i nieprzemyślane wymagania możemy sami zaproponować dużo lepsze rozwiązanie – bardziej kreatywne, ergonomiczne, szybsze w realizacji, efektywniejsze w działaniu. To właśnie piękno naszego zawodu. W innym przypadku szybko zastąpiłyby nas automatyczne generatory kodu, które tworzyły by aplikację na podstawie innych rozwiązań z repozytorium gita.

      Zwróć proszę uwagę również na pewne słowa kluczowe w wymaganiach w punkcie 4:
      „(może to być np modal, nowa strona, …„
      „Komponent dodawania nowej oceny. Tu dowolność, może to być…”
      Czy nie sugerują one, że deweloper może zaproponować w tym przypadku najlepsze rozwiązanie jakie uważa za możliwe (swoją wizję)?

Dodaj komentarz

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