Book Rentals - Spring Boot - ocena

Book Rentals - Spring Boot - ocena
MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
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

Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
1

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

Zamiast pisania (w pseudokodzie):

Kopiuj
class AccountRepository {
  /* ... */

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

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

Kopiuj
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):

Kopiuj
bookrental/

  Account/
    Account.java
    AccountRepository.java
    MySQLAccountRepository.java
    InMemoryAccountRepository.java
  
  Book/
    Book.java
    BookRepository.java

...

Filmik w temacie: .


edytowany 4x, ostatnio: Patryk27
Zobacz pozostałe 6 komentarzy
Patryk27
Ad 1: jeden rabin powie tak, inny inaczej. U siebie trzymam kontrolery w przestrzeni Application, lecz nikogo nie zmuszam ;-) Zrób tak jak CI się wydaje ładniej / sensowniej i najwyżej zobaczysz czy też to czujesz.
MU
Szczerze powiedziawszy to najsensowniej dla mnie był podział, który miałem na samym początku, bo kontrolery miałem w pakunku kontrolery, serwisy w serwisach etc. :P Aczkolwiek może zrobie, żę kontrolery w kontrolerach a reszta tak jak mam teraz, czyli: w bookrentals klasy dotyczące wypożyczeń, w book klasy dotyczące samych w sobie książek etc. Dobry pomysł?
Patryk27
Brzmi oki. Twój początkowy podział może zdawać się mieć sens w przypadku mniejszych aplikacji, lecz staje się cholernie uciążliwy z czasem - zwyczajowo implementuje się / poprawia rzeczy zgodnie z funkcjonalnościami (np. muszę mieć możliwość edycji załączników), a nie wzorcami architektonicznymi (np. muszę mieć koniecznie w tym katalogu repozytorium), więc dzięki układowi zgodnie z domeną nie musisz co chwilę przeskakiwać z katalogu do katalogu, bo wszystko masz w jednym miejscu. W temacie: Jaki to wzorzec architektoniczny?
MU
@Patryk27: dzięki, jakieś konkretne jeszcze uwagi co do samego kodu?
Patryk27
Wydaje mi się, że reszta jest okay :-)
MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
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?

edytowany 3x, ostatnio: flowCRANE
Patryk27
Nie cytuj całego postu ;-)
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
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.


edytowany 5x, ostatnio: Patryk27
MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
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ć?

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

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

Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
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:

Kopiuj
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.

edytowany 1x, ostatnio: Patryk27
MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
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ć?

MU
pomyłka, nie metodę tylko klasę
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
0

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


MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
0

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

Kopiuj
 @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

  1. 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.

edytowany 2x, ostatnio: Patryk27
MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
0
Szalony Polityk napisał(a):

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.

Chodziło mi o PostgreSQLAccountRepository gdzie operuje na private CrudRepository<Account, Integer> repository;
W jaki sposób mam użyć w tej klasie w metodzie doesAccountExistsWithGivenIDinstrukcje @Query("SELECT CASE WHEN COUNT(account) > 0 THEN true ELSE false END FROM Account account WHERE account.id =:accountID"), którą miałem poprzednio w interfejsie.
Mam interfejs z tą metodą. Robie tak jak napisał @Patryk27, czyli interfejs z wszystkimi metodami, które pozniej implementuje w PostgreSQLAccountRepository

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?

Chce wyświetlić po prostu wszystkie dostępne książki. Nie wiem jak to zrobić za pomocą HashMapy i jak wyglądał by test do tego.

edytowany 2x, ostatnio: must
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
1

Ad 1: Przecie już masz taką metodę: https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/CrudRepository.html#existsById-ID-.
Ad 2: this.hashmap.values().

Btw, staraj się skracać nazwy metod - jeśli masz interfejs AccountRepository, metoda nie musi nazywać się getAccountById - wystarczy samo getById. Podobnie nie doesAccountExistsWithGivenID, tylko raczej własnie existsByID lub pochodne.

jak wyglądał by test do tego.

Pobieranie wszystkich książek nie jest funkcjonalnością / nie zawiera żadnej logiki, zatem nie ma co w tym przypadku testować - chyba że jest to część czegoś większego (np. serwisu liczącego statystyki z wszystkich książek z bazy), no ale wtedy to testuje się całościowo to coś większego, a nie samą metodę findAll().


edytowany 3x, ostatnio: Patryk27
MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
0
  1. Możliwe, że jest. Nie szukałem czy są takowe to fakt, tylko sam zrobiłem. Mam wiele innych metod, które używają JPQL, dlatego wygodniej by mi było użyć @Query aniżeli się zagłębiać, które metody są już napisane a które nie.

  2. Zgłoszę się w takim razie z innymi metodami, jak uda mi się zrobić tę klasę InMemory... Swoją drogą, dlaczego akurat tutaj używamy HashMapy?

  3. czy to this w this.repository jest potrzebne?

edytowany 3x, ostatnio: must
Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
0

Ad 1: W takim razie pewien nie jestem - musiałbyś poczytać w dokumentacji jak odpalać zapytania bez adnotacji.
Ad 2: Ponieważ dzięki temu możemy łatwo mapować id obiektu na jego faktyczną instancję - moglibyśmy używać np. tablicy, ale jak wtedy chciałbyś zaimplementować metodę delete bez zmieniania idków wszystkich następnych encji?
Ad 3: Nie, to tylko przykład.


MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
0
  1. Najpewniej trzeba użyć SessionFactory.
  2. Czy na pewno można zainicjalizować CrudRepository, tak o? Gdy usunąłem poprzednią wersję AccountRepository która rozszerzała CrudRepository wywaliło mi błąd.:
Kopiuj
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled.
2019-01-03 23:21:06.427 ERROR 7608 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in bookrental.account.PostgreSQLAccountRepository required a bean of type 'org.springframework.data.repository.CrudRepository' that could not be found.


Action:

Consider defining a bean of type 'org.springframework.data.repository.CrudRepository' in your configuration.

AccountRepository, PostrgreSQLAccountRepository oraz AccountService wygląda tak: https://gist.github.com/must1/1929fedff64af9857362ea963818351a

Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
0

Ad 2: W takim razie najwyraźniej niestety trzeba by podejść inaczej - niestety sam javowcem nie jestem, więc tutaj kończy się moja możliwość podpowiadania :-P Być może mógłbyś np. przez DI wstrzyknąć sobie entity manager i na jego podstawie budować zapytania, lecz może też da się podejść do tego lepiej.

Przy okazji co do Twojego gistu: AccountService jest słabą nazwą na klasę, bo nie wiadomo tak właściwie czym się zajmuje (a jeśli wszystkim, to też źle). Serwisy powinny być sprecyzowane (np. AccountStatisticsReporter), nie ogólne + nie potrzebujesz serwisu, którego jedynym zachowaniem jest odpalanie repo - nie ma nic złego we wrzucaniu repozytorium jako zależności do kontrolera.


edytowany 1x, ostatnio: Patryk27
MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
0

Co do nazw, to pozmieniam na końcu.

Rozwiązaniem tego o co się pytałem jest:

Kopiuj
@Repository
public interface AccountRepository implements  CrudRepository<Account, Integer>{
}
Kopiuj
@Repository
public class PostgreSQLAccountRepository {

    private final AccountRepository repository;

    @Autowired
    public PostgreSQLAccountRepository(AccountRepository repository) {
        this.repository = repository;
    }

    public void deleteById(Integer id) {
       this.repository.deleteById(id);
    }

    public List<Account> findAll() {
        return (List<Account>) repository.findAll();
    }
   
     public boolean doesAccountExistsWithGivenID(int accountID){
       ...
     }

tylko nie wiem czy jest to dobre rozwiązanie, co sądzisz?

Patryk27
Moderator
  • Rejestracja:ponad 17 lat
  • Ostatnio:ponad rok
  • Lokalizacja:Wrocław
  • Postów:13042
0

To nie jest rozwiązanie w ogóle:

  1. Klasa PostgreSQLAccountRepository nie implementuje AccountRepository, czyli tracisz możliwość podmiany implementacji (jak chcesz teraz zaimplementować InMemoryAccountRepository i przekazać jedną bądź drugą implementację do serwisu?).
  2. Twój interfejs AccountRepository przestał być wyspecjalizowany i zaczął eksportować na zewnątrz metody w stylu deleteAll(), które niekoniecznie będą Ci potrzebne.

edytowany 4x, ostatnio: Patryk27
MU
w takim razie ciężko o rozwiązanie :/
PM
  • Rejestracja:prawie 8 lat
  • Ostatnio:7 miesięcy
  • Postów:30
0

Kilka uwag:

  1. Nazwy restowych mappingow moga byc zdefiniowane na poziomie klasy zamist powtarzac je przy kazdej metodzie(zamist "books" powinno byc "book")
  2. Brak zdefiniowanych interfejsow w implementacjach serwisów
  3. Manualne przepisywanie kolekcji zamiast uzycia collectors
  4. Serwisy nie powinny zwracac wartosci null
  5. Serwisy zwracaja wartosci String zamiast zmodyfikowane objekty.
  6. Mutowale klasy Entity, moze to prowadzic do trudnych do wykrycia problemow, lepiej uzyc konstruktor kopiujacy.
Zobacz pozostałe 5 komentarzy
Patryk27
@p_maciek: to może inaczej: co, według Ciebie, daje Ci bezwarunkowe tworzenie interfejsów do każdego serwisu? Według mnie, na przykład, nie jest to wcale pisanie zgodnie z dobrymi zasadami, bo podstawową dobrą zasadą zawsze powinno być KISS / YAGNI. Podczas pisania kodu należy kierować się czytelnością i maintenability, które znacznie maleje w momencie, kiedy w trakcie analizy kodu musisz przeciskać się przez setkę interfejsów w imię jakiejś reguły - bo trzeba tak, bo tak,i już. Been there, done that - zrezygnowałem właśnie z wymienionego w poprzednim zdaniu powodu.
Patryk27
Kontynuując: IMO nie należy udawać, że jakieś moduły są niezależne - fragmenty kodu zależą od siebie i jest to całkowicie naturalne (gdyby wszystkie moduły były odrębne, stanowiłyby osobne aplikacje) - martwić powinien co najwyżej wysoki coupling (A zależy bezpośrednio od B, które zależy bezpośrednio od C, które zależy od A i D, które zależą od B, C, D i MagendoBeanFactoryProxyWrapper). Udawanie, że klasa A nie zależy od klasy B, bo przecież ona wymaga BInterface brzmi jak próba kłamania prosto w oczy swoje oraz innych programistów.
PM
Powinny być jak najbardziej niezależne, i po to właśnie tworzymy warstwę abstrakcji. Implementacja serwisu nie powinna byc w ogole publiczna. Odzielenie implementacji od kontraktu daje tyle, że zmiana implemetacji danego serwisu nie wpływa na moduł który z niej korzysta. Ten tak naprawdę nie powinien wiedzieć, że coś się zmienia.
Patryk27
W imię tego powstały fasady, nie interfejsy (https://www.youtube.com/watch?v=ILBX9fa9aJo). Fasada daje możliwość oddzielenia wnętrzności modułu od jego implementacji (czyli osiągnięcia właśnie tego, o czym wspominasz), podczas gdy interfejs daje możliwość stworzenia wielu różnych implementacji (np. dla repozytoriów) - subtelna różnica, lecz nie należy jednego mylić z drugim. Nie będzie wielu różnych implementacji PenaltyService, ponieważ oczekiwane od aplikacji zachowanie jest jedno, zatem nie ma potrzeby tworzyć takiego interfejsu.
jarekr000000
Publiczne metody klasy to... jej interfejs :)
MU
  • Rejestracja:około 11 lat
  • Ostatnio:ponad 5 lat
  • Postów:75
0
p_maciek napisał(a):

Kilka uwag:

  1. Nazwy restowych mappingow moga byc zdefiniowane na poziomie klasy zamist powtarzac je przy kazdej metodzie(zamist "books" powinno byc "book")

Faktycznie, poprawię.

  1. Brak zdefiniowanych interfejsow w implementacjach serwisów

Tak jak pisał, @Patryk27 w komentarzu, nie jest to chyba potrzebne.

  1. Manualne przepisywanie kolekcji zamiast uzycia collectors

Mógłbyś podać przykład o co chodzi dokładnie, w którym miejscu?

  1. Serwisy nie powinny zwracac wartosci null

Tak samo jak w 3.

  1. Serwisy zwracaja wartosci String zamiast zmodyfikowane objekty.

Co złego jest w zwracaniu Stringa? W niektórych miejscach zwracam obiekty, w niektórych Stringa. Wiem też, że powinno się się zwracać HTTP STATUS. Dlaczego wg Ciebie obiekty są najlepsze?

  1. Mutowale klasy Entity, moze to prowadzic do trudnych do wykrycia problemow, lepiej uzyc konstruktor kopiujacy.

Wszystkie pola są oznaczone private, mam je oznaczyć także jako final, nie udostępniać getterów ? Mógłbyś ten punkt rozwinąć?

edytowany 1x, ostatnio: must
PM
  • Rejestracja:prawie 8 lat
  • Ostatnio:7 miesięcy
  • Postów:30
1
  1. Manualne przepisywanie kolekcji zamiast uzycia collectors

Mógłbyś podać przykład o co chodzi dokładnie, w którym miejscu?

public List<Account> getAllAccounts() {
List<Account> accounts = new ArrayList<>();
accountRepository.findAll().forEach(accounts::add);
return accounts;
}

  1. Serwisy nie powinny zwracac wartosci null

Tak samo jak w 3.

public Book deleteBook(int id) {
Book bookToDelete = bookRepository.findById(id).orElse(null);
bookRepository.deleteById(id);
return bookToDelete;
}

  1. Serwisy zwracaja wartosci String zamiast zmodyfikowane objekty.

Co złego jest w zwracaniu Stringa? W niektórych miejscach zwracam obiekty, w niektórych Stringa. Wiem też, że powinno się się zwracać HTTP STATUS. Dlaczego wg Ciebie obiekty są najlepsze?

np. bo trzeba parsować stringa aby sprawdzić co się stało.

  1. Mutowale klasy Entity, moze to prowadzic do trudnych do wykrycia problemow, lepiej uzyc konstruktor kopiujacy.

Wszystkie pola są oznaczone private, mam je oznaczyć także jako final, nie udostępniać getterów ? Mógłbyś ten punkt rozwinąć?

Tzn. chodziło o to aby klasa była niemutowalna, czyli final na polach, gettery mogą być ale bez setterów.

edytowany 1x, ostatnio: p_maciek
MU
jak w takim razie zmieniać pola jak nie za pomocą setterów? Dlaczego settery są złe?
DA
  • Rejestracja:ponad 10 lat
  • Ostatnio:2 miesiące
  • Postów:176
0

Ogólnie co do interfejsów, uważam, że jeżeli jego implementację nazywamy z końcówką Impl, to znaczy, że nie masz pojęcia co ta klasa ma robić. Zatem, mając MyService i MyServiceImpl interfejs jest zbędny, i moglibśmy mieć jedynie sam serwis.

Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)