Naszej firmie udało się uzyskać nowe i ekscytujące zlecenie, mamy zbudować system opłat dla przewoźnika morskiego. Nasz klient jest właścicielem firmy oferującej przewóz ludzi, pojazdów i towarów drogą morską. Naszym zadaniem jest zaprojektowanie i wykonanie systemu poboru opłat za taką usługę.
Podstawowy interfejs może być wykonany na podstawie konsoli i interakcji z nią. W trakcie poszczególnych zadań będziemy posługiwali się terminem ekran pod tym pojęciem rozumiem wydruk z konsoli możliwych komend i oczewkianie na informacje od użytkownika np:
-------------------------------------------
Witamy w systemie twój rejs, wybierz akcję:
1. Utworzenie nowego rejsu
2. Odczyt zapisanych danych rejsu
3. Wyszukanie uczestnika rejsu
...
1 // użytkownik wybiera pierwszą opcję i konsola drukuje interfejs zgodnie w wyborem
-------------------------------------------
Witamy w systemie twój rejs, wybierz akcję:
1. Dodanie osoby do rejsu
2. Dodanie pojazdu do rejsu
3. Dodanie towaru do rejsu
4. Zakończenie rejsu
5. Odczyt zapisanych danych rejsu
6. Wyszukanie uczestnika rejsu
Podczas samego zadania, klient wymaga od nas aby kod był jak najwyższej jakości, do pomocy nam przyjdą wzorce projektowe, a cały kod postaramy się pokryć testami, które pozwolą nam ze spokojnym sumieniem wdrożyć aplikację na produkcję. Pamiętajcie, nie ma programistów którzy wiedzą wszystko, nie bójcie się korzystać z zewnętrznych źródeł czy nawet bibliotek.
Zadanie to polega na przygotowaniu podstawowego interfejsu na kontakt z użytkownikiem, na tym etapie projektowania aplikacji, wystarczający będzie kontakt za pomocą konsoli. System.out
drukuje pytania w konsoli, natomiast new Scanner(System.in)
przyjmuje odpowiedzi. Do wykonania będzie:
- Uzyskanie informacji o rodzaju biletu, osoba czy pojazd, po wyborze, przeniesienie do odpowiedniego ekranu po czym danie możliwości powrotu do poprzedniego ekranu. ######Podpowiedź: Można wykorzystać wzorzec projektowy strategia i fabryka abstrakcyjna.
W ekranie osoby, musimy poprosić o wiek, w zależności od niego dostosować cenę biletu, wyświetlić ją na ekranie i dać możliwość powrotu do ekranu głównego.
Stawki:
- do 3 lat: 0zł
- od 3 do 18 lat: 5zł
- od 18 do 70 lat: 10zł
- powyżej 70 lat: 5zł ######Podpowiedź: Można wykorzystać wzorzec projektowy visitor i factory.
W ekranie pojazdu, musimy wybrać typ pojazdu, w zależności od wybranego typu w taki sposób naliczane będą opłaty za przejazd.
Stawki:
- osobowy: 20zł
- motor: 10zł
- ciężarówka: 10zł za tonę
- autobus: 5zł za metr
Więc w ekranie pojazdu, pytamy o rodzaj pojazdu, w zależności od rodzaju, możemy ale nie musimy zadać dodatkowe pytanie, np o ciężar czy długość, wyświetlamy naliczoną cenę, po czym dajemy możliwość powrotu do poprzedniego ekranu. ######Podpowiedź: Można wykorzystać wzorzec projektowy visitor i factory.
Do tej pory wszystkie akcje które wykonywaliśmy były "jednorazowe", naszym kolenym zadaniem jest dodanie stanu. Pod pojęciem stan rozumiem modyfikację ekranu głównego o możliwość rozpoczęcia sesji sprzedaży i wyświetlanie całkowitego salda transakcji w ramach jednej sesji. Spowoduje to że każdy powrót do ekranu głównego z ekranu bieltu nie spowoduje zapomnienia kwoty biletu a powiększenie aktualnego salda o kwotę wyliczoną w ekranie biletu. ######Podpowiedź: Można wykorzystać wzorzec projektowy singleton.
Ekran główny powinien posiadać możliwość wyświetlenia aktualnego stanu aplikacji. Pod tym pojęciem rozumiem dodanie możliwości wyświetlenia wszystkich dotychczasowych transakcji. Czli typu biletu (pojazd/osoba), rodzaju (dziecko/nastolatek/ciężarówka/motor) biletu oraz jego całkowitej ceny.
Ekran główny powinien posiadać opcję zapisu całego obecnego stanu aplikacji. Zapisać powinniśmy do pliku tak aby w przyszłości móc go odczytać.
Ekran główny powinien mieć możliwość przywrócenia dowolnej sesji zapisanej w pliku, po wyborze opcji powinny wyświetlić nam się dostępne pliki zapisu. Po wyborze dowolnego z nich, zapisana sesja powinna być przywrócona, i wszystkie możliwe opcje dotyczące sesji powinny być aktywne (sprawdzenie stanu/dodanie blietu itp.).
Powinniśmy mieć możliwość zakończenia obecnej sesji, co powoduje zapisanie do pliku i wyczyszczenie wszystkich danych w pamięci aplikacji związanych z nią.
Dodajmy obsługę przewozu towaru, wyliczanie ceny na podstawie objętości - 50 zł za m3.
Dodajmy możliwość wyszukiwania osób po imieniu i nazwisku we wszystkich rejsach, tak więc osoba musi dostać kontekst nazwy, a stan musi wzbogacić się o jego przechowywanie. Wszytko to musi być zapisane podczas kończenia poszczególnych sesji.
Dodajmy łączenie pojazdu z właścicielem który też płynie danym promem, i ograniczenie unimożliwiające dodanie auta bez właściciela.
Niech każdy rejs będzie powiązany z promem. Promy mają jednostki miejsca. Każdy prom może przewieźć 1000 jednostek, człowiek zabiera 5/10 jednostek, pojazdy 15/30/50 a towar 5 za m3, wprowadźmy możliwość sprawdzenia ile promowi zostało miejsca, oraz walidację przy sprzedaży kolejnego biletu, czy na promie będzie miejsce biorąc pod uwagę dotychczasowe sprzedane bilety.
Nasz biznes stwierdził że musimy wprowadzić promocje. Dodajcie zniżki, zależne od konfiguracji bilietów. Niech całkowity koszt sesji przelicza się ponownie podczas wykrycia istnienia określonej konfiguracji. Możliwe konfiguracje:
- Osoba dorosła i dziecko do 18 roku życia - 20% zniżki na oba bilety
- 2 samochody - 10% zniżki na oba bilety
- 2 dorosłych i dowolna ilość dzieci do 18 roku życia - 15% zniżki na wszystkie bilety
- Samochód i min. 3 osoby - 20% zniżki na auto i 10% zniżki na każdą osobę
- Min. 5 osób - 10% zniżki na każdą osobę
- Min. 7 osób - 15% zniżki na każdą osobę
- Za każde 3 samochody powyżej 2 do maksymalnej ilości 26 aut - += 5% (2 samochody to 10%, 5 samochodów to 15% a 8 samochodów to 20%) ######Podpowiedź: Można wykorzystać wzorzec projektowy strategy lub decorator. Tutaj baaardzo przydałyby się testy jednostkowe.
Powinniśmy napisać testy jednostkowe pokrywające wszystkie klasy serwisowe, a więc wszystkie metody przeliczeniowo-pomocnicze.
Nasz biznes stwierdził, że warto dać możliwość wydruku rachunku naszym użytkownikom. Tak więc potrzebujemy kolejnego punktu w menu oraz ekranu na którym wyświetlimy wszystkie zdobyte do tej pory informacje. Czyli ile jakich biletów, jaki statek/rejs, jakie zniżki do jakich biletów zostały zastosowane.
####Rady ogólne
- Pamiętajcie o obiektowości, enkapsulując domenowo spójną logikę w jednym obiekcie, możememy nie dość że go dobrze przetestować, napisać metody pomocnicze w stylu toString czy equals, to jeszcze dobrze panować nad kodem. A co najważniejsze, takie rozwiązania przeważnie są zwyczajnie - ładne :). ####Wzorce projektowe do wykorzystania:
-
Singleton:
public enum EnumIvoryTower { INSTANCE; String foo = "test"; String getFoo() { return foo; } EnumIvoryTower enumIvoryTower1 = EnumIvoryTower.INSTANCE; EnumIvoryTower enumIvoryTower2 = EnumIvoryTower.INSTANCE; assertEquals(enumIvoryTower1, enumIvoryTower2); // true assertEquals(enumIvoryTower1.getFoo(), "test");
-
Strategy
public interface TextFormatterStrategy { public String format(String text); } public class LowerCaseFormatter implements TextFormatterStrategy { public String format(String text) { return text.toLowerCase(); } } public class UpperCaseFormatter implements TextFormatterStrategy { public String format(String text) { return text.toUpperCase(); } } public class FormatterContext { private TextFormatterStrategy strategy; public void set(TextFormatterStrategy strategy) { this.strategy = strategy; } public void print(String text) { String formattedText = strategy.format(text); System.out.println(formattedText); } } public class Client { private static final String TEXT = "Strategy Design Pattern"; public static void main(String[] args) { FormatterContext context = new FormatterContext(); context.set(new CapitalizeFormatter()); context.print(TEXT); context.set(new UpperCaseFormatter()); context.print(TEXT); context.set(new LowerCaseFormatter()); context.print(TEXT); } }
więcej informacji i źródło: https://javadeveloper.pl/wzorzec-strategia/
-
Visitor - to wzór z gwiazdką, trudniejszy ale niezwykle satysfakcjonujący
public class Customer { //... public <T> T accept(CustomerVisitor<T> visitor){ return visitor.visit(this); } //... } public interface CustomerVisitor<T> { T visit(NormalCustomer customer); T visit(VipCustomer customer); T visit(GroupCustomer customer); } public class InvitationLetterGeneratorVisitor implements CustomerVisitor<Letter> { Letter visit(NormalCustomer customer) {/*...*/} Letter visit(VipCustomer customer) {/*...*/} Letter visit(GroupCustomer customer) {/*...*/} } public class LetterService { public Letter generateInvitationLetter(Customer customer){ return customer.accept(this); } }
źródło: https://www.nurkiewicz.com/2009/03/wzorzec-visitor-realny-przykad.html
-
Abstract Factory
public interface Animal { String getAnimal(); String makeSound(); } public class Duck implements Animal { @Override public String getAnimal() { return "Duck"; } @Override public String makeSound() { return "Squeks"; } } public interface AbstractFactory<T> { T create(String animalType) ; } public class AnimalFactory implements AbstractFactory<Animal> { @Override public Animal create(String animalType) { if ("Dog".equalsIgnoreCase(animalType)) { return new Dog(); } else if ("Duck".equalsIgnoreCase(animalType)) { return new Duck(); } return null; } } public class FactoryProvider { public static AbstractFactory getFactory(String choice){ if("Animal".equalsIgnoreCase(choice)){ return new AnimalFactory(); } else if("Color".equalsIgnoreCase(choice)){ return new ColorFactory(); } return null; } }
żródło: http://www.braintelligence.pl/wzorce-projektowe-factory/, https://www.baeldung.com/java-abstract-factory-pattern
-
Factory
public interface Polygon { String getType(); } public class PolygonFactory { public Polygon getPolygon(int numberOfSides) { if(numberOfSides == 3) { return new Triangle(); } if(numberOfSides == 4) { return new Square(); } if(numberOfSides == 5) { return new Pentagon(); } if(numberOfSides == 7) { return new Heptagon(); } else if(numberOfSides == 8) { return new Octagon(); } return null; } }
-
Builder
public class BankAccount { private String name; private String accountNumber; private String email; private boolean newsletter; // constructors/getters public static class BankAccountBuilder { private String name; private String accountNumber; private String email; private boolean newsletter; public BankAccountBuilder(String name, String accountNumber) { this.name = name; this.accountNumber = accountNumber; } public BankAccountBuilder withEmail(String email) { this.email = email; return this; } public BankAccountBuilder wantNewsletter(boolean newsletter) { this.newsletter = newsletter; return this; } public BankAccount build() { return new BankAccount(this); } } } BankAccount newAccount = new BankAccount .BankAccountBuilder("Jon", "22738022275") .withEmail("[email protected]") .wantNewsletter(true) .build();
-
Testy jednostkowe: W strukturze projektu obok katalogu main mamy jego odbicie w postaci tests, to jest miejsce w którym odwzorowujemy strukturę katalogu projektowego. Przydatnym skrótem w InteliJ jest Ctrl+Shift+T, automatyczne utworzenie klasy testowej w odpowiednim miejscu.
@Test
public void twoIsEqualToTwo() {
assertEquals(twoService.getTwo(), 2);
}