Cześć,
tworzę projekt w ramach nauki. Do tej pory stosowałem package by feature
- na przykład wyglądało to tak: miałem pakiet registration
bez sub-pakietów, a w nim wszystkie klasy związane z rejestracją użytkownika, wszystkie klasy były publiczne.
Postanowiłem to poprawić i zrobić jakiś refaktor. Tak oto trafiłem na pojęcie hexagonal architecture
. Trochę poczytałem m.in. ten wątek:
Architektura heksagonalna to oszustwo
ważne posty odnośnie architektury heksagonalnej
z tego wątku:
some_ONE napisał(a):
Ja heksagonalną/czystą architekturę rozumiem tak, że mam wewnętrzną warstwę biznesową i ona powinna wiedzieć jak najmniej o zewnętrznym świecie. Dlatego jak muszę odpytać jakiś zewnętrzny serwis (REST, SOAP, a może bezpośrednio bazę systemu legacy) to wtedy tworzę port i warstwy biznesowej nie obchodzi co jest pod spodem.
Ale to, że jakiś serwis biznesowy ma coś wstrzyknięte, to jeszcze nie znaczy, że wszystkie zależności mają być portami schowanymi za interfejsem. Jak w handlerze mam walidator to nie tworzę portu walidatora, bo to nie ma sensu. Jak mam pomocniczy serwis określający czy dany użytkownik może wykonać akcję to nie tworzę do niego portu, bo to znowu nie ma sensu.Przynajmniej tak ja to widzę ;)
Grzyboo napisał(a):
Też rozumiem to jak some_ONE wspomniał - porty tworzysz do zewnętrznych(pozaaplikacyjnych) usług (REST service, baza danych, operacje na filesystemie itp.), a nie do jakiś drobnych klas, które wydzieliłeś, żeby zachować SRP. A do tych zewnętrznych serwisów prawie zawsze i tak chcesz mieć wiele implementacji: do testów, do środowisk nie-produkcyjnych itp.
Tworzenie tony single-implementation interfejsów tylko dla zasady jest głupie i szkodliwe.
Shalom napisał(a):
Dla mnie generalnie rozbija się to o bardzo prosty koncept:
domain
bez zależności.
Domena nie wie nic o tym ze komunikujemy się z jakimiś zewnętrznymi systemami i nigdzie w domenie nie ma referencji do żadnej "zewnętrznej" klasy (oczywiście nie mówię tu o jakimś vavr czy innych libkach), bo w moim przypadku modułdomain
nie ma dependency na nic takiego.
To automatycznie wymusza, żeby wszystkie "zewnętrze" zależności były owrapowane jakimś "naszym" modułem (adapterem), który spina naszą domenę z tym zewnętrznym serwisem a sam jest wystawiony w domenie jako jakiś nasz interfejs (port).
Na podstawie tych postów, zrobiłem refaktor pakietu registration
na coś takiego, tworząc port i adapter dla zewnętrznej usługi - bazy danych:
registration
├── adapter
│ └── registrationdb
│ ├── RoleRepositoryAdapter
│ └── UserRepositoryAdapter
├── application
│ ├── controller
│ │ └── RegistrationController
│ └── dto
│ └── RegisterRequest
├── domain
│ ├── exception
│ │ ├── CredentialValidationException
│ │ └── RoleNotFoundException
│ ├── model
│ │ ├── Role
│ │ ├── User
│ │ └── UserRole
│ ├── ports
│ │ ├── RoleRepository
│ │ └── UserRepository
│ └── service
│ ├── EmailExistenceValidationStrategy
│ ├── EmailFormatValidationStrategy
│ ├── PasswordEncoderService
│ ├── PasswordFormatValidationStrategy
│ ├── RegistrationValidation
│ ├── RegistrationValidationStrategy
│ ├── UserFactory
│ ├── UsernameExistenceValidationStrategy
│ ├── UsernameValidationStrategy
│ ├── UserRegistration
│ ├── UserRegistrationService
│
- W tym rozwiązaniu część klas będzie musiała być publiczna, z uwagi na to, że w Javie prywatne klasy są dostępne tylko w obrębie swojego pakietu. Czyli exception, dto itp. - klasy publiczne.
Podział odpowiedzialności port-adapter wygląda tak:
port
public interface UserRepositoryPort {
int save(User user);
void assignRoleToUser(int userId, int roleId);
}
adapter
class UserRepositoryAdapter implements UserRepositoryPort {
private final UserDAO userDAO;
public UserRepositoryPortAdapter(UserDAO userDAO) {
this.userDAO = userDAO;
}
@Override
public int save(User user) {
return userDAO.save(user);
}
@Override
public void assignRoleToUser(int userId, int roleId) {
userDAO.assignRoleToUser(userId, roleId);
}
}
dao
@Repository
public interface UserDAO {
@SqlUpdate("INSERT INTO users (username, email, password) VALUES (:username, :email, :password)")
@GetGeneratedKeys
int save(@BindBean User user);
@SqlUpdate("INSERT INTO user_roles (user_id, role_id) VALUES (:userId, :roleId)")
void assignRoleToUser(@Bind("userId") int userId, @Bind("roleId") int roleId);
}
Czy może tak być?
-
Encji
User
używam w funkcjonalności rejestracji - pakietregistration
. Co jeśli tej samej encji będę musiał użyć w nowej funkcjonalności, przykładowologin feature
. Powinienem stworzyć jakiś wspólny pakiet typucommon
i tam ją przechowywać? -
Czy mógłbym zastosować jakieś lepsze podejście do podziału na pakiety?
Dzięki za pomoc