Architektura aplikacji - hexagonal/podział na pakiety - kilka pytań

Architektura aplikacji - hexagonal/podział na pakiety - kilka pytań
Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
1
Ornstein napisał(a):

Tu mnie zaskoczyłeś. Czytałem trochę o package scope i odniosłem wrażenie, że powinienem zwracać dużą uwagę na to, aby jak najmniej klas było widocznych poza pakietem.

Package-scope nie ma nic wspólnego z dependency inversion. To są jakby osobne mechanizmy.

Package-scope to jest jeden ze sposobów enkapsulacji w javie - sposób na ukrycie niektórych szczegółów, a pozostawienie wydocznym jedynie interfejs modułu. Natomiast w architekturze hexagonalnej, i ogólnie w dependency inversion chodzi o to żeby żeby wysokopoziomowe elementy (domena) nie zależały od niżej poziomowych szczegółów implementacyjnych (widok i baza). W szczególności chodzi o to, żeby zmiana szczegółu (implementacji widoku lub bazy) nie musiała oznaczać zmiany w wysokopoziomowych elementach (domenie). Bez dependency inversion nie zawsze jest to możliwe.

To odniosłem wrażenie, że powinienem zwracać dużą uwagę na to, aby jak najmniej klas było widocznych poza pakietem. to też nie jest do końca prawda.

Jeśli myślisz o pakietach w javie jak o folderach, to równie dobrze wszystkie klasy w pakiecie mogłyby być public. Nic byś nie stracił.

Ale jeśli myślisz o nich jak o modułach, czyli częściach aplikacji odpowiedzialnych za konkretne zadanie, to należy na to patrzeć w taki sposób że moduł wystawia pewien interfejs: class/interface/enum/record (elementy publiczne), żeby inne moduły mogły z nim gadać, a sam wykonuje jakieś operacje, które są już szczegółem implementacyjnym tego modułu (i one mogą być package-private - a właściwie nawet powinny, bo ich zmiana nie powinna wpłynąć na żadnego użytkownika tego modułu). Pod tym względem moduły i klasy zaczynają być do siebie podobne - obie z nich mają interfejs (sposób w jaki ktoś z nich korzysta), i obie mają swój sposób działania (swój wewnętrzny mechanizm i implementacje). Zmiana interfejsu wymaga żeby użytkownicy również się zaktualizowali (dlatego niektórzy mogą Ci radzić żeby public było mało), z kolei zmiana implementacji nie wpływa na nic poza modułem (dlatego ich może być dużo). Ale to nie jest tak, że publiców MUSI BYĆ mało. Chodzi o to że wszystko o czym nie muszą wiedzieć użytkownicy modułu lepiej zrobić package-private, po to żeby przypadkiem nikt z tego nie skorzystał, po to zeby refaktor szczegółów implementacyjnych niepotrzebnie nie wymuszał zmian w innych miejscach.

Ornstein napisał(a):

W pakiecie service praktycznie wszystkie klasy są prywatne, poza klasą odpowiedzialną za wejście. Teraz chciałbym je skonfigurować i stworzyć beana. Nie chcę tego robić w domenie, bo wymieszam domenę ze Springiem. Utworzę nowy oddzielny pakiet registration -> config. Problem polega na tym, że nie jestem w stanie zaimportować prywatnych klas z innego pakietu. Musiałbym nałożyć na nie jakiś interfejs. Nie wiem, czy nie byłby to trochę overengineering.

No to skoro masz jedną klasę odpowiedzialną za wejście, a reszta pakietu jest prywatna, to chyba powinno dać się móc zrobić beana używając tylko i wyłącznie tej klasy wejściowej, a prywatnych elementów nie dotykać?

jarekr000000
  • Rejestracja: dni
  • Ostatnio: dni
  • Lokalizacja: U krasnoludów - pod górą
  • Postów: 4712
2

package scope w javie to niestety g**no.

Widoczność package w javie jest ograniczona tylko do pakietu, natomiast nie ma jej w podpakietach.
To mocno ogranicza użyteczność tego mechanizmu, bo jeśli robimy jakiś większy moduł i naprawdę nie da się wszystkiego włożyć w jeden pakiet (co jestg normalne) to niestety należy użyć okazjonalnie public, żeby dane elementy były dostępne w podpakietach. I wtedy nie wiemy co jest publicznym API, a co jest technicznie potrzebnym public (po to żeby się skompilowało).

W javie jest na to rozwiązanie - mocno niepopularne - czyli obowiązujący od javy 9 module system, który pozwala określić które public są naprawede public (eksportowane), czyli są naszym oficjalnym API, a które public - są tylko na potrzeby modułu. I jest to nawet wymuszane potem przez JVM.
Generalnie nikt z tego nie korzysta :-(

W scali jest to o wiele lepiej rozwiązane, bo jest właśnie hierarchiczny package scope (de facto nazywa się private[your_module_name]).

Również nieźle jest to rozwiązane w kotlinie, gdzie jest public i internal (internal to public na potrzeby modułu) - w zasadzie jest to mechanizm dość podobny javowego module system, tylko na poziomie kompilacji (a nie jvm). W kotlinie (tak jak w javie przy użyciu module system) - każdy "moduł" (składajacy się potencjalnie z kilku package) jest kompilowany jako osobny moduł (maven, gradle).

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
jarekr000000 napisał(a):

package scope w javie to niestety g**no.

Widoczność package w javie jest ograniczona tylko do pakietu, natomiast nie ma jej w podpakietach.

Tylko że w Javie nie ma czegoś takiego jak podpakiet.

Jak masz pakiet foo.bar, to w systemie plików trzeba go wsadzić do foo, przesz co to może sprawiać wrażenie kompozycji, ale to jest artefakt folderów. W języku żadnego "osadzania" nie ma, i o tych pakietach trzeba myśleć jako osobnych bytach. Nie da się zagnieździć pakietów a Javie.

Ornstein
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 121
0
Riddle napisał(a):

Ale jeśli myślisz o nich jak o modułach, czyli częściach aplikacji odpowiedzialnych za konkretne zadanie, to należy na to patrzeć w taki sposób że moduł wystawia pewien interfejs: class/interface/enum/record (elementy publiczne), żeby inne moduły mogły z nim gadać, a sam wykonuje jakieś operacje, które są już szczegółem implementacyjnym tego modułu (i one mogą być package-private - a właściwie nawet powinny, bo ich zmiana nie powinna wpłynąć na żadnego użytkownika tego modułu). Pod tym względem moduły i klasy zaczynają być do siebie podobne - obie z nich mają interfejs (sposób w jaki ktoś z nich korzysta), i obie mają swój sposób działania (swój wewnętrzny mechanizm i implementacje). Zmiana interfejsu wymaga żeby użytkownicy również się zaktualizowali (dlatego niektórzy mogą Ci radzić żeby public było mało), z kolei zmiana implementacji nie wpływa na nic poza modułem (dlatego ich może być dużo). Ale to nie jest tak, że publiców MUSI BYĆ mało. Chodzi o to że wszystko o czym nie muszą wiedzieć użytkownicy modułu lepiej zrobić package-private, po to żeby przypadkiem nikt z tego nie skorzystał, po to zeby refaktor szczegółów implementacyjnych niepotrzebnie nie wymuszał zmian w innych miejscach.

Czy powinienem testować szczegóły implementacyjne modułu które znajdują się w prywatnych klasach, czy wystarczy, że przetestuję publiczny interfejs (class/interface/enum/record)? i tam je sprawdzę?

Przykładowo moduł rejestracji posiada takie klasy:

  • UserRegistrationService - publiczny wystawiony interfejs,
  • UserRegistration - prywatna klasa wywołuję logikę walidacji i tworzenia użytkownika, używa portu do zapisu użytkownika i portu do wysyłania wiadomości email.
  • UserCreator - prywatna klasa odpowiedzialna na tworzenie użytkownika,
  • RequestValidation - prywatna klasa odpowiedzialna za walidację requesta

Testy powinny objąć każdą klasę?

Riddle napisał(a):
Ornstein napisał(a):

W pakiecie service praktycznie wszystkie klasy są prywatne, poza klasą odpowiedzialną za wejście. Teraz chciałbym je skonfigurować i stworzyć beana. Nie chcę tego robić w domenie, bo wymieszam domenę ze Springiem. Utworzę nowy oddzielny pakiet registration -> config. Problem polega na tym, że nie jestem w stanie zaimportować prywatnych klas z innego pakietu. Musiałbym nałożyć na nie jakiś interfejs. Nie wiem, czy nie byłby to trochę overengineering.

No to skoro masz jedną klasę odpowiedzialną za wejście, a reszta pakietu jest prywatna, to chyba powinno dać się móc zrobić beana używając tylko i wyłącznie tej klasy wejściowej, a prywatnych elementów nie dotykać?

Nie mogę tego zrobić. Może z moim podejściem jest coś nie tak.
W pakiecie registration -> domain -> service jest wystawiony jeden publiczny interfejs a pozostałe klasy są prywatne.
Do registration dodaję pakiet config i tam chcę utworzyć beana, ponieważ nie chcę w domenie używać Springa.
To jest to wejście:

Kopiuj
public class UserRegistrationService {
    private final UserRegistration registration;

    public UserRegistrationService(UserRegistration registration) {
        this.registration = registration;
    }

    public ApiResponse register(RegisterRequest request) {
        return registration.registerUser(request);
    }
}

Zależność tej klasy UserRegistration jest prywatna. Podczas tworzenia beana dla UserRegistrationService muszę przekazać tę zależność jako parametr, a nie mogę jej zaimportować bo jest private i znajduję się w innym pakiecie.

Zrobiłem coś takiego, ale czy ma to jakikolwiek sens? Ciężko mi to ocenić.

Do pakietu registration -> domain -> service, dodałem klasę publiczną, która tworzy instancję UserRegistrationService. Ta klasa jest publiczna i mogę ją wywołać w innym pakiecie.

Kopiuj
public class UserRegistrationServiceSetup {


    public UserRegistrationService createUserRegistrationService(RegistrationUserRepository userRepository,
                                                                 RegistrationRoleRepository roleRepository,
                                                                 PasswordEncoder encoder,
                                                                 UserSaver userSaver)  {
        RequestValidation validation = new RequestValidation(userRepository);
        UserCreator userCreator = new UserCreator(encoder, roleRepository);
        UserRegistration userRegistration = new UserRegistration(validation, userCreator, userSaver);
        return new UserRegistrationService(userRegistration);
    }
}

Teraz w pakiecie registration -> config tworzę instancję klasy UserRegistrationServiceSetup.

Kopiuj
@Configuration
class SpringConfiguration {
    @Bean
    UserRegistrationService registrationService(RegistrationUserRepository userRepository,
                                                RegistrationRoleRepository roleRepository,
                                                PasswordEncoder encoder,
                                                UserSaver userSaver) {

        UserRegistrationServiceSetup configuration = new UserRegistrationServiceSetup();
        return configuration.createUserRegistrationService(userRepository, roleRepository, encoder, userSaver);
    }
}
Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
1
Ornstein napisał(a):

Zależność tej klasy UserRegistration jest prywatna. Podczas tworzenia beana dla UserRegistrationService muszę przekazać tę zależność jako parametr, a nie mogę jej zaimportować bo jest private i znajduję się w innym pakiecie.

Czyli mam rozumieć że cała logika domenowa jest w UserRegistration, a UserRegistrationService jest tylko po to żeby dało się to wstrzyknąć do springa?

Jeśli tak, to UserRegistration powinien być public w tym module.

Ornstein napisał(a):

Zrobiłem coś takiego, ale czy ma to jakikolwiek sens? Ciężko mi to ocenić.

Dobrze że robisz eksperymenty. Masz jakiś use-case (rejestracja), i sprawdzasz różne podejścia żeby sprawdzić które pasuje. Dostrzegasz wady i zalety. To jest dobre podejście do projektowania softu.

Ornstein
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 121
0
Riddle napisał(a):
Ornstein napisał(a):

Zależność tej klasy UserRegistration jest prywatna. Podczas tworzenia beana dla UserRegistrationService muszę przekazać tę zależność jako parametr, a nie mogę jej zaimportować bo jest private i znajduję się w innym pakiecie.

Czyli mam rozumieć że cała logika domenowa jest w UserRegistration, a UserRegistrationService jest tylko po to żeby dało się to wstrzyknąć do springa?

Jeśli tak, to UserRegistration powinien być public w tym module.

Tak, cała logika domenowa jest w UserRegistration. W takim razie usunę UserRegistrationService.

Ornstein napisał(a):

Zrobiłem coś takiego, ale czy ma to jakikolwiek sens? Ciężko mi to ocenić.

Dobrze że robisz eksperymenty. Masz jakiś use-case (rejestracja), i sprawdzasz różne podejścia żeby sprawdzić które pasuje. Dostrzegasz wady i zalety. To jest dobre podejście do projektowania softu.

Tylko w tych eksperymentach często trudno mi stwierdzić, czy to co napisałem ma sens, pomimo tego, że program działa.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
Ornstein napisał(a):

Tak, cała logika domenowa jest w UserRegistration. W takim razie usunę UserRegistrationService.

Jeśli UserRegistrationService jest niepotrzebne, to usuń. Myślałem że potrzebujesz jej, żeby wstrzyknąć UserRegistration do springa. (bo oczywiście nie dodałeś adnotacji springowych od klasy domenowej, nie?).

Ornstein napisał(a):

Tylko w tych eksperymentach często trudno mi stwierdzić, czy to co napisałem ma sens, pomimo tego, że program działa.

To jest normalna rzecz. Taka ocena wymaga bardzo dobrej umiejętności projektowania oprogramowania, która przychodzi z kolejnymi próbami i eksperymentami.

Zrób eksperyment, i porównaj sobie w gicie różnice między nimi, popatrz na oba rozwiązania, raz na jedno, raz na drugie. Jeśli któreś Ci się wydaje np. na 75% lepsze, użyj go. Jeśli nie, to możesz po prostu cofnąć eksperyment.

Ornstein
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 121
0
Riddle napisał(a):
Ornstein napisał(a):

Tak, cała logika domenowa jest w UserRegistration. W takim razie usunę UserRegistrationService.

Jeśli UserRegistrationService jest niepotrzebne, to usuń. Myślałem że potrzebujesz jej, żeby wstrzyknąć UserRegistration do springa. (bo oczywiście nie dodałeś adnotacji springowych od klasy domenowej, nie?).

Wcześniej miałem w tej klasie - UserRegistrationService adnotacje springa, ale z uwagi że ta klasa znajduje się w domenie, to z nich zrezygnowałem.

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
Ornstein napisał(a):

Wcześniej miałem w tej klasie - UserRegistrationService adnotacje springa, ale z uwagi że ta klasa znajduje się w domenie, to z nich zrezygnowałem.

No to UserRegistration da się normalnie wstrzyknąć do użycia w kontrolerze springowym?

Ornstein
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 121
0
Riddle napisał(a):
Ornstein napisał(a):

Wcześniej miałem w tej klasie - UserRegistrationService adnotacje springa, ale z uwagi że ta klasa znajduje się w domenie, to z nich zrezygnowałem.

No to UserRegistration da się normalnie wstrzyknąć do użycia w kontrolerze springowym?

Po zmianie na public mogę tę klasę zaimportować w kontrolerze. Muszę jeszcze tylko stworzyć beana dla tej klasy.

Ornstein
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 121
0

Mam teraz taki podział na cztery główne pakiety: adapter, domain, infrastructure i application - ten pakiet jest na ten moment pusty. Przeglądałem kilka projektów na githubie i zauważyłem, że twórcy często tworzą usecase w application. I tak się zastanawiam, może też powinienem tak zrobić.

Taki przykład, feature registration wygląda teraz u mnie tak

Moja domena:
Klasa która odpowiada za rejestrację - wstrzykuję ją do kontrolera

Kopiuj
public class UserRegistration {

    private final RequestValidation validation;
    private final UserCreator userCreator;
    private final UserMapper mapper;
    private final UserSaver userSaver;

    UserRegistration(RequestValidation validation,
                     UserCreator userCreator,
                     UserMapper mapper,
                     UserSaver userSaver) {
        this.validation = validation;
        this.userCreator = userCreator;
        this.mapper = mapper;
        this.userSaver = userSaver;
    }

    public ApiResponse registerUser(RegisterRequest request) {
        validation.validate(request);
        User user = userCreator.createUser(request);
        userSaver.saveUser(mapper.toDto(user));
        return new ApiResponse("User registered successfully.");
    }
}

Klasa odpowiedzialna za tworzenie użytkownika

Kopiuj
class UserCreator {
   //kod
}

Klasa odpowiedzialna za walidację

Kopiuj
class RequestValidation {
    //kod
}

Port i adapter odpowiadający za zapis

Kopiuj
public interface UserSaver {
    void saveUser(UserDto user);
}
Kopiuj
@Service
class TransactionUserSaver implements UserSaver {

    private final UserDAO userDAO;
    private final TransactionTemplate transactionTemplate;
    private final UserMapper mapper;

    public TransactionUserSaver(UserDAO userDAO,
                                TransactionTemplate transactionTemplate,
                                UserMapper mapper) {
        this.userDAO = userDAO;
        this.transactionTemplate = transactionTemplate;
        this.mapper = mapper;
    }

    @Override
    public void saveUser(UserRegistrationDTO userDto) {
        transactionTemplate.executeWithoutResult(status -> {
            User user = mapper.toEntity(userDto);
            int userId = userDAO.save(user);

            for (Role roles : user.getRoles()) {
                userDAO.assignRoleToUser(userId, roles.getId());
            }
        });
    }
}

Gdybym dodał usecase do application, wyglądałoby to tak, że w domenie zostawiłbym tylko logikę walidacji i tworzenia użytkownika, a resztę przeniósł do application.
W application stworzyłbym coś takiego:

Interfejs

Kopiuj
public interface UserRegistration {
    ApiResponse registerUser(RegisterRequest request);
}

I usecase który go implementuje

Kopiuj
@Service
public class UserRegistrationHandler implements UserRegistration {

    private final RequestValidation validation;
    private final UserCreator userCreator;
    private final UserSaver userSaver;

    UserRegistrationHandler(RequestValidation validation,
                            UserCreator userCreator,
                            UserSaver userSaver) {
        this.validation = validation;
        this.userCreator = userCreator;
        this.userSaver = userSaver;
    }

    @Override
    public ApiResponse registerUser(RegisterRequest request) {
        validation.validate(request);
        User user = userCreator.createUser(request);
        userSaver.saveUser(user);
        return new ApiResponse("User registered successfully.");
    }
}

Co myślicie? Tak będzie lepiej?

Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0

Ciężko to przewidzieć. Dodaj, poużywaj - sam zobaczysz co jest prostsze do utrzymania.

Zarejestruj się i dołącz do największej społeczności programistów w Polsce.

Otrzymaj wsparcie, dziel się wiedzą i rozwijaj swoje umiejętności z najlepszymi.