Value object - DDD, Hexagonal - jak podejść do waldiacji

Value object - DDD, Hexagonal - jak podejść do waldiacji
Ornstein
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 121
0

Przykład:
Funkcjonalność rejestracji użytkownika.

Do tej pory w kontrolerze mapowałem RegistrationRequest na DTO.
W warstwie application wywoływałem domenowy walidator, który sprawdzał m.in. czy username z tego DTO ma odpowiedni format.

Walidator domenowy rzucał dedykowane wyjątki (np. InvalidUsernameFormat), które w application przechwytywałem i mapowałem na Result z przypisanym ErrorType.

Teraz chciałbym przejść na VO.
Wychodzi na to że muszę zrobić tak. Zmiana z walidacji w walidatorze:

Kopiuj
 if (!data.username().matches("^[a-zA-Z0-9]{3,20}$")) {
            throw new InvalidUsernameFormat("Invalid username format. The username can contain only letters " +
                    "and numbers, and should be between 3 and 20 characters long.");
        }

na walidację w VO:

Kopiuj
public record Username(String value) {
    public Username {
        if (value == null || value.isBlank()) {
            throw new IllegalArgumentException("Username cannot be null or blank");
        }
        if (!value.matches("^[a-zA-Z0-9]{3,20}$")) {
            throw new IllegalArgumentException("Username must be 3-20 alphanumeric characters");
        }
    }
}

Czytałem, że VO nie powinny rzucać dedykowanych wyjątków, tylko najlepiej IllegalArgumentException.

Problem polega na tym, że rzucając taki wyjątek, nie jestem w stanie w warstwie application zmapować go na konkretny ErrorType, bo przecież inne pola również mogą rzucać IllegalArgumentException.

Może po prostu zostawię domenowy walidator i format będę walidował w dwóch miejscach: VO i w walidatorze.

Kopiuj
public Result<SuccessResponse> registerUser(UserRegistrationDto userDto) {
    try {
       validation.validate(userDto); // pierwsza linia obrony

        Username username = new Username(userDto.username()); // ostateczna linia obrony
        } catch (DomainException e) {
        // mapowanie na errorType
        } catch (IllegalArgumentException e) {
        /// ErrorType.INVALID_INPUT
        }

Jak do tego najlepiej podejść? Może jest jakieś lepsze rozwiązanie.

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

Do tej pory w kontrolerze mapowałem RegistrationRequest na DTO.

Nie kumam, request na DTO? Masz na myśli to że tworzyłeś UserDto? 🤔

Ornstein napisał(a):

W warstwie application wywoływałem domenowy walidator, który sprawdzał m.in. czy username z tego DTO ma odpowiedni format.

Walidator domenowy rzucał dedykowane wyjątki (np. InvalidUsernameFormat), które w application przechwytywałem i mapowałem na Result z przypisanym ErrorType.

Teraz chciałbym przejść na VO.

Czyli co konkretnie chcesz zrobić? Bo jeśli chodzi po prostu o to żeby przenieść logikę walidacji z application do requestu, do dto albo do vo; to moim zdaniem to zły pomysł.

Chodzi o to że w application jest za dużo logiki walidacji? Bo jeśli tak, to można do ogarnąć w jakiś inny sposób, np. wynieść do z application do jakiejś innej klasy (nie koniecznie do request, dto, vo), albo wytworzyć bardziej deklaratywny sposób - np. specyfikować rule'sy.

Na pewno łapanie IllegalArgumentException to słaby pomysł i moim zdaniem walidacja w VO to też słaby pomysł.

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

Do tej pory w kontrolerze mapowałem RegistrationRequest na DTO.

Nie kumam, request na DTO? Masz na myśli to że tworzyłeś UserDto? 🤔

Tak. RegistrationRequest to model controllera z adnotacjamio springowymi typu @NotEmpty. Żeby do warstwy application nie trafiał model z adnotacjami Spirngowymi, to go zmapowałem na UserRegistrationData.

Ornstein napisał(a):

W warstwie application wywoływałem domenowy walidator, który sprawdzał m.in. czy username z tego DTO ma odpowiedni format.

Walidator domenowy rzucał dedykowane wyjątki (np. InvalidUsernameFormat), które w application przechwytywałem i mapowałem na Result z przypisanym ErrorType.

Teraz chciałbym przejść na VO.

Czyli co konkretnie chcesz zrobić? Bo jeśli chodzi po prostu o to żeby przenieść logikę walidacji z application do requestu, do dto albo do vo; to moim zdaniem to zły pomysł.

Myślałem o przeniesieniu walidacji z tej klasy domenowej do VO. Wydaje mi się, że jest to naturalne jak chcę wprowadzić VO. Mylę się? Ewentualnie współdzielić regex i w pierwszej lini obrony sprawdzać w walidatorze a ostateczna w VO, albo w VO sprawdzać tylko null/blank a całą reszta w walidatorze.

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

Do tej pory w kontrolerze mapowałem RegistrationRequest na DTO.

Nie kumam, request na DTO? Masz na myśli to że tworzyłeś UserDto? 🤔

Tak. RegistrationRequest to model controllera z adnotacjamio springowymi typu @NotEmpty. Żeby do warstwy application nie trafiał model z adnotacjami Spirngowymi, to go zmapowałem na UserRegistrationData.

Czyli chodzi Ci o to że mapujesz takie springowe żądanie utworzenia na żądanie utworzenia w Twojej aplikacji? Jeśli tak to ja nazwałbym je app.spring.SpringRegisterRequest (które ma adnotacje springowe) oraz app.RegisterRequest (który jest czysty).

Ornstein napisał(a):

W warstwie application wywoływałem domenowy walidator, który sprawdzał m.in. czy username z tego DTO ma odpowiedni format.

Walidator domenowy rzucał dedykowane wyjątki (np. InvalidUsernameFormat), które w application przechwytywałem i mapowałem na Result z przypisanym ErrorType.

Teraz chciałbym przejść na VO.

Czyli co konkretnie chcesz zrobić? Bo jeśli chodzi po prostu o to żeby przenieść logikę walidacji z application do requestu, do dto albo do vo; to moim zdaniem to zły pomysł.

Myślałem o przeniesieniu walidacji z tej klasy domenowej do VO. Wydaje mi się, że jest to naturalne jak chcę wprowadzić VO. Mylę się?

A czym według Ciebie się różni DTO od VO? Bo orginalne znaczenie tych słów mam wrażenie zostalo zatracone, i teraz są używane wymiennie.

Ornstein napisał(a):

Ewentualnie współdzielić regex i w pierwszej lini obrony sprawdzać w walidatorze a ostateczna w VO.

Ja bym trzymał walidację w jednym miejscu. Nawet bym się zastanowił czy potrzebuję @NotEmpty ze springa.

Gdybym ja budował taką apkę, to chciałbym mieć CreateUserRequest (który jest czysty, po prostu ma publiczne pola bez niczego), i on byłby w całości walidowany przez coś co siedzi w application, i wynik walidacji byłby potem zwrócony do springa, żeby odesłać informacje do klienta. Osobna klasa requestu pod springa jest akceptowalna, ale ja wolałbym jej uniknąć.

Alternatywa to napisanie całej walidacji w springu, tylko wtedy walidacja jest ciasno-przywiązana do frameworka. Czasem to się opłaca, czasem nie.

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

Do tej pory w kontrolerze mapowałem RegistrationRequest na DTO.

Nie kumam, request na DTO? Masz na myśli to że tworzyłeś UserDto? 🤔

Tak. RegistrationRequest to model controllera z adnotacjamio springowymi typu @NotEmpty. Żeby do warstwy application nie trafiał model z adnotacjami Spirngowymi, to go zmapowałem na UserRegistrationData.

Czyli chodzi Ci o to że mapujesz takie springowe żądanie utworzenia na żądanie utworzenia w Twojej aplikacji?

Tak, dokładnie to robię.

Jeśli tak to ja nazwałbym je app.spring.SpringRegisterRequest (które ma adnotacje springowe) oraz app.RegisterRequest (który jest czysty).

Dobry pomysł.

Ornstein napisał(a):

W warstwie application wywoływałem domenowy walidator, który sprawdzał m.in. czy username z tego DTO ma odpowiedni format.

Walidator domenowy rzucał dedykowane wyjątki (np. InvalidUsernameFormat), które w application przechwytywałem i mapowałem na Result z przypisanym ErrorType.

Teraz chciałbym przejść na VO.

Czyli co konkretnie chcesz zrobić? Bo jeśli chodzi po prostu o to żeby przenieść logikę walidacji z application do requestu, do dto albo do vo; to moim zdaniem to zły pomysł.

Myślałem o przeniesieniu walidacji z tej klasy domenowej do VO. Wydaje mi się, że jest to naturalne jak chcę wprowadzić VO. Mylę się?

A czym według Ciebie się różni DTO od VO? Bo orginalne znaczenie tych słów mam wrażenie zostalo zatracone, i teraz są używane wymiennie.

VO zgłębiam od wczoraj. Z tego co zrozumiałem to element modelu domenowego, gdzie dto służy do przenoszenia danych między warstwami.

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

A czym według Ciebie się różni DTO od VO? Bo orginalne znaczenie tych słów mam wrażenie zostalo zatracone, i teraz są używane wymiennie.

VO zgłębiam od wczoraj. Z tego co zrozumiałem to element modelu domenowego, gdzie dto służy do przenoszenia danych między warstwami.

Podeślesz źródło z którego to czytasz?

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

@Ornstein no więc tak, te praktyki które są omawiane w tym filmie technicznie są jakby okej. Faktycznie czasem się używa obiektów które służą do przenoszenia wartości między warstwami, oraz obiekty które mają symbolizować wartość i są immutable.

Natomiast co do samej omawianej praktyki (obiekt który ma być wartością, jak int), ogólnie to jest dobry pomysł, ale nie wiem czy akurat walidacja requestu rejestracji to jest dobre miejsce na to. Walidacja żądania (co robisz w aplikacji) i walidacja obiektu (o czym mówi autor filmu) to nie jest to samo.

Nie rób tak że jak zobaczysz jakąś praktykę to starasz się od razu gdzieś jej użyć w projekcie, to często przynosi słaby efekt. Lepiej - poznaj ją, zrozum, I poczekaj aż podczas programowania zauważysz ze warto jej użyć.

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

@Ornstein przypomniałem sobie. jeśli jesteś zainteresowany takimi obiektami o jakich mowa w filmie, to jest pewien (moim zdaniem dużo lepszy) filmik o takich obiektach od Kevlina Henney.

Tylko Tutaj jest przykład na podstawie numeru karty płatniczej. Bardzo merytorycznie wysokiej jakości.

ShyAnteater
  • Rejestracja: dni
  • Ostatnio: dni
  • Postów: 14
2

Value Object posiada kilka charakerystyk, o których warto tu wspomnieć i w mojej opini różni się diametralnie od DTO.

Ulotność/krótkie życie

W pierwszej kolejności przy porównywaniu dwóch Value Object'ów ze sobą nie interesuje nas ich referencja tylko to co hermetyzują w środku. To znaczy, że jeśli chcemy porównać obie klasy z dużym pradopodobieństwem napiszemy swoją metodę "equals". Skąd bierze się ich ulotność? Możemy wziąć na tapet przykład zamodelowania klasy Money. Taka klasa będzie przychowywać nominał oraz walutę, która jest widoczna na banknocie. Z racji tego - możemy co chwilę tworzyć obiekt new Money(20, 'PLN') oraz go kasować, ponieważ tak na prawdę interesuję nas tylko wartość. W NASZYM KONTEŚCIE nie musimy śledzić co dzieje się z tymi pieniędzmi. Gdybyśmy implementowali za to klasę Money dla Narodowego Banku Polskiego. To sytuacja się zmienia. Wtedy taką kasę nazwalibyśmy encją w rozumieniu DDD i z wysokim pradopodobieństwem taka implementacją posiadałaby ID i cały system śledzenia ze statusami.

Immutability/brak side effect'ów

Jeśli chcemy zsumować dwie kwoty ze sobą to możemy zrobić coś takiego:

Kopiuj
class Money {
  private value: number;

  constructor(value: number) {
    // tutaj walidacja jesli jest potrzebna
  }

  public add(other: Money): Money {
    return new Money(this.value + other.asNumber());
  }
}

W tym przykładzie jak możesz zauważyć używam metody "other.asNumber()", która służy do "projekcji" wewnetrzego atrybutu klasy Money. To dosyć ważne, ponieważ Value Object z definicji wyklucza gettery i settery. Ale to nie jest ważne. Ważne jest jak wygląda metoda do sumowania obu kwot. Za każdym razem zachowujemy silną hermetyzację i nie modyfikujemy bebechów takiej klasy za pomocą settera. Po prostu przy każdej takiej operacji staramy się tworzyć nowy obiekt z nową wartością.

Zastępowalność

To wynika z punktu Ulotność/krótkie życie. Z racji tego, że taki obiekt "żyje" bardzo, krótko i interesuję nas tylko wewnetrzna wartość to w takim wypadku możemy z łatwością zstępować takie obiekty nowymi.

Domena

VO posiada przede wszystkim metody, które są silnie powiązane z domeną klienta.

Oto inna implementacja Value Object'u dla biegu w skrzyni biegów w samochodzie. Oczywiście mój model rzeczywistości jest uproszczony na potrzeby przykładu 😀

Kopiuj
class Gear {
  private gear: number;
  
  constructor(gear: number) {
    if (gear < 0) {
      throw new Error('Negative representation of gear');
    }

    this.gear = gear;
  }
  
  public next(): Gear {
    return new Gear(this.gear + 1);
  }
  
  public previous(): Gear {
    return new Gear(this.gear - 1);
  }
  
  public equals(otherGear: Gear): boolean {
    return this.gear === otherGear.toIntValue();
  }
  
  public greaterThan(gear: Gear): boolean {
    return this.gear > gear.toIntValue();
  }
  
  public lowerOrEqualTo(gear: Gear): boolean {
    return this.gear <= gear.toIntValue();
  }
  
  public toIntValue(): number {
    return this.gear;
  }
}

DTO - to obiekt, który jest anemiczny. Nie posiada metod. Po prostu transportuje potrzebne informacje na potrzebny pewnej operacji w naszym systemie lub mikroserwisie. Za pomocą DTO ustalamy co może dotrzeć do naszego API w warstwie UI.

@Riddle ma 100% racji. Cytuję:

Nie rób tak że jak zobaczysz jakąś praktykę to starasz się od razu gdzieś jej użyć w projekcie, to często przynosi słaby efekt. Lepiej - poznaj ją, zrozum, I poczekaj aż podczas programowania zauważysz ze warto jej użyć.

W takim wypadku zapytałbym. Czy rejestracja użytkownika jest powiazana z logiką biznesową klienta? Czy jednak implementacja rejestracji oraz logowania to robimy zawsze niezależnie od domeny? 😀 Prawdopodobnie kumaty senior takie operacje jak rejestracja lub logowanie wyniesie do osobnego modułu lub mikroserwisu, ponieważ to nie jest domena klienta.

Tak na prawdę mówisz o dwóch różnych aspektach i dwóch różnych warstwach w rozumieniu DDD. Walidacja DTO, która odbywa się na poziomie warstwy UI oraz walidacji w konteście warstwy domenowej. To w zupełności dwie różne koncepcje. Tutaj odsyłam Cie do Implementing Domain-Driven Design by Vernon Vaughn oraz Domain-Driven Design: Tackling Complexity in the Heart of Software by Eric Evans. Tak znajdziesz opisy warstw oraz ich przeznaczenie oraz znajdziesz bardziej komplementarny opis czym jest Value Object.

Ciekawe źródła do przejrzenia

https://github.com/BottegaIT/ddd-leaven-v2
https://github.com/ddd-by-examples
https://bottega.com.pl/pdf/materialy/ddd/ddd1.pdf

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.