Referencja do serwisu w obiekcie domenowym

0

Cześć,

zacząłem się zastanawiać dlaczego raczej nie przekazuje się serwisów budując obiekty domenowe? Ma to jakieś wady? A może się tak robi, tylko ja tego jeszcze nie widziałem?

Standardowe podejście:

Book book = booksFactory.create("Ender's Game", "Orson Scott Card")
booksRepository.save(book);

Dlaczego nie robimy np. tak:

Book book = booksFactory.create("Ender's Game", "Orson Scott Card")
book.save();

W drugim przykładzie zakładam, że fabryka przy tworzeniu obiektu (np. mapowaniu go z postaci bazodanowej) dodatkowo w konstruktorze przekazuje repozytorium.
Jeśli jest to kwestia SRP to można znaleźć inne przykłady gdzie takie przekazanie jakiegoś portu do obiektu wydawałoby się ok. Np book.fetchPage(int page)

1

Jeśli miałbym tak zrobić, to nie przekazywałbym tam serwisu jako takiego, tylko raczej jakieś Function albo Supplier.

0
Shalom napisał(a):

Jeśli miałbym tak zrobić, to nie przekazywałbym tam serwisu jako takiego, tylko raczej jakieś Function albo Supplier.

A dlaczego tak nie robisz? Bo próbuję znaleźć jakieś wady tego rozwiązania i dlaczego to się nie przyjęło.
Bo poszedłbym z tym nawet dalej - oznaczając wszystkie pola takiej klasy jako private (może oprócz id)

1

Drugie podejście to tzw. active records, które jest wspierane w takich frameworkach jak np. Ruby on Rails. W Springu też da się to osiągnąć: https://www.baeldung.com/spring-inject-bean-into-unmanaged-objects, chociaż referencja do beana/singletona z obiektu, który żyje jakiś krótszy czas mogłaby wydawać się nieco kontr-intuicyjna.

Osobiście nie spotkałem się z takim podejściem w Javie. Zwykle kod odpowiedzialny za wczytywanie i zapisywanie encji znajdował się poza samą encją. Encja przechowuje stan i nim zarządza (albo serwis), natomiast odczyt i zapis realizowany jest przez jakieś DAO/Repository. Natomiast nie jestem 100% przekonany, że tak musi być i tak jest najlepiej :)

0

Podejrzewam, że winna temu jest nieszczęsna obsługa transakcji w Springu. Z twojego przykładu - obiekt book nie będzie zarządzane przez kontekst springowy, więc takie @Transactional nie zadziała. Tzn. pewnie nie zadziała, bo dawno się nie bawiłem w Springa.

0
Charles_Ray napisał(a):

Drugie podejście to tzw. active records, które jest wspierane w takich frameworkach jak np. Ruby on Rails. W Springu też da się to osiągnąć: https://www.baeldung.com/spring-inject-bean-into-unmanaged-objects, chociaż referencja do beana/singletona z obiektu, który żyje jakiś krótszy czas mogłaby wydawać się nieco kontr-intuicyjna.

Ale mógłbym też to wstrzykiwać bez całej magii Springa. W ogóle na razie myślę o oderwaniu od springa, transakcji itd. Bo może powód dla którego tego się nie robi to właśnie zapisywanie bezpośrednio encji w bazie danych przez Hibernate.

Najprostsza wersja bez Springa:

class Book(name: String, save: (Book) -> Unit)

class BooksFactory(
    private val booksRepository: BooksRepository
) {

    fun create(name: String) = Book(name, booksRepository::save)

}
Charles_Ray napisał(a):

Osobiście nie spotkałem się z takim podejściem w Javie. Zwykle kod odpowiedzialny za wczytywanie i zapisywanie encji znajdował się poza samą encją. Encja przechowuje stan i nim zarządza (albo serwis), natomiast odczyt i zapis realizowany jest przez jakieś DAO/Repository. Natomiast nie jestem 100% przekonany, że tak musi być i tak jest najlepiej :)

Też co do samego zapisywania nie jestem przekonany czy powinno być zawarte w obiekcie, po prostu najprościej mi było sobie to wyobrazić.
Ale równie dobrze mogłaby być np. metoda translate(String language), która zwróci książkę z przetłumaczonym tytułem (uderzając przy tym do jakiegoś serwisu tłumaczącego)

0

Ale mógłbym też to wstrzykiwać bez całej magii Springa. W ogóle na razie myślę o oderwaniu od springa, transakcji itd. Bo może powód dla którego tego się nie robi to właśnie zapisywanie bezpośrednio encji w bazie danych przez Hibernate.

Hibernate zakłada, że istnieje transakcja z aktywną sesją. W jakiś sposób musisz ją umieścić w ThreadLocal, co swoją magią zapewnia (albo i nie - różnie z tym bywa) Spring. Nie wiem czy nie skończyłoby się na manualnej obsłudze transakcji z poziomu JPA API wtedy. Aby coś zapisać w bazie, musisz mieć transakcję, przynajmniej na poziomie JDBC.

Ale równie dobrze mogłaby być np. metoda translate(String language), która zwróci książkę z przetłumaczonym tytułem (uderzając przy tym do jakiegoś serwisu tłumaczącego)

To już raczej update takiej encji, skoro coś tłumaczysz. Albo zwrócenie nowej. A co jeśli proces tłumaczenia jest asynchroniczny - zaczyna puchnąć taka encja, która miała ładnie wszystko chować. Nie wiem czy idea active records nie była taka, że to jest po prostu "głupi" rekord z bazy danych, który możesz sobie zapisać. Obawiam się, że przy mieszaniu encji jako stanu z operacjami biznesowymi na encji powstanie niezły potworek.

0
Charles_Ray napisał(a):

Hibernate zakłada, że istnieje transakcja z aktywną sesją. W jakiś sposób musisz ją umieścić w ThreadLocal, co swoją magią zapewnia (albo i nie - różnie z tym bywa) Spring. Nie wiem czy nie skończyłoby się na manualnej obsłudze transakcji z poziomu JPA API wtedy. Aby coś zapisać w bazie, musisz mieć transakcję, przynajmniej na poziomie JDBC.

Dawno nie używałem Hibernate, więc może nie widzę problemu. Może nie chciałbym wrzucać tego obiektu Book do hibernate tylko mapować go sobie na encję bazodanową w innej warstwie i nią sobie zarządzać? Na razie myślę o tym w oderwaniu od springa/jdbc/hibernate.

Charles_Ray napisał(a):

To już raczej update takiej encji, skoro coś tłumaczysz. Albo zwrócenie nowej. A co jeśli proces tłumaczenia jest asynchroniczny - zaczyna puchnąć taka encja, która miała ładnie wszystko chować. Nie wiem czy idea active records nie była taka, że to jest po prostu "głupi" rekord z bazy danych, który możesz sobie zapisać. Obawiam się, że przy mieszaniu encji jako stanu z operacjami biznesowymi na encji powstanie niezły potworek.

Wiadomo, że trzeba się pilnować (tak samo jak przy olbrzymich serwisach, które wszystko robią), ale jeśli ta metoda translate sprawdzałaby tylko stan wewnętrzny i delegowała akcję do jakiegoś portu to nie wydaje mi się, żeby musiał z tego powstać potworek. Metoda mogłaby zwracać jakiś Future<TranslatedBook> czy coś w tym stylu. Plusem tego rozwiązania jest to, że takie obiekty ładnie enkapsulują swoje właściwości i pozwalają na fajną eksplorację domeny. W głowie mi się to na razie klei :D

0

Robi się tak, czasem to widuję w różnych dllkach czy api. Ale później okazuje się to utrudnieniem i jest łatwiej jak obiekty trzymające dane (rekordy) są głupie. Przykładowo możesz zserializować takie książki lokalnie do zapisania, zdeserializować przy następnej sesji i zapisać, przy active records się tak nie da - musiałbyś mapować obiekt na inny głupszy obiekt i z powrotem.
Jest parę innych przykładów gdzie to utrudnia - przykładowo klonowanie obiektów, lub kopiowanie danych między dwoma bazami (zapis do innej niż odczyt).
Albo powiedzmy w Twoim przykładzie z translate - zmiana języka tłumaczenia (parametrów lub serwisu tłumaczącego) podczas działania aplikacji może być trudniejsza.
Trudniejsze staje się testowanie tego obiektu i wszystkich klas które z niego korzystają. Trudniejsze staje się tworzenie builderów i fabryk które może tworzą klasy które korzystają z tych obiektów.
Ogólnie może prowadzić to do przekazywania tych serwisów i żonglowania nimi w wielu miejscach gdzie byś ich normalnie nie potrzebował.
Możesz próbować - utrudnienia wyjdą w praniu. W prostej aplikacji nie powinno być z tym problemów

0

Trochę średni pomysł moim zdaniem wrzucać do encji/dto repozytorium. Dtosy raczej nie mają logiki.

1

Plusem tego rozwiązania jest to, że takie obiekty ładnie enkapsulują swoje właściwości i pozwalają na fajną eksplorację domeny. W głowie mi się to na razie klei :D

IMO to dobry kierunek myślenia, który pozwala minimalizować anemiczne encje i rozsianie logiki po wielu serwisach. Swoją droga jednym z częstych pytań odnośnie implementacji DDD jest w jaki sposób wstrzyknąć do encji EventPublishera - podobny wątek.

2

Jest jeszcze jeden myk:

Book {
 Function<Repository, BookPage> fetchPage(int i);
}

Potem, żeby to się lepiej komponowało zmieniamy jeszcze to Function w normalną Nomadę i jest całkiem ok.

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