Zadanie

Zgłosił się do mnie ostatnio Pablo z niemałym problemem. Mówił, że chce zostać architektem domów i potrzebuje do tego jednego narzędzia. Chce mieć możliwość przenoszenia projektów domów z plików o rozszerzeniu .home do aplikacji oraz w drugą stronę.

Prosił mnie o zaimplementowanie możliwości ładowania plików .home do aplikacji. Rozpocząłem rozwiązywanie tego zadania, ale stwierdziłem, że może Tobie się ono uda? Już tłumaczę na czym ono polega.

Mamy plik tekstowy o rozszerzeniu .home w takim formacie:

nazwa_domu%adres%liczba_domownikow%liczba_pokoi
nazwa_pokoju%kolor pokoju%wysokość pokoju%powierzchnia pokoju w m2%ilosc elementow w pokoju
nazwa_elementu%pozycja_x%pozycja_y%pozycja_z%dlugość elementu%szerokość elementu%wysokość elementu%waga

Czyli przykładowy plik wygląda tak (w pliku może być tylko jeden dom):

Dom na zielonym wzgórzu%Ulica zielona 33/24%3%2
Pokoj mamy%#00ff00%10.5%25%3
Biurko%10%3.5%0.0%2%1.5%0.7%30.3
Lampka%10%35%0.7%0.1%0.1%0.1%0.5
Dlugopis%12%38%0.7%0.01%0.15%0.03%0.1
Pokoj taty%#ffffff%10.5%23%1
Fotel%8.3%5.4%1.3%1.5%1.5%2%23

Pablo chce taki plik wczytać do swojej aplikacji, tak jak wspomniałem rozpocząłem już realizację zadania – stworzyłem interfejsy, modele, klasy oraz napisałem jeden test. Wszystko powinno Ci się przydać.

import factories.HomeFactory;
import factories.HomeFactoryImpl;
import models.Home;

public class HomeLoaderImpl implements HomeLoader {
    HomeFactory homeFactory = new HomeFactoryImpl();

    @Override
    public Home loadHome(String fileName, String separator) {
        //load file and use home factory to create home
    }
}

Wszystko zaczyna się w tej klasie, podajemy nazwę pliku oraz separator jaki oddziela wartości w pliku (wyżej jest to %).

Napisałem specjalnie dla Ciebie również krótki test, abyś mógł szybciej przetestować swoje rozwiązanie.

package factories;

import models.*;
import org.junit.Assert;
import org.junit.Test;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class HomeFactoryTest {

    private final HomeFactory homeFactory = new HomeFactoryImpl();

    @Test
    public void testCreateHome() {
        //given
        final List<String> homeStr = Arrays.asList(
                "Dom na zielonym wzgórzu%Ulica zielona 33/24%3%2",
                "Pokoj mamy%#00ff00%10.5%25%2",
                "Biurko%10%3.5%0.0%2%1.5%0.7%30.3",
                "Lampka%10%35%0.7%0.1%0.1%0.1%0.5",
                "Pokoj taty%#ffffff%10.5%23%1",
                "Lampka%10%35%0.7%0.1%0.1%0.1%0.5"
        );
        final String separator = "%";
        final Element biurko = new Element("Biurko", new Position(10.0f, 3.5f, 0.0f), 30.3f, new Size(2.0f, 1.5f, 0.7f));
        final Element lampka = new Element("Lampka", new Position(10.0f, 35f, 0.7f), 0.5f, new Size(0.1f, 0.1f, 0.1f));
        final Room pokojMamy = new Room("Pokoj mamy", "#00ff00", 25.0f, 10.5f, Arrays.asList(biurko, lampka));
        final Room pokojTaty = new Room("Pokoj taty", "#ffffff", 23.0f, 10.5f, Arrays.asList(lampka));
        final Home home = new Home("Dom na zielonym wzgórzu", "Ulica zielona 33/24", 3, Arrays.asList(pokojMamy, pokojTaty));

        //is
        Home result = homeFactory.createHome(homeStr, separator);


        //expected
        Assert.assertEquals(home, result);
    }
}

Cały szablon, który dla Ciebie przygotowałem możesz znaleźć tutaj.

Punktacja

Przygotowanych jest 7 testów – za każdy możesz otrzymać 50 expa. Dodatkowo za 100 expa za użycie wzorca projektowego Singleton dla klas Factory oraz HomeLoaderImpl.

Łącznie można otrzymać 7x50exp + 100 exp = 450expa.

Porady

  • Przygotowałem dla Ciebie enumy dla każdych property. Możesz je wykorzystać podczas parsowania linii – na początku stworzyć tablicę wykonując split(Separator) na danej linii i zapisywać dane z tablicy do EnumMap w parach Property -> Value. Dzięki takiemu rozwiązaniu w dalszej części tworzenia obiektu nie pomylisz wartości – o ile je dobrze wyciągniesz z tablicy. 😉
import java.util.EnumMap;
import java.util.Map;

enum HomeProperty {
    NAME, ADDRESS
}

public class Main {
    public static void main(String[] args) {

        String line = "Moj dom%Zielona";

        String [] values = line.split("%");

        Map<HomeProperty, String> homeDetails = new EnumMap<>(HomeProperty.class);

        homeDetails.put(HomeProperty.NAME, values[0]);
        homeDetails.put(HomeProperty.ADDRESS, values[1]);

    }
}
  • Rozpocznij od implementacji klas Factory – przygotowałem dla Ciebie test, którym możesz sprawdzać działanie HomeFactory.
  • Na podstawie mojego testu możesz stworzyć sobie po jednym teście dla każdej z Factory, implementacja takiego testu pozwoli Ci szybciej wyszukiwać błędy w kodzie. Poświęć nawet kilka minut więcej, aby mieć pewność, że Twoje rozwiązanie jest przetestowane jednostkowo.
  • Testy możesz uruchamiać przy użyciu komend mvn test/mvn install lub z poziomu IntelliJ.
  • Trzymaj się schematu: ElementFactory -> Tworzy jeden element na podstawie podanego Stringa, RoomFactory – Tworzy jeden pokój na podstawie listy Stringów -> HomeFactory -> Tworzy dom na podstawie listy Stringów. Dzięki temu otrzymasz czytelność kodu i użyjesz wzorca projektowego Factory (Fabryka). 😉

Czas

Zadanie zostało opublikowane 25 listopada 2018 roku, rozwiązania można przesyłać do 6 grudnia 2018 roku do godziny 23:59. Zadania wysłane później będą automatycznie usuwane.

Format

Zadanie należy wysłać na email: njd@1024kb.pl z tematem: TWÓJ-NICK_HOME.

Tym razem zadanie przesyłamy jako repozytorium Git – może być hostowany na GitHub, GitLab, Bitbucket – gdzie tylko chcesz. W wiadomości podajemy tylko link do repozytorium projektu. 😉

Pamiętajcie, aby do plkiku .gitignore dodać:

  • /target
  • /out
  • /.idea
  • *.iml

I inne pliki/katalogi, które nie powinny być na zdalnym repozytorium.

W razie jakichkolwiek wątpliwości pytajcie, a dopytam Pablo (product ownera) jak ma dokładnie wyglądać finalny produkt!

Rozwiązanie

Zacznijmy trochę od końca – od klasy, której zadaniem było stworzenie pokoju na podstawie podanej jej linii.

ElementFactory

package factories;

import models.Element;
import models.Position;
import models.Size;
import models.properties.ElementProperty;

import java.util.EnumMap;
import java.util.Map;

class ElementFactoryImpl implements ElementFactory {
    private static final ElementFactory instance = new ElementFactoryImpl();

    private ElementFactoryImpl() {

    }

    static ElementFactory getInstance() {
        return instance;
    }

    public Element createElement(String elementDetailsStr, String separator) {
        final Map<ElementProperty, String> elementDetails = getElementProperty(elementDetailsStr, separator);

        final String elementName = elementDetails.get(ElementProperty.ELEMENT_NAME);
        final float weight = Float.valueOf(elementDetails.get(ElementProperty.WEIGHT));
        final Position position = getPosition(elementDetails);
        final Size size = getSize(elementDetails);

        return new Element(elementName, position, weight, size);
    }

    private Map<ElementProperty,String> getElementProperty(String elementDetailsStr, String separator) {
        final String [] elementDetailsValues = elementDetailsStr.split(separator);
        final Map<ElementProperty, String> elementDetails = new EnumMap<>(ElementProperty.class);

        elementDetails.put(ElementProperty.ELEMENT_NAME, elementDetailsValues[0]);
        elementDetails.put(ElementProperty.POSITION_X, elementDetailsValues[1]);
        elementDetails.put(ElementProperty.POSITION_Y, elementDetailsValues[2]);
        elementDetails.put(ElementProperty.POSITION_Z, elementDetailsValues[3]);
        elementDetails.put(ElementProperty.LENGTH, elementDetailsValues[4]);
        elementDetails.put(ElementProperty.WIDTH, elementDetailsValues[5]);
        elementDetails.put(ElementProperty.HEIGHT, elementDetailsValues[6]);
        elementDetails.put(ElementProperty.WEIGHT, elementDetailsValues[7]);


        return elementDetails;
    }

    private Size getSize(Map<ElementProperty,String> elementDetails) {
        final float length = Float.valueOf(elementDetails.get(ElementProperty.LENGTH));
        final float width = Float.valueOf(elementDetails.get(ElementProperty.WIDTH));
        final float height = Float.valueOf(elementDetails.get(ElementProperty.HEIGHT));

        return new Size(length, width, height);

    }

    private Position getPosition(Map<ElementProperty,String> elementDetails) {
        final float positionX = Float.valueOf(elementDetails.get(ElementProperty.POSITION_X));
        final float positionY = Float.valueOf(elementDetails.get(ElementProperty.POSITION_Y));
        final float positionZ = Float.valueOf(elementDetails.get(ElementProperty.POSITION_Z));
        return new Position(positionX, positionY, positionZ);
    }




}

Wrzuciłem całą klasę, ale już pokazuję o co chodzi. Na początku mamy tylko jedną publiczną metodę – createElement, reszta metod jest prywatna. Pozwala to podzielić jedną dużą metodę na kilka pomniejszych. Pozwala nam to tworzyć czysty i bardziej zrozumiały kod.

Metoda getElementProperty jest odpowiedzialna za zwrócenie map wartosci na podstawie danego Stringa. Mapa jest w parach nazwa wartości -> wartość.

Metody getSize i getPosition są odpowiedzialne za stworzenie odpowiednio obiekty: size i position na podstawie utworzonej wcześniej mapy.

Cała magia dzieje się w tym, że wszystkie wartości wrzucamy od samego początku w mapę i tam po Enum kluczach wyszukujemy interesującej nas właściwości.

RoomFactory

package factories;

import models.Element;
import models.Room;
import models.properties.RoomProperty;

import java.util.EnumMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

class RoomFactoryImpl implements RoomFactory{
    private final static RoomFactory instance = new RoomFactoryImpl();
    private final ElementFactory elementFactory = ElementFactoryImpl.getInstance();

    private RoomFactoryImpl() {

    }

    static RoomFactory getInstance() {
        return instance;
    }

    public Room createRoom(List<String> roomDetailsList, String separator) {
        int indexLine = 0;
        final Map<RoomProperty, String> roomDetails = getRoomDetails(roomDetailsList.get(indexLine), separator);
        indexLine++;

        final String roomName = roomDetails.get(RoomProperty.ROOM_NAME);
        final String roomColor = roomDetails.get(RoomProperty.ROOM_COLOR_HEX);
        final float area = Float.valueOf(roomDetails.get(RoomProperty.AREA));
        final float height = Float.valueOf(roomDetails.get(RoomProperty.HEIGHT));
        final int elementsCount = Integer.valueOf(roomDetails.get(RoomProperty.COUNT_ELEMENTS));

        final List<Element> roomElements = new LinkedList<>();

        for (int j = 0; j < elementsCount; j++) {
            Element element = elementFactory.createElement(roomDetailsList.get(indexLine), separator);
            roomElements.add(element);
            indexLine++;
        }

        return new Room(roomName, roomColor, area, height, roomElements);
    }

    private Map<RoomProperty,String> getRoomDetails(String roomDetailsStr, String separator) {
        final String [] roomDetailsValues = roomDetailsStr.split(separator);
        final Map<RoomProperty, String> roomDetails = new EnumMap<>(RoomProperty.class);

        roomDetails.put(RoomProperty.ROOM_NAME, roomDetailsValues[0]);
        roomDetails.put(RoomProperty.ROOM_COLOR_HEX, roomDetailsValues[1]);
        roomDetails.put(RoomProperty.HEIGHT, roomDetailsValues[2]);
        roomDetails.put(RoomProperty.AREA, roomDetailsValues[3]);
        roomDetails.put(RoomProperty.COUNT_ELEMENTS, roomDetailsValues[4]);


        return roomDetails;
    }

}

Analogicznie jak w ElementFactory mamy jedną publiczną metodę.

Na początek wyciągamy pierwszą linię z listy i na jej podstawie tworzymy tak jak poprzednio mapę wartości – robi to dla nas metoda getRoomDetails (dzieli ona Stringa na tablicę Stringów na podstawie podanego separatora).

Następnie przygotowujemy interesujące nas dane o pokoju – wyciągając je z mapy.

Następnie musimy każdą kolejną linię z listy wrzucić do ElementFactory, ponieważ zostały nam już tylko w liście informację o elementach pokoju. 😉

Na koniec zwracamy obiekt Room.

HomeFactory

Ostatnią klasą odpowiedzialną za tworzenie obiektów jest HomeFactory.

Analogicznie jak w poprzednich klasach wystawiamy tylko jedną publiczną metodę.

package factories;

import models.Home;
import models.Room;
import models.properties.HomeProperty;

import java.util.EnumMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

public class HomeFactoryImpl implements HomeFactory {
    private static final HomeFactory instance = new HomeFactoryImpl();
    private final RoomFactory roomFactory = RoomFactoryImpl.getInstance();

    private HomeFactoryImpl() {

    }

    public static HomeFactory getInstance() {
        return instance;
    }

    public Home createHome(List<String> homeStr, String separator) {
        int indexLine = 0;
        final Map<HomeProperty, String> homeDetails = getHomeDetails(homeStr.get(indexLine), separator);
        indexLine++;

        final String homeName = homeDetails.get(HomeProperty.HOME_NAME);
        final String homeAddress = homeDetails.get(HomeProperty.ADDRESS);
        final int homeMadeCount = Integer.valueOf(homeDetails.get(HomeProperty.HOMEMADE_COUNT));
        final int roomCount = Integer.valueOf(homeDetails.get(HomeProperty.ROOMS_COUNT));
        final List<Room> rooms = new LinkedList<>();

        for (int i = 0; i < roomCount; i++) {
            List<String> roomDetailsList = new LinkedList<>(homeStr.subList(indexLine, homeStr.size()));
            final Room room = roomFactory.createRoom(roomDetailsList, separator);
            final int roomElementsCount = room.getElements().size();
            indexLine  += roomElementsCount + 1;
            rooms.add(room);
        }

        return new Home(homeName, homeAddress, homeMadeCount, rooms);
    }

    private Map<HomeProperty, String> getHomeDetails(String homeDetailsStr, String separator) {
        final String [] homeDetailsValues = homeDetailsStr.split(separator);
        final Map<HomeProperty, String> homeDetails = new EnumMap<>(HomeProperty.class);

        homeDetails.put(HomeProperty.HOME_NAME, homeDetailsValues[0]);
        homeDetails.put(HomeProperty.ADDRESS, homeDetailsValues[1]);
        homeDetails.put(HomeProperty.HOMEMADE_COUNT, homeDetailsValues[2]);
        homeDetails.put(HomeProperty.ROOMS_COUNT, homeDetailsValues[3]);


        return homeDetails;

    }
}

Działanie klasy jest podobne do RoomFactory.

Przy użyciu metody getHomeDetails i pierwszej linii z listy tworzymy mapę podstawowych wartości o domie.

Wyciągamy je z mapy do osobnych pól, aby mieć już przygotowane wartości do utworzenie obiektu.

Następnie w pętli musimy wyciągać odpowiednio sublisty z głównej listy. Na początku początkowym indeksem jest 1, a końcowym jest zawsze rozmiar tablicy. W HomeFactory nigdy nie wiemy, gdzie kończy się np. pierwszy pokój – zawsze przesyłamy coraz mniejszą sublistę – po prostu pilnujemy się tego, aby pierwszy element zawsze odpowiadał za informację o pokoju.

W następnym obiegu pętli naszym indeksem będzie 1 + ilość elementów w utworzonym wcześniej pokoju i tak, aż do przejścia po wszystkich w pokojach pętli.

Na sam koniec zwracamy obiekt Home.

HomeLoader

Na koniec musimy wczytać informacje o domie z pliku.

import factories.HomeFactory;
import factories.HomeFactoryImpl;
import models.Home;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.LinkedList;
import java.util.List;

public class HomeLoaderImpl implements HomeLoader {
    private static final HomeLoader instance = new HomeLoaderImpl();
    private final HomeFactory homeFactory = HomeFactoryImpl.getInstance();

    private HomeLoaderImpl() {

    }

    public static HomeLoader getInstance() {
        return instance;
    }


    @Override
    public Home loadHome(String fileName, String separator) {
        List<String> lines = readFile(fileName);
        return homeFactory.createHome(lines, separator);
    }

    private List<String> readFile(String fileName) {
        List<String> lines = new LinkedList<>();
        BufferedReader bufferedReader;

        try {
            bufferedReader = new BufferedReader(new FileReader(fileName));

            String line = bufferedReader.readLine();
            while (line != null) {
                lines.add(line);
                line = bufferedReader.readLine();
            }

            bufferedReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return lines;
    }

}

Nie ma tutaj nic skomplikowanego, na początku wczytujemy cały plik i jego treść wrzucamy do już gotowej HomeFactory.

Plik wczytujemy linia po linii i tak zapisujemy do naszej listy.

Testy

Przygotowane jest 7 testów, wszystkie testy możecie zobaczyć tutaj na repozytorium. Bez sensu, abym to wszystko tutaj wklejał. 😛

Dodatkowe uwagi

  • Można było uzyskać dodatkowe 100 pkt za prostą konstrukcję Singletona np.:
private static final HomeLoader instance = new HomeLoaderImpl();
private final HomeFactory homeFactory = HomeFactoryImpl.getInstance();

private HomeLoaderImpl() {

}

public static HomeLoader getInstance() {
    return instance;
}

Chodzi o to, że klasy bezstanowe (wykonujące tylko dane operację) mogą być Singletonami, aby przy każdym użyciu nie tworzyć nowego obiektu.

  • Klas RoomFactory i ElementFactory nie powinny być publiczne, tylko widoczne w paczce – z paczki powinna „wystawać” tylko klasa HomeFactory (być publiczna). 😉
  • Proszę nie zmieniać modeli (nie usuwać, nic nie dodawać – no chyba, że naprawde brakuje), które podaję. W jednym przypadku wywaliły się wszystkie testy, które napisałem z braku metody equals i hashCode.
  • Powtarzam jeszcze raz: w nieco trudniejszych zadaniach naprawdę warto pisać testy, rozwiązanie tego zadania zajęłoby mi więcej czasu, gdybym za każdym razem musiałbym testować ręcznie.
  • Warto programować w stylu – jak ja to nazywam Waterfall programming – czyli dzielić jedną metodę na wiele prywatnych -> Waterfall, ponieważ każda kolejna metoda znajduje się poniżej publicznej. 😉
  • Wysłanie do mnie zadania można utożsamiać z produkcja – usuwajcie zawsze niepotrzebne println!
  • W tym przypadku warto byłoby również stworzyć Buildery dla modeli, gdy mamy więcej niż 3 argumenty w konstruktorze. 😉

Podsumowanie

Gratuluję 5 osobom, które podjęły się wykonania zadania. Nie każdemu udało się uzyskać 100%, ale i tak naprawdę każdemu bardzo gratuluję i życzę powodzenia w kolejnych zadaniach! 😉