Book Rentals - Spring Boot - ocena

0

Cześć. Napisałem swoją pierwsza webową apke w SpringBoocie.
Użyłem PostgreSQL.
Jak to wypożyczalnia składa się z:

  • możliwości wypożyczania i zwracania książek z wieloma zabezpieczeniami
  • typowych CRUD'owych operacji
  • systemu kar pieniężnych

Za dużo logiki nie ma. Chodzi mi bardziej o jakość kodu, co poprawić, co usunąć, co zmienić, jakieś uwagi odnośnie testów.
Może coś dodać do tego? Szczerze powiedziawszy, to nie wiedziałem jaki "ficzer" mógłbym tutaj jeszcze wcisnąć.

I pytanie na koniec, na czym się skupić po tym.

Link do GH: bookrentals

1

IMO tworzysz ogromną liczbę mocków w sytuacji, gdzie tak naprawdę nie potrzebujesz ani jednego.

Zamiast pisania (w pseudokodzie):

class AccountRepository {
  /* ... */

  Account findById(Id id) {
    return this.database.findById(Account, id);
  }
}

Możesz podejść do tego bardziej obiektowo - z wykorzystaniem interfejsów;

interface AccountRepository {
  Account findById(Id id);
}

class MySQLAccountRepository implements AccountRepository {
  /* ... */

  Account findById(Id id) {
    return this.database.findById(Account, id);
  }
}

class InMemoryAccountRepository implements AccountRepository {
  accounts: Map<Id, Account>;

  Account findById(Id id) {
    return this.accounts.get(id);
  }
}

Jeśli we wszystkich serwisach będziesz polegał na interfejsie AccountRepository, będziesz w stanie w produkcyjnej aplikacji przekazać implementację MySQLAccountRepository, a w testach InMemoryAccountRepository - dzięki temu obejdziesz się z zerową liczbą mocków :-)

Zmieni to zupełnie Twoje podejście do testów i zaczną nabierać dzięki temu większego sensu - testy oparte o mocki często wyglądają tak: zakładając, że wszystko jest ok, odpal mocka, który potwierdzi, że wszystko jest ok, przez co pokrywają małą ilość rzeczywistego (Twojego) kodu.

Dodatkowo warto byłoby zmienić układ katalogów - wydaje mi się, że @jarekr000000 nazwał kiedyś to architekturą nieśmiałą, co zaskakująco dobrze oddaje tę sytuację: patrzę na katalogi, widzę service, repository czy configuration... i nie wiem co z czym się łączy, nie wiem o co w aplikacji chodzi.

Zamiast katalogować aplikację wzorcami projektowymi, znacznie przejrzyściej wychodzi układ względem domeny (tego, co aplikacja robi):

bookrental/

  Account/
    Account.java
    AccountRepository.java
    MySQLAccountRepository.java
    InMemoryAccountRepository.java

  Book/
    Book.java
    BookRepository.java

...

Filmik w temacie:

.

0

O ile katalogowanie rozumiem i to zrobię, tak pierwszą poradę nie wiem jak zrobić.

  1. Rozszerzam interfejs o klasę CrudRepository i jakaś inna klasa ma ten interfejs jeszcze implementować?
  2. Czym się różni w takim razie MySQLAccountRepository od InMemoryAccountRepository?
  3. Mam rozumieć, że w testach nie mockuje żadnej klasy tylko używam już gotowego InMemory... ?

Różnica pomiędzy tym co mam teraz polega tylko na dodatkowej klasie InMemoryAccountRepository, prawda?

1

Rozszerzam interfejs o klasę CrudRepository i jakaś inna klasa ma ten interfejs jeszcze implementować?

Nie możesz rozszerzać interfejsu o klasę, zatem nie do końca rozumiem pytanie.

Czym się różni w takim razie MySQLAccountRepository od InMemoryAccountRepository?

Sposobem działania / zachowaniem - MySQLAccountRepository pobiera dane z bazy danych (stąd wewnątrz masz this.database.findById), podczas gdy InMemoryAccountRepository działa na wewnętrznej hashmapie (dlatego tam z kolei jest this.accounts.get).

Mógłbyś nawet mieć np. FileAccountRepository oparte o plik CSV i Twoje wszystkie serwisy nadal działałyby poprawnie - dzięki interfejsowi nie ograniczasz swojej aplikacji tylko do MySQLa, możesz szybciej prototypować (możesz zrobić kilka pustych interfejsów i zająć się implementacją później) no i dochodzą też porządne testy.

Znam kilka osób, które oparły prototypy aplikacji właśnie o takie FileCośtamRepository i dopiero później (po etapie prototypowania) przeskoczyli na prawdziwą bazę danych - przeskok był bezproblematyczny właśnie dlatego, że wszystkie serwisy aplikacji działały na abstrakcji (interfejsie CośtamRepository) i nie obchodziło ich to czy pod spodem działa MySQL, czy też ręcznie obrabiane pliki.

Fachowo mówimy w tej chwili o separation of concerns i dependency inversion principle.

Mam rozumieć, że w testach nie mockuje żadnej klasy tylko używam już gotowego InMemory... ?

Tak, dokładnie.

Różnica pomiędzy tym co mam teraz polega tylko na dodatkowej klasie InMemoryAccountRepository, prawda?

Pojawia się dodatkowy interfejs (AccountRepository), pojawiają się jego dwie implementacje (InMemoryAccountRepository + MySQLAccountRepository) no i wywalasz wszystkie mocki z testów.

0

Okej, rozumiem całą ideę. Nie do końca wiem jednak jak to zrobić. Teraz mam np:
public interface BookRepository extends CrudRepository<Book, Integer> czyli korzystam z wbudowanych już metod crudowych. Ty u góry podałeś bym stworzył całkiem nowy interfejs bez rozszerzania tego. Skąd teraz wziąć te metody? Samemu implementować?

Account findById(Id id) {
    return this.database.findById(Account, id);
  }

czym jest to database? W sensie jak wyglądała by inicjalizacja tego.

1

Rozszerzanie takiego CrudRepository jest swego rodzaju anty-patternem - interfejs powinien być sprecyzowany i zawierać tylko metody, które potrzebujesz.

Co nie znaczy oczywiście, że trzeba wynajdywać koło od nowa - spójrz na coś takiego:

class MySQLAccountRepository implements AccountRepository {
  private CrudRepository<Account, Integer> repository;

  // tutaj konstruktor

  public Account findById(Id id) {
    return this.repository.findById(id); // czy jak tam ta metoda się nazywa
  }
}

Wykorzystanie kompozycji zamiast dziedziczenia może początkowo sprawiać wrażenie zachęcającego do duplikowania istniejącego już kodu (przecież CrudRepository już daje mi metodę Foo!), lecz dzięki temu:

  1. Nasz interfejs / klasa nie eksponuje na zewnątrz metod, których nie potrzebujemy w aplikacji - taki CrudRepository przykładowo wystawia na świat np. findAll(), saveAll() czy count(), które mogą być przydatne podczas implementacji repozytorium (tak jak w przykładzie wyżej), lecz niekoniecznie od razu wewnątrz całej aplikacji.
  2. Załóżmy, że opieramy nasz interfejs AccountRepository o CrudRepository (czyli załóżmy, że mamy interface AccountRepository extends CrudRepository<...>) - w jaki sposób chcesz szybko zaimplementować te kilkanaście metod z CrudRepository w Twoim hipotetycznym InMemoryAccountRepository? Byłoby to dosyć żmudne i - mało tego - prawdopodobnie zbędne, chyba że wszystkie te metody faktycznie wykorzystujesz w aplikacji.
  3. Układanie klas w hierarchie przeważnie kończy się źle długofalowo - w większości przypadków wystarczy kompozycja.
  4. Można jeszcze dywagować na temat persistence ignorance, lecz jest to dosyć grząski grunt, więc jedynie zasiewam ziarno, że coś takiego też istnieje i jest nazwane.
0

Dobra, czyli mam zrobić coś takiego jeżeli chodzi o aplikację poza testami: gist.
Natomiast jeżeli chodzi o tę metodę InMemory, którą mam używać tylko w testach - mam tam operować tylko na hashmapie i implementować sam te metody czy w jaki sposób ma to wyglądać?

0

Tak, hashmapa + re-implementacja metod interfejsu tak, aby opierały się na hashmapie.

0

1)Jak mam obejść metody w interfejsie, które miały taką postać:

 @Query("SELECT CASE WHEN COUNT(account) > 0 THEN true ELSE false END FROM Account account WHERE account.id =:accountID")
    boolean doesAccountExistsWithGivenID(@Param("accountID") int accountID);

Bo teraz musze zaimplementować tę metodę w klasie PostgreSQLAccountRepository

2) Mógłbys zaimplementować przykładową metodę np. findAll() na hashmapie i jakby wyglądał test dla niego?

0

Ad.1.
Tworzysz HashMap<Integer, Account> i sprawdzasz. Co to znaczy obejść? Powinienes mieć interfejs boolean doesAccountExist(int accountID), a dopiero w jakiejś jego implementacji zrobić to Query.

Ad.2.
Pytanie, czy wyciąganie wszystkich rekordów jest dobre? Może zrób stronicowanie? Jak wyobrażasz sobie pobieranie całej bazy danych na produkcji?

Powinienes mieć interfejs z metodą boolean...
I w sumie lepiej jest użyć ConcurrentHashMap.

1 użytkowników online, w tym zalogowanych: 0, gości: 1, botów: 0