Wątek przeniesiony 2023-09-24 21:04 z Java przez Riddle.

Upload pliku do S3 w DDD

0

Cześć, mam problem z rozwiązaniem pewnego dylematu. Ogólnie powiedzmy, że podczas dodania pliku trzeba go wysłać do jakiegoś tam storage np. s3. Czy agregat "Comment" powinien zawierać serwis, który wysyła plik w odpowiednie miejsce, czy jednak jest to po stronie serwisu?

Przykładowy kod:




  static class Storage {
    public void save(UUID id, InputStream file) {
    }
  }


  static class Comment {
    private UUID id;
    private String title;
    private List<File> files;

    //Transient
    private final Storage storage;

    void addFile(FileRequest file) {
      UUID id = UUID.randomUUID();
      files.add(new File(id, file.name));
      storage.save(id, file.fileContent);
    }
  }


  @AllArgsConstructor(access = AccessLevel.PROTECTED)
  static class File {
    private UUID id;
    private String name;
  }

  interface CommentRepository {
    void save(Comment comment);

    Comment findById(UUID id);
  }

  record FileRequest(

      InputStream fileContent,
      String name
  ) {
  }

  @RequiredArgsConstructor
  static class CommentService {

    private final CommentRepository commentRepository;

    public void addFileToComment(UUID commentId, FileRequest request) {
      Comment comment = commentRepository.findById(commentId);
      comment.addFile(request);
      commentRepository.save(comment);
    }
  }
  

vs



  static class Storage {
    public void save(UUID id, InputStream file) {
    }
  }


  static class Comment {
    private UUID id;
    private String title;
    private List<File> files;
    
    public void addFile(UUID fileId, String name) {
      files.add(new File(fileId, name));
    }
  }


  @AllArgsConstructor(access = AccessLevel.PROTECTED)
  static class File {
    private UUID id;
    private String name;
  }

  interface CommentRepository {
    void save(Comment comment);

    Comment findById(UUID id);
  }

  record FileRequest(

      InputStream fileContent,
      String name
  ) {
  }

  @RequiredArgsConstructor
  static class CommentService {

    private final CommentRepository commentRepository;
    private final Storage storage;

    public void addFileToComment(UUID commentId, FileRequest request) {
      Comment comment = commentRepository.findById(commentId);
      UUID fileId = UUID.randomUUID();
      storage.save(fileId, request.fileContent);
      comment.addFile(fileId, request.name());
      commentRepository.save(comment);
    }
  }

A może powinien on przyjąć tę usługę jako parametr metody?


 static class Storage {
    public void save(UUID id, InputStream file) {
    }
  }


  static class Comment {
    private UUID id;
    private String title;
    private List<File> files;


    void addFile(FileRequest file, Storage storage) {
      UUID id = UUID.randomUUID();
      files.add(new File(id, file.name));
      storage.save(id, file.fileContent);
    }
  }


  @AllArgsConstructor(access = AccessLevel.PROTECTED)
  static class File {
    private UUID id;
    private String name;
  }

  interface CommentRepository {
    void save(Comment comment);

    Comment findById(UUID id);
  }

  record FileRequest(

      InputStream fileContent,
      String name
  ) {
  }

  @RequiredArgsConstructor
  static class CommentService {

    private final CommentRepository commentRepository;
    private final Storage storage;

    public void addFileToComment(UUID commentId, FileRequest request) {
      Comment comment = commentRepository.findById(commentId);
      comment.addFile(request, storage);
      commentRepository.save(comment);
    }
  }

4

Agregat ch..a powinien wiedzieć o metodzie persystencji. Od tego w DDD jest repozytorium. I nie, repozytorium to nie jest baza danych, repozytorium to warstwa persystencji i może to być baza, bucket s3, azure table storage czy plik xml.

Serwis jest tu ci w ogóle niepotrzebny

0

Dziękuję bardzo za odpowiedź.

W przypadku gdy wymagane jest wysłać e-maila po każdym dodaniu pliku, jak wtedy byś to wykonał?

Załóżmy, że mamy taki interfejs do wysłania e-maila:

  interface EmailSender {
    void invoke(String email, String content);
  }

Czy emailSender powinien się znaleźć w aggregacie czy w serwisie?

  static class Comment {
    private UUID id;
    private String title;
    private List<File> files;

    void addFile(FileRequest file) {
      UUID id = UUID.randomUUID();
      files.add(new File(id, file.name));
    }
  }

  @AllArgsConstructor(access = AccessLevel.PROTECTED)
  static class File {
    private UUID id;
    private String name;
  }

  interface CommentRepository {
    void save(Comment comment);
    
    Comment findById(UUID id);
  }

  record FileRequest(
      InputStream fileContent,
      String name
  ) {
  }

  @RequiredArgsConstructor
  static class CommentService {

    private final CommentRepository commentRepository;

    public void addFileToComment(UUID commentId, FileRequest request) {
      Comment comment = commentRepository.findById(commentId);
      comment.addFile(request);
      commentRepository.save(comment);
    }
  }

0
swirant5 napisał(a):

Dziękuję bardzo za odpowiedź.

W przypadku gdy wymagane jest wysłać e-maila po każdym dodaniu pliku, jak wtedy byś to wykonał?

Co to znaczy po każdym dodaniu? Skąd wynika to wymaganie i na którym poziomie powstaje?

Załóżmy, że wymaganie byłoby po to, żeby użytkownik wiedział, że plik się dodał, jak kliknie dodaj. Czyli dość taka wysokopoziomowa logika biznesowa.

W takiej sytuacji komentarz powinien mieć to w dupie, bo jest tylko komentarzem i nie musi wiedzieć nic o wysyłaniu mejli i podejmować decyzji o tym, czy należy to zrobić. Bo o tym decyduje już jakaś "wyższa instancja" w logice apki. Być może będzie to wspomniany serwis, być może jeszcze wyżej.

Czy agregat "Comment" powinien zawierać serwis, który wysyła plik w odpowiednie miejsce, czy jednak jest to po stronie serwisu?

Swoją drogą, czy komentarz w ogóle powinien być agregatem? Co on agreguje? Ogólnie strasznie mało informacji podajesz, a pytasz tak, jakby można było odpowiedzieć jednoznacznie na coś bez większego kontekstu.

0

Dąże do tego czy w agregacie jest możliwość umieszczenia jakiś „serwisow”, które umożliwiłyby jakieś zachowanie, właśnie w tym przypadku wysłanie wiadomość e-mail.

Nie chciałbym pozwolić aby inny programista, mógł omylnie wziąć daną metodę z agregatu i bezmyślnie jej użyć np. Bez wysyłania e-maila. Mógłbym wykonać to w zarówno w serwisie (modyfikator metody dodania pliku byłby package scope) albo „serwis” do wysyłania maili umieścić w agregacie i zamknąć to wszystko w metodzie.

Wiadomość jest agregatem, ponieważ agreguje wiadomosci i nie może pozwolić, aby dodac plik do już wysłanej wiadomości.

Mój przykład jest totalnie abstrakcyjny i jest wytworzony tylko w celach rozważań.

1

Dąże do tego czy w agregacie jest możliwość umieszczenia jakiś „serwisow”, które umożliwiłyby jakieś zachowanie, właśnie w tym przypadku wysłanie wiadomość e-mail.

Nie. Agregat nie powinien mieć takich zależności. Jeśli masz logikę biznesową, która wymaga takich zależności, np przed wykonaniem operacji, pobrać informacje z zewnętrznego systemu to w DDD mamy serwisy domenowe Domain Service, do których możesz wstrzyknąć za pomocą abstrakcji i DI jakąś tam implementację.

W przypadku gdy wymagane jest wysłać e-maila po każdym dodaniu pliku, jak wtedy byś to wykonał?

Zdarzenia domenowe Domain Events. Po zapisaniu stanu agregatu publikujesz zdarzenie i odpowiednie handlery/usługi nasłuchują sobie na to zdarzenie i jeżeli takowe się pojawi to wykonują jakąś tam swoją logikę powiązaną z tym zdarzeniem, np. wysyłka emaila. Tutaj technicznie rozwiązania są dwa:

  • obsługa zdarzenia w pamięci (in process/in memory) w ramach tej samej aplikacji
  • publikacja zdarzenia na kolejkę wiadomości i obsługa jej w innym procesie.
    • publikujesz zdarzenie od razu na kolejkę po zapisie
    • zapisujesz zdarzenie do bazy danych w jednej transakcji z agregatem i dedykowany proces nasłuchuje przykładową tabelę events, w momencie jak pojawi się w niej nowe zdarzenie to go publikuje na kolejkę

Jeżeli chodzi o obsługę zdarzenia w pamięci i publikujację zdarzenia bezpośrednio na kolejkę, to musisz uważać na sytuację w której po zapisie stanu agregatu coś się w aplikacji posypie i zdarzenie nie zostanie wykonane lub powtórzone, jest to tzw. at most once delivery. Istnieje ryzyko że wiadomość (zdarzenie) nie dotrze do adresata).

Przy zapisie zdarzenia do bazy danych i jego późniejszej publikacji stosuje się tzw, Outbox Pattern i tu już mamy at least once delivery jeśli chodzi o gwarancje dostarczenia wiadomości, czyli adresat może otrzymać wiadomość raz lub więcej (zostanie ponowiona po przywróceniu systemu po awarii). Dlatego ten model dostarczania wiadomości wymaga uwzględnienia takiej sytuacji po stronie konsumenta, czyli np. aby nie pobrać z konta komuś dwa razy pieniędzy. Jako dodatek to po stronie konsumenta wiadomości/zdarzenia możesz zastosować Inbox Pattern

Inbox i Outbox mają też tą zaletę że operacje nie wykonują się od razu tylko sekwencyjnie zdarzenie po zdarzeniu (chyba że zaimplementujemy je inaczej), więc w przypadku dużej ilości requestów w aplikacji mamy mechanizm chroniący nas przed przegrzaniem systemu.

Tu o gwarancjach dostarczenia wiadomości po polsku: https://oskar-dudycz.netlify.app/en/jak_nie_zgubic_zdarzenia_czyli_outbox_i_inbox_w_praktyce/

Mój przykład jest totalnie abstrakcyjny i jest wytworzony tylko w celach rozważań.

Jak większość przykładów :)

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