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

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

Port:

Kopiuj
public interface WeatherDataFetcher {
    WeatherDto fetchWeatherData(String locationName) throws IOException;
}

Adapter:

Kopiuj
@Service
class WeatherDataClient implements WeatherDataFetcher {

    @Override
    public WeatherDto fetchWeatherData(String locationName) throws IOException {
        // implementacja np. jakieś WeatherAPI
    }
}

Domena:

Kopiuj
class UserWeatherDataHandler {
   private final WeatherDataFetcher weatherDataFetcher;
   private final UserWeatherDataSaver userWeatherDataSaver;

    UserWeatherDataHandler(WeatherDataFetcher weatherDataFetcher,
                           UserWeatherDataSaver userWeatherDataSaver) {
        this.weatherDataFetcher = weatherDataFetcher;
        this.userWeatherDataSaver = userWeatherDataSaver;
    }

    public void fetchAndSaveWeather(int userId, String locationName) {
        WeatherDto weather = weatherDataFetcher.fetchWeather("Warszawa");
        userWeatherDataSaver.saveWeatherToUser(weather, userId);
    }
}

Klasa która zawiera szczegóły techniczne - i tutaj odbywa się całą magia, czyli przypisanie "pogody" użytkownikowi:

Kopiuj
class UserWeatherDataSaver {
}
Riddle napisał(a):

Jedyne do czego bym się doczepił, to to że ta Twoja klasa UserWeatherDataHandler nie ma logiki żadnej. Sens istnienia tej klasy UserWeatherDataHandler jest trochę... wątpliwy. No ale technicznie może być.

Tak się zastanawiam. UserWeatherDataHandler to domena. Zasugerowałeś, że ta klasa powinna zostać usunięta, czego do końca nie rozumiem. Bo usuwając UserWeatherDataHandler, usunę domenę. Co w takim razie będzie domeną po usunięciu tej klasy?

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

Tak się zastanawiam. UserWeatherDataHandler to domena. Zasugerowałeś, że ta klasa powinna zostać usunięta, czego do końca nie rozumiem. Bo usuwając UserWeatherDataHandler, usunę domenę. Co w takim razie będzie domeną po usunięciu tej klasy?

W domenie powinna być logika biznesowa Twojej aplikacji. Ale w Twoim przypadku (a przynajmniej w tym kawałku kodu który pokazałeś) nie ma logiki.

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

Tak się zastanawiam. UserWeatherDataHandler to domena. Zasugerowałeś, że ta klasa powinna zostać usunięta, czego do końca nie rozumiem. Bo usuwając UserWeatherDataHandler, usunę domenę. Co w takim razie będzie domeną po usunięciu tej klasy?

W domenie powinna być logika biznesowa Twojej aplikacji. Ale w Twoim przypadku (a przynajmniej w tym kawałku kodu który pokazałeś) nie ma logiki.

To byłoby ok, gdybym stworzył kontroler i dodał te zależności bezpośrednio w kontrolerze?
-WeatherDataFetcher - port,
-UserWeatherDataSaver - odpowiada za przypisanie pogody użytkownikowi

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

To byłoby ok, gdybym stworzył kontroler i dodał te zależności bezpośrednio w kontrolerze?
-WeatherDataFetcher - port,
-UserWeatherDataSaver - odpowiada za przypisanie pogody użytkownikowi

No chyba tak.

Tylko jeśli dojdzie dodatkowa logika, jakieś ify, jakaś walidacja, jakieś dodatkowe operacje, jakieś powiadomienie - to wtedy to musisz wynieść z powrotem do domeny.

RequiredNickname
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 650
2

Twój controller to adapter wejściowy, orkiestrację możesz załatwić w warstwie application (serwisy aplikacyjne, use case'y) która z kolei może wołać min. serwisy domenowe czy też inne adaptery.

Update:
Czytając sobie pobieżnie wątek i samemu odpowiadając odnośnie oriekstracji procesu w warstwie aplikacyjnej naszło mnie pytanie na które być może @Riddle będziesz potrafił odpowiedzieć lub nakierować.

Przyjmijmy, że chcemy, w warstwie application, napisać use case "add new user".
Use case ten jest odpowiedzialny za orkeistrację tego procesu a w ramach niego potrzebujemy:

  1. dodać nowego uzytkownika do systemu (finalnie do db)
  2. założyć mu jakąś czystą kartotekę (kolejna tabelka w db)
  3. naliczyć N gratisowych punktów w programie lojalnościowym (strzał do zewnętrznego serwisu)
  4. wyemitować event (np. na jakiś topic kafkowy)o tym, że nowy użytkownik został zarejestrowany (bo np. inny system na tej podstawie wyśle mu powitalny email)

Zastanawia mnie kwestia transakcyjności (w rozumieniu bazodanowym) tego procesu.
Punkt 3 i 4 nie dotyczy stricte bazy danych więc na upartego możemy to obsłużyć za pomocą np. outboxa (ale akurat nie tego dotyczy moje pytanie więc nie musimy się na tym skupiać). Za to punkt 1 i 2 to już z grubsza operacje typu CRUD (pomijamy też ich prostotę, zakłądamy że system jest skomplikowany i architektura została dobrana poprawnie) które spina (powinna spinać?) transakcja bazodanowa (stosując outbox dla punktu 3 i 4 naturalnie wszystkie te rzeczy zepniemy w ramach jednej transakcji).

Pytanie:
Na jakim poziomie powinna znajdować się deklaracja transakcji?

  1. Moglibyśmy użyć springowej @Transactional na tym naszym serwisie aplikacyjnym enkapsulujacym proces dodawania użytkownika ale to się gryzie z zasadą, że w warstwie aplikacyjnej nie powinno być zależności do frameworka.
  2. Moglibyśmy @Transactional umieścić na poziomie controllera ale atomowość tych operacji jest nie tylko wymaganiem technicznym ale też biznesowym więc mimo wszystko chcielibyśmy to mieć zagwarantowane na poziomie procesu a nie martwić się, czy wcześniej ktoś poprawnie dodał techniczną adnotację.

Jest jakiś pattern aby na poziomie use case zagwarantować transakcyjność w czasie wołania N różnych serwisów (które skutkują dodaniem rekordów do różnych tabel w db) ale by jednocześnie nie pchać np. @Transactional do tego serwisu aplikacyjnego?

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

Pytanie:
Na jakim poziomie powinna znajdować się deklaracja transakcji?

Samo otworzenie transakcji z pewnością powinno być w warstwie bazodanowej (begin, commit, ewentualny rollback); i jeśli da się to zrobić bez udziału domeny, to super. Wszelkie calle do driverów bazodanowych i ORM na pewno powinny być w warstwie bazodanowej.

Ale jeśli jest use-case w którym to domena ma np zdecydować czy w wyniku błędu zrobić commit czy rollback, to należy na to nałożyć odpowiednią abstrakcję, która nie zna żadnych szczegółów bazodanowych.

Taka abstrakcja mogłaby wyglądać

Kopiuj
interface Persistance {
  void open();

  void addUser(User user);

  void addKartoteka(Kartoteka kartoteka);

  void close();

  void reset();
}

I te metody open() i close() mogłyby zaczynać transkację. Oczywiście ten interfejs powinien być głównie zrobiony pod domenę - zależnie od use-case'u. Mógłbyś nazwać te metody np. beforeAll() (która robi "being transaction") oraz afterAll() (która robi commit), albo jakoś inaczej. To też nie musi być jeden interfejs, możesz to zaprojektować tak, żeby w domenie tego się fajnie używało. Możesz nawet zrobić coś takiego jak:

Kopiuj
interface Persistance {
  void inPersistanceScope(Consumer<?> block);
}

i używać tego tak:

Kopiuj
Persistance persistance = ; // weź to skądś
persistance.inPersistanceScope(db => {
  db.addUser(user);
  db.addKartoteka(kartoteka);
});

Po prostu trzeba dobrać odpowiedni design pod use-case. Interfejs domenowy nie musi być wcale podobny do interfejsu bazodanowego.

RequiredNickname napisał(a):

Jest jakiś pattern aby na poziomie use case zagwarantować transakcyjność w czasie wołania N różnych serwisów (które skutkują dodaniem rekordów do różnych tabel w db) ale by jednocześnie nie pchać np. @Transactional do tego serwisu aplikacyjnego?

Wszystko się sprowadza do odpowiedniej umiejętności projektowania oprogramowania.

Jeśli chcesz taki bardzo basic example, to napisz sobie interfejs, i przekaż implementacje która np zapisuje dane do pliku zamiast do bazy. Czy możesz zamienić implementację z plików na db bez zmian w interfejsie? Jeśli tak, to zrobiłeś dobrą abstrakcję.

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

Zrobiłem refaktor feature'a registration. Między innymi wydzieliłem szczegóły techniczne z domeny, takie jak zapis użytkownika i jego ról, do osobnej klasy. Czy teraz domena jest dobrze odseparowana?

Komentarze w kodzie na potrzeby tego postu.

Domena:

Kopiuj
class UserRegistration {

    private final PasswordEncoder passwordEncoder; //port
    private final UserSaver userSaver; //port
    private final RoleRepository roleRepository; //port

    UserRegistration(PasswordEncoder passwordEncoder,
                     UserSaver userSaver,
                     RoleRepository roleRepository) {
        this.passwordEncoder = passwordEncoder;
        this.userSaver = userSaver;
        this.roleRepository = roleRepository;
    }

    ApiResponse registerUser(RegisterRequest request) {
        User user = createUser(request);
        userSaver.saveUser(user);
        return new ApiResponse("User registered successfully.");
    }

    private User createUser(RegisterRequest request) {
        User user = new User();
        user.setUsername(request.username());
        user.setEmail(request.email());
        user.setPassword(passwordEncoder.encode(request.password()));
        user.setRoles(Collections.singleton(fetchDefaultUserRole()));
        return user;
    }

    private Role fetchDefaultUserRole() {
        return roleRepository.findByName(UserRole.ROLE_USER).orElseThrow(
                () -> new RoleNotFoundException("Role not found"));
    }
}

Port

Kopiuj
public interface UserSaver {
    void saveUser(User user);
}

Adapter

Kopiuj
@Service
class TransactionUserSaver implements UserSaver {

    private final UserDAO userDAO;
    private final TransactionTemplate transactionTemplate;

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

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

            for (Role roles : user.getRoles()) {
                userDAO.assignRoleToUser(userId, roles.getId());
            }
        });
    }
}
Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
1

Noi moim zdaniem jest git.

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

Zastanawiam się, jak dobrze zaimplementować klasę, która będzie używana w kilku różnych funkcjonalnościach.

Przykładowo mam:

  • funkcjonalność a (pakiet a),
  • funkcjonalność b (pakiet b),
  • funkcjonalność c (pakiet c)

We wszystkich tych funkcjonalnościach potrzebuję wygenerować token. W tym celu tworzę klasę SecureTokenGenerator, nowy pakiet common -> utils i umieszczam tę klasę w tym pakiecie.

Kopiuj
class SecureTokenGenerator {

    private final SecureRandom secureRandom = new SecureRandom();

    private final Base64.Encoder encoder = Base64.getUrlEncoder();

    String generateToken() {
        byte[] randomBytes = new byte[24];
        secureRandom.nextBytes(randomBytes);
        return encoder.encodeToString(randomBytes);
    }
}

Teraz chciałbym dodać generowanie tokenu do tych moich funkcjonalności. Ale myślę, jak to dobrze zrobić?

Zrobiłem tak, w pakiecie common -> utils dodałem interface TokenGenerator:

Kopiuj
public interface TokenGenerator {
    String generateToken();
}

I zrobiłem tak, aby SecureTokenGenerator implementował ten interface:

Kopiuj
class SecureTokenGenerator implements TokenGenerator {

    private final SecureRandom secureRandom = new SecureRandom();

    private final Base64.Encoder encoder = Base64.getUrlEncoder();

    @Override
    public String generateToken() {
        byte[] randomBytes = new byte[24];
        secureRandom.nextBytes(randomBytes);
        return encoder.encodeToString(randomBytes);
    }
}

Teraz przechodzę do moich funkcjonalności i przykładowo w funkcjonalności a tworzę taki port:

Kopiuj
public interface SessionTokenGenerator extends TokenGenerator {
     String generateToken();
}

Następnie korzystam z tego portu w domenie. W kolejnej funkcjonalności tworzę port, np. PasswordResetTokenGenerator, i tak dalej...

Czy moje rozwiązanie jest dobre?

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

Moim zdaniem trochę średnio dobre.

  1. Nie wsadzaj tego do żadnego common.utils, tylko wsadź to po prostu do domeny. Możesz nazwać ten pakiet po prostu token. Robienie pakietów common albo utils to jest dobra droga do zrobienia pakietów "śmieciowych" do których się wrzucają klasy bez designu.
  2. Nie widzę powodu żeby robić osobny interfejs na ten element, chyba że potrzebujesz go parametryzować lub wstrzyknąć w testach (tylko w sumie nawet wtedy nie potrzebujesz interfejsu, tylko możesz zrobić fejk w testach).

Tak na prawdę patrząc na samą klasę SecureTokenGenerator ciężko powiedzieć czy jest dobrze zaprojektowana - musiałbyś pokazać miejsca jej użycia.

RequiredNickname
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 650
1

Tak jak Riddle pisze nie rób żadnych commonsów bo po utworzeniu commonsów trzeba mieć dużą dyscyplinę by tam nie pakować wszystkiego co ma kiepski design zamiast napisać porządnie.
Wrzuć to sobie gdzieś biżej domeny (w ostateczności jako port & adapter).

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

Tak na prawdę patrząc na samą klasę SecureTokenGenerator ciężko powiedzieć czy jest dobrze zaprojektowana - musiałbyś pokazać miejsca jej użycia.

Domena logowania - przed tworzeniem sesji generuję token sesji.

Kopiuj
class LoginHandler {

    private final UserSessionCreator userSessionCreator;
    private final UserRepository userRepository; //port
    private final Authentication userAuth; //port
    private final TokenHasher tokenHasher;
    private final SecureTokenGenerator tokenGenerator;

   LoginHandler(SecureTokenGenerator tokenGenerator,
                UserSessionCreator userSessionCreator,
                UserRepository userRepository,
                Authentication userAuth,
                TokenHasher tokenHasher) {
        this.tokenGenerator = tokenGenerator;
        this.userSessionCreator = userSessionCreator;
        this.userRepository = userRepository;
        this.userAuth = userAuth;
        this.tokenHasher = tokenHasher;
   }

    public String login(LoginRequest request) {
       String username = userAuth.authenticateUser(request.usernameOrEmail(), request.password());
       User user = userRepository.findByUsername(username).orElseThrow(
                () -> new UserNotFoundException("User not found with username :" + username));

        String sessionToken = tokenGenerator.generateToken();
        String hashedToken = tokenHasher.hashToken(sessionToken);
        userSessionCreator.createUserSession(hashedToken, user.getId());
        return sessionToken;
    }
}

Planuję dodać funkcjonalność resetowania hasła. Generuję token do linku resetującego.
Mam szkic domeny, która odpowiada za wysyłanie emaila:

Kopiuj
class PasswordResetEmailSender {

    private final SecureTokenGenerator tokenGenerator;
    private final EmailSender emailSender; //port
    private final PasswordResetTokenRepository tokenRepository; //port

    public PasswordResetEmailSender(SecureTokenGenerator tokenGenerator,
                                    EmailSender emailSender,
                                    PasswordResetTokenRepository tokenRepository) {
        this.tokenGenerator = tokenGenerator;
        this.emailSender = emailSender;
        this.tokenRepository = tokenRepository;
    }

    void sendPaswordResetEmail(String email) {
        String token = tokenGenerator.generateToken();
        LocalDateTime expiryDate = LocalDateTime.now().plusHours(1);

        PasswordResetToken passwordResetToken = new PasswordResetToken(token, expiryDate, email);

        tokenRepository.save(passwordResetToken);

        String resetLink = "http://skysong.com/reset-password?token=" + token;
        String emailContent = "Click the link to reset your password: " + resetLink;


        emailSender.sendEmail(email, "Reset password instructions", emailContent);
    }
}
Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
0
Ornstein napisał(a):
Riddle napisał(a):

Tak na prawdę patrząc na samą klasę SecureTokenGenerator ciężko powiedzieć czy jest dobrze zaprojektowana - musiałbyś pokazać miejsca jej użycia.

Domena logowania - przed tworzeniem sesji generuję token sesji.

No to żeby ocenić czy to ma sens ten kod, musiałbyś pokazać jak jest użyty ten LoginHandler.

Ornstein napisał(a):

Planuję dodać funkcjonalność resetowania hasła.
Mam szkic domeny, która odpowiada za wysyłanie emaila:

Ten PasswordResetToken mam nadzieję że to nie żaden model JPA?

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

Tak na prawdę patrząc na samą klasę SecureTokenGenerator ciężko powiedzieć czy jest dobrze zaprojektowana - musiałbyś pokazać miejsca jej użycia.

Domena logowania - przed tworzeniem sesji generuję token sesji.

No to żeby ocenić czy to ma sens ten kod, musiałbyś pokazać jak jest użyty ten LoginHandler.

Kopiuj
@RestController
@RequestMapping("/api/v1/users")
public class LoginController {

    private final LoginHandler login;

    public LoginController(LoginHandler login) {
        this.login = login;
    }

    @PostMapping("/login")
    public ResponseEntity<ApiResponse> login(@Valid @RequestBody LoginRequest request,
                                             HttpServletResponse response) {
        String sessionToken = login.login(request);

        Cookie sessionCookie = new Cookie("session_id", sessionToken);
        sessionCookie.setHttpOnly(true);
        sessionCookie.setPath("/");
        response.addCookie(sessionCookie);

        return ResponseEntity.ok(new ApiResponse("Logged successfully."));
    }
}
Riddle napisał(a):
Ornstein napisał(a):

Planuję dodać funkcjonalność resetowania hasła.
Mam szkic domeny, która odpowiada za wysyłanie emaila:

Ten PasswordResetToken mam nadzieję że to nie żaden model JPA?

To jest czysty model, bez żadnych adnotacji JPA.

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

@Ornstein Czyli Twój sessionToken jest wysyłany do klienta. To w takim razie nie rozumiem czemu w LoginHandler jest to:

Kopiuj
String hashedToken = tokenHasher.hashToken(sessionToken);
userSessionCreator.createUserSession(hashedToken, user.getId());

Jaki jest sens zapisywać zahashowany token użytkownika w sesji? Czemu nie zapisać nie zahashowanego?

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

@Ornstein Czyli Twój sessionToken jest wysyłany do klienta. To w takim razie nie rozumiem czemu w LoginHandler jest to:

Kopiuj
String hashedToken = tokenHasher.hashToken(sessionToken);
userSessionCreator.createUserSession(hashedToken, user.getId());

Jaki jest sens zapisywać zahashowany token użytkownika w sesji? Czemu nie zapisać nie zahashowanego?

Gdzieś przeczytałem, że w bazie danych powinien ten token być zahashowany.

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

Gdzieś przeczytałem, że w bazie danych powinien ten token być zahashowany.

🤨

RequiredNickname
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 650
1
Ornstein napisał(a):
Riddle napisał(a):

@Ornstein Czyli Twój sessionToken jest wysyłany do klienta. To w takim razie nie rozumiem czemu w LoginHandler jest to:

Kopiuj
String hashedToken = tokenHasher.hashToken(sessionToken);
userSessionCreator.createUserSession(hashedToken, user.getId());

Jaki jest sens zapisywać zahashowany token użytkownika w sesji? Czemu nie zapisać nie zahashowanego?

Gdzieś przeczytałem, że w bazie danych powinien ten token być zahashowany.

Wcale nie musisz trzymać w db tokenu o ile nie potrzebujesz go inwalidować (ale możesz ustawiać odpowiednio krótki czas życia i umozliwiaćzdobycie refresh tokenu).
Możesz przy logowaniu zwracać token zabezpieczony swoim podpisem i później weryfikować ten podpis.
Temat jest bardzo ciekawy więc rzuć sobie okiem:

https://devszczepaniak.pl/json-web-token/
https://sekurak.pl/jwt-security-ebook.pdf

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

@Ornstein Czyli Twój sessionToken jest wysyłany do klienta. To w takim razie nie rozumiem czemu w LoginHandler jest to:

Kopiuj
String hashedToken = tokenHasher.hashToken(sessionToken);
userSessionCreator.createUserSession(hashedToken, user.getId());

Jaki jest sens zapisywać zahashowany token użytkownika w sesji? Czemu nie zapisać nie zahashowanego?

Gdzieś przeczytałem, że w bazie danych powinien ten token być zahashowany.

Wcale nie musisz trzymać w db tokenu o ile nie potrzebujesz go inwalidować (ale możesz ustawiać odpowiednio krótki czas życia i umozliwiaćzdobycie refresh tokenu).
Możesz przy logowaniu zwracać token zabezpieczony swoim podpisem i później weryfikować ten podpis.
Temat jest bardzo ciekawy więc rzuć sobie okiem:

https://devszczepaniak.pl/json-web-token/
https://sekurak.pl/jwt-security-ebook.pdf

Trochę się bawiłem tym JWT - fajna sprawa, szczególnie że nie trzeba za każdym razem komunikować się z bazą danych. W następnym projekcie na pewno z tego skorzystam, w tym już chyba zostawię tak jak jest.

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

Mam takie DAO - UserDAO. Następnie mam kilka funkcjonalności, powiedzmy login, registration, weather. W każdej funkcjonalności chcę stworzyć port (interface) do UserDAO. Myślałem, żeby UserDAO rozszerzała te interfejsy (porty). Tylko tutaj pojawia się problem z nazwami dla tych interfejsów (portów). Nie mogę ich nazwać w każdej funkcjonalności tak samo, bo będzie to wyglądało tak:

Kopiuj
public interface UserDAO extends UserRepository, UserRepository, UserRepository 

Pomyślałem, żeby może dodać przedrostek funkcjonalności do każdego portu:

Kopiuj
public interface UserDAO extends LoginUserRepository, RegistrationUserRepository, WeatherUserRepository 

Drugie podejście o którym myślałem to zrobienie czegoś takiego:
DAO - ogólne dla wszystkich funkcjonalności

Kopiuj
@Repository
public interface UserDAO {
    @SqlQuery("SELECT EXISTS (SELECT 1 FROM users WHERE username = :username)")
    boolean existsByUsername(@Bind("username") String username);

    @SqlQuery("SELECT EXISTS (SELECT 1 FROM users WHERE email = :email")
    boolean existsByEmail(@Bind("email") String email);

  //reszta kodu
}

Port:

Kopiuj
public interface UserRepository {
    boolean existsByUsername(String username);
    boolean existsByEmail( String email);
}

Adapter:

Kopiuj
@Service
class UserRepositoryImpl implements UserRepository {

    private final UserDAO userDAO;

    UserRepositoryImpl(UserDAO userDAO) {
        this.userDAO = userDAO;
    }

    @Override
    public boolean existsByUsername(String username) {
        return userDAO.existsByUsername(username);
    }

    @Override
    public boolean existsByEmail(String email) {
        return userDAO.existsByEmail(email);
    }
}

Co myślicie o tych dwóch propozycjach?

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

Mam takie DAO - UserDAO. Następnie mam kilka funkcjonalności, powiedzmy login, registration, weather. W każdej funkcjonalności chcę stworzyć port (interface) do UserDAO. Myślałem, żeby UserDAO rozszerzała te interfejsy (porty). Tylko tutaj pojawia się problem z nazwami dla tych interfejsów (portów). Nie mogę ich nazwać w każdej funkcjonalności tak samo, bo będzie to wyglądało tak:

Kopiuj
public interface UserDAO extends UserRepository, UserRepository, UserRepository 

Pomyślałem, żeby może dodać przedrostek funkcjonalności do każdego portu:
[...]
Co myślicie o tych dwóch propozycjach?

Żadna z nich nie ma sensu. Myślę że robisz przerost formy nad treścią.

Moim zdaniem każda z tych funkcjonalności: login, register, weather powinna korzystać z tego samego interfejsu - jaki sens to rozdzielać?

Kopiuj
public interface UserRepository {
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}

class Login {
  private UserRepository users;
  Login(UserRepository users) {
    this.users = users;
  }
}

class Register {
  private UserRepository users;
  Register(UserRepository users) {
    this.users = users;
  }
}

class Weather {
  private UserRepository users;
  Weather(UserRepository users) {
    this.users = users;
  }
}
Ornstein
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 121
0
Riddle napisał(a):

Moim zdaniem każda z tych funkcjonalności: login, register, weather powinna korzystać z tego samego interfejsu - jaki sens to rozdzielać?

Chciałem zrobić coś takiego, aby każda funkcjonalność była niezależna i odizolowana od implementacji, w tym przypadku od DAO. Na przykład, jeśli usunę port w jednej funkcjonalności, nie powinno to naruszać drugiej, port wykorzystuję tylko w obrębie funkcjonalności.

Riddle napisał(a):
Kopiuj
public interface UserRepository {
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}

class Login {
  private UserRepository users;
  Login(UserRepository users) {
    this.users = users;
  }
}

class Register {
  private UserRepository users;
  Register(UserRepository users) {
    this.users = users;
  }
}

class Weather {
  private UserRepository users;
  Weather(UserRepository users) {
    this.users = users;
  }
}

UserRepository miałbym umieścić gdzieś w common pakiecie?

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

Moim zdaniem każda z tych funkcjonalności: login, register, weather powinna korzystać z tego samego interfejsu - jaki sens to rozdzielać?

Chciałem zrobić coś takiego, aby każda funkcjonalność była niezależna i odizolowana od implementacji, w tym przypadku od DAO. Na przykład, jeśli usunę port w jednej funkcjonalności, nie powinno to naruszać drugiej, port wykorzystuję tylko w obrębie funkcjonalności.

😐

Kombinujesz za bardzo. Samo to że używasz interfejsu UserRepository a nie repo z JPA już jest wystarczającym odizolowaniem.

Jeśli chcesz dodać funkcję do UserRepository to po prostu dodaj - Twoje trzy klasy o tym nie będą wiedzieć.
Jeśli przestajesz używać jakiejś funkcji w UserRepository, ale inne klasy nadal jej używają to po prostu zostaw.
Jeśli przestajesz używać jakiejś funkcji w UserRepository oraz żadna inna klasa jej nie używa, wtedy możesz bez obaw usunąć.

Nie widzę powodu na takie kombinowanie z wieloma różnymi implementacjami UserDao. Po co niby miałbyś to robić? Nic Ci to nie da.

Ornstein napisał(a):

Chciałem zrobić coś takiego, aby każda funkcjonalność była niezależna i odizolowana od implementacji, w tym przypadku od DAO.

Robiąc to w ten sposób jak opisałeś, miałbyś praktycznie 0 code reuse; pisanie takiej aplikacji zajęłoby 100x więcej czasu niż powinno.

Moim zdaniem totalnie nie worth.

Ornstein napisał(a):

UserRepository miałbym umieścić gdzieś w common pakiecie?

Ale po co chcesz na siłę rozłączyć te trzy elementy? Na prawdę to są różne moduły? Bo mi totalnie wyglądają jak jeden.

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

Ale po co chcesz na siłę rozłączyć te trzy elementy? Na prawdę to są różne moduły? Bo mi totalnie wyglądają jak jeden.

Tak, to osobne moduły.

  • login,
  • registration,
  • location - na podstawie adresu usera pobieram współrzędne lokalizacji z API,
  • weather - na podstawie współrzędnych usera pobieram i przypisuję do niego dane o pogodzie
    ...

Czytałem trochę o package by feature i tak sobie to podzieliłem.

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

Ale po co chcesz na siłę rozłączyć te trzy elementy? Na prawdę to są różne moduły? Bo mi totalnie wyglądają jak jeden.

Tak są to osobne moduły.

  • login,
  • registration,
  • location - na podstawie adresu usera pobieram współrzędne lokalizacji z API,
  • weather - na podstawie współrzędnych usera pobieram i przypisuję do niego dane o pogodzie

No to moim zdaniem masz dwa wyjścia:

  • Albo trzymaj je wszystkie w jednym pakiecie, i niech korzystają wszystkie z jednego interfejsu UserRepository.
  • A jeśli koniecznie chcesz je rozdzielić, to tylko jeden z tych modułów powinien korzytać z UserRepository, a pozostałe powinny dostać potrzebne dane w innej formie - np. location mógłby dostać sam tylko String address (a nie user), a "weather" mógłby dostać same tylko double lat, double long.
Ornstein napisał(a):

Czytałem trochę o package by feature i tak sobie to podzieliłem.

Pokaż link/linki.

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

https://medium.com/expedia-group-tech/package-by-feature-not-by-layer-5ba04a070003
https://labs.madisoft.it/folder-structure-for-big-projects-package-by-type-layer-or-feature/

Oba te źródła mówią o tym żeby nie grupować kodu warstwami, czyli coś czego w Twoim kodzie i tak nie ma. Nie przejmuj się na razie tym "package by feature". Te trzy klasy, login, register, weather, location; na pewno tak długo jak są pojedynczą klasą moga być w jednym module bez problemu.

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

Odizolowałem domenę od zewnętrznych zależności. I tak oto mam przykładowo feature login i kilka pytań.

Kopiuj
class LoginHandler {
    private final UserSessionCreator sessionCreator;
    private final UserAuthentication auth; //port
    private final LoginUserRepository userRepository; //port

    LoginHandler(UserSessionCreator sessionCreator,
                 UserAuthentication auth,
                 LoginUserRepository userRepository) {
        this.sessionCreator = sessionCreator;
        this.auth = auth;
        this.userRepository = userRepository;
    }

    public String login(LoginRequest request) {
       String username = auth.authenticateUser(request.usernameOrEmail(), request.password());
       User user = userRepository.findByUsername(username).orElseThrow(
                () -> new UserNotFoundException("User not found with username :" + username));

       String sessionToken = sessionCreator.createUserSession(user.getId());
       return sessionToken;
    }
}

Kopiuj

class SessionTokenGenerator {

    private final SecureRandom secureRandom = new SecureRandom();

    private final Base64.Encoder encoder = Base64.getUrlEncoder();

    String generateToken() {
        byte[] randomBytes = new byte[24];
        secureRandom.nextBytes(randomBytes);
        return encoder.encodeToString(randomBytes);
    }
}
Kopiuj
class UserSessionCreator {
    private final LoginSessionRepository sessionRepository; //port
    private final SessionTokenGenerator tokenGenerator;

    UserSessionCreator(LoginSessionRepository sessionRepository,
                       SessionTokenGenerator tokenGenerator) {
        this.sessionRepository = sessionRepository;
        this.tokenGenerator = tokenGenerator;
    }

    public String createUserSession(int userId) {
        String token = tokenGenerator.generateToken();
        Session session = createSession(token, userId);
        sessionRepository.save(session);
        return token;
    }

    private Session createSession(String token, Integer userId) {
        Session session = new Session();
        session.setSessionId(token);
        session.setUserId(userId);
        session.setCreateAt(new Date());
        session.setExpiresAt(new Date(System.currentTimeMillis() + (24 * 60 * 60 * 1000)));

        return session;
    }
}
  1. Wszystkie klasy domenowe są prywatne. Zastanawiam się, w jaki sposób wywołać logikę w kontrolerze. Pomyślałem, żeby stworzyć publiczną klasę z adnotacją @Service jako łącznik.
Kopiuj
@Service
public class LoginHandlerService {

    private final LoginHandler login;

    public LoginHandlerService(LoginHandler login) {
        this.login = login;
    }

    public String userLogin(LoginRequest request) {
        return login.login(request);
    }
}

I stworzyć klasę konfiguracyjną z portami w parametrach:

Kopiuj
@Configuration
class LoginConfiguration {

    @Bean
    public LoginHandler loginHandler(LoginSessionRepository sessionRepository,
                                     UserAuthentication auth,
                                     LoginUserRepository userRepository) {
        SessionTokenGenerator tokenGenerator = new SessionTokenGenerator();
        UserSessionCreator sessionCreator = new UserSessionCreator(sessionRepository, tokenGenerator);
        return new LoginHandler(sessionCreator, auth, userRepository);
    }
}

Następnie tę klasę z adnotacją @Service wstrzyknąłbym do kontrolera. Czy takie podejście będzie dobre?

  1. Przymierzam się do napisania testów. Czy wszystkie prywatne klasy z domeny powinny być pokryte testami?
  • LoginHandler
  • SessionTokenGenerator
  • UserSessionCreator
  • LoginHandlerService - klasa łącznik z adnotacją @Service - tutaj nie wiem, czy jest sens dodawać jakieś testy, skoro nie zawiera żadnej logiki.

Mam taki port i adapter który odpowiada ze uwierzytelnienie użytkownika. Pod to też powinienem napisać testy? Jeśli tak to testować port czy bezpośrednio adapter?

Kopiuj
public interface UserAuthentication {
    String authenticateUser(String usernameOrEmail, String password);
}
Kopiuj
@Service
class SpringAuthentication implements UserAuthentication {

    private final AuthenticationManager authManager;

    SpringAuthentication(AuthenticationManager authManager) {
        this.authManager = authManager;
    }

    @Override
    public String authenticateUser(String usernameOrEmail, String password) {
        try {
            Authentication auth = authManager.authenticate(new UsernamePasswordAuthenticationToken(usernameOrEmail, password));
            SecurityContextHolder.getContext().setAuthentication(auth);
            return auth.getName();
        } catch (BadCredentialsException e) {
            throw new BadCredentialsException("    Login failed due to invalid credentials.");
        }
    }
}
Riddle
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 10227
1
Ornstein napisał(a):
  1. Wszystkie klasy domenowe są prywatne. Zastanawiam się, w jaki sposób wywołać logikę w kontrolerze.

Klasy domenowe nie powinny być prywatne. Pozostałe elementy Twojej aplikacji (widok, http, etc.) mogą wiedzieć o domenie. W całym zabiegu chodziło o to żeby domena nie wiedziała o widoku i http; ale w drugą stronę to jest okej. Nie musisz tego na siłę oddzielać.

Chcesz żeby domena była niezależna od http i widoku; ale raczej nie chcesz żeby http i widok były niezależne od domeny.

Ornstein napisał(a):
  1. Pomyślałem, żeby stworzyć publiczną klasę z adnotacją @Service jako łącznik.

Wszystko jedno.

Zrób najprościej jak się da, bez kombinowania.

Ornstein
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 121
0
Riddle napisał(a):
Ornstein napisał(a):
  1. Wszystkie klasy domenowe są prywatne. Zastanawiam się, w jaki sposób wywołać logikę w kontrolerze.

Klasy domenowe nie powinny być prywatne. Pozostałe elementy Twojej aplikacji (widok, http, etc.) mogą wiedzieć o domenie. W całym zabiegu chodziło o to żeby domena nie wiedziała o widoku i http; ale w drugą stronę to jest okej. Nie musisz tego na siłę oddzielać.

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.

Chociaż takie podejście jest trochę problematyczne.
No bo mam takie pakiety:
registration -> domain -> service

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.

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.