
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! 😉