Walidacja danych i tworzenie konta z Either - lepszy zapis

Walidacja danych i tworzenie konta z Either - lepszy zapis
SA
  • Rejestracja:ponad 3 lata
  • Ostatnio:ponad 2 lata
  • Postów:94
0

Mam klasę AccountCreator z metodą create, która przyjmuje DTO z danymi potrzebnymi do utworzenia konta. Na początku jest próba stworzenia 2 value objectów (UserName i Password), potem walidacja unikalności user name, utworzenie encji Account, która przyjmuje w konstruktorze te 2 value objecty i zapisanie w repo. Oczywiście mogą zostać zwrócone błędy typu nieprawidłowa długość hasła itd. Użyłem do tego Eitherów i teraz pytanie czy ten kod jest ok czy może da się to jakoś lepiej zapisać?

Kopiuj
public Either<Error, AccountDto> create(AccountCreateDto accountCreateDto) {

        final var errorType = ErrorType.ACCOUNT_PERSISTENCE_ERROR;

        final var errorMessage = "Not unique user name: " + accountCreateDto.userName;

        final var error = new Error(errorType,errorMessage);

        return UserName
                .create(accountCreateDto.userName)
                .flatMap(userName ->

                        userNameUniquenessChecker.isUnique(userName.text) ?

                                Password
                                        .create(accountCreateDto.password)
                                        .flatMap(password -> {

                                            final var createdAccount = new Account(
                                                    userName,
                                                    password,
                                                    AccountStatus.OPEN,
                                                    LocalDateTime.now(),
                                                    new ArrayList<>());

                                            final var addedAccount= accountRepository.add(createdAccount);

                                            final var accountDto = new AccountDto(
                                                    addedAccount.userName.text,
                                                    addedAccount.password.text,
                                                    addedAccount.status,
                                                    addedAccount.creationDate,
                                                    (long) addedAccount.tasks.size());

                                            return Either.right(accountDto);

                                        }) : Either.left(error));
Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
2

Ty tak poważnie? Ja bym spytał czy da się to zapisać jakoś mniej czytelnie :D

  1. Po co są ci te value objecty? Jeśli juz są, to czemu to nie one przeprowadzają walidacje tylko jest ona jakoś obok?
  2. Nie rób wielolinijkowych lambd!
  3. Czemu nie zwrócisz Either<Error, Account> na którym następnie zawołasz .flatMap(AccountRepository::add) a później .flatMap(Account::toDto) ? Będzie 100 razy czytelniej
  4. Te errory zdefiniowane na początku to tez jakiś dziwny pomysł.

edit: oczywiście nie raczyłeś dać stubów do reszty klas, zeby dało sie z tym wygodnie coś poczarować. Moze np.:

Kopiuj
import io.vavr.control.Either;

class AccountDto {
}

record AccountCreateDto(String userName, String password) {
}

class Error {
}

class UsernameUniquenessChecker {
    Either<Error, String> validateUnique(String name) {
        // if unique....
        return Either.right(name);
    }
}

class UniqueUsernameCreator {
    private final UsernameUniquenessChecker checker = null

    public Either<Error, UniqueUserName> create(String name) {
        return checker.validateUnique(name)
                .map(UniqueUserName::new);
    }
}

class ValidPasswordCreator {
    public Either<Error, ValidPassword> create(String name) {
        return Either.right(new ValidPassword(name));
    }
}

record UniqueUserName(String username) {
}

record ValidPassword(String password) {
}

record Account(UniqueUserName username, ValidPassword password) {
    public AccountDto toDto() {
        return new AccountDto();
    }
}

class AccountRepository {
    public Either<Error, Account> add(Account account) {
        return Either.right(account);
    }
}

public class Accounty {
    private final AccountRepository accountRepository = null;
    private final UniqueUsernameCreator uniqueUsernameCreator = null;
    private final ValidPasswordCreator passwordCreator = null;

    public Either<Error, AccountDto> create(AccountCreateDto accountCreateDto) {
        return uniqueUsernameCreator
                .create(accountCreateDto.userName())
                .flatMap(username -> passwordCreator.create(accountCreateDto.password())
                        .map(password -> new Account(username, password))
                )
                .flatMap(accountRepository::add)
                .map(Account::toDto);
    }
}

"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
edytowany 2x, ostatnio: Shalom
PerlMonk
"oczywiście nie raczyłeś" - oczywiście nie przegapiłeś okazji, żeby tego nie zauważyć ;)
Shalom
? Masz jakiś problem? Spróbuj poprawić kawałek kodu, który jest cały czerwony bo wymaga 10 klas których nie masz. 90% czasu na poprawienie tego kodu schodzi na dodanie brakujących kawałków, zamiast na poprawienie tego co dostałes.
PerlMonk
"Masz jakiś problem?" - niee, nie jestem z Pragi :P . Śmieszkuję. Sam w ogóle nie mogę patrzeć na ten kod :D
SA
  • Rejestracja:ponad 3 lata
  • Ostatnio:ponad 2 lata
  • Postów:94
0

@Shalom: @PerlMonk :

Teraz może być?

Kopiuj
public final class AccountCreator {

    private final UserNameUniquenessChecker userNameUniquenessChecker;
    private final AccountRepository accountRepository;

    public AccountCreator(UserNameUniquenessChecker userNameUniquenessChecker, AccountRepository accountRepository) {
        this.userNameUniquenessChecker = userNameUniquenessChecker;
        this.accountRepository = accountRepository;
    }
    
    private Account createAccount(UserName userName, Password password) {
        
        return new Account(userName,password,AccountStatus.OPEN,LocalDateTime.now(),new ArrayList<>());
    }
    
    private AccountDto toDto(Account account) {
        
        return new AccountDto(
                account.userName.text,
                account.password.text,
                account.status,
                account.creationDate,
                (long) account.tasks.size());
    }

    public Either<Error, AccountDto> create(AccountCreateDto accountCreateDto) {
        
        return UserName
                .create(accountCreateDto.userName,userNameUniquenessChecker)
                .flatMap(userName -> Password
                        .create(accountCreateDto.password)
                        .map(password -> createAccount(userName,password))
                        .map(accountRepository::add)
                        .map(this::toDto));
    }
}

edytowany 2x, ostatnio: Sampeteq
Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

Ja osobiście nadal nie rozumiem sensu istnienia tych klas UserName oraz Password.Co one tutaj wnoszą konkretnie? Przepisujesz jedno DTO (wejściowe) na drugie a potem na trzecie (bazodanowe). Jaki sens ma to środkowe?


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
LU
Factory i walidacja nie jest częścią domeny?
Shalom
Ale tutaj nic takiego nie ma. Jest przepisanie jednego DTO na drugie i od razu na trzecie. Linijka po linijce. A walidacja jest zrobiona osobno, poza tymi obiektami.
SA
  • Rejestracja:ponad 3 lata
  • Ostatnio:ponad 2 lata
  • Postów:94
0

@Shalom: UserName i UserPassword to value objecty z logiką biznesową.

Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
0

Może jestem ślepy, ale tutaj zupełnie jej nie widzę. Wziałeś DTO, przepisałeś do obiektów domenowych, tylko po to żeby następnie przepisać to do kolejnego DTO które leci do bazy. Jeszcze pal licho jakbyś tam miał jakiś obiekt domenowy Credentials który ma jakąś metodę Account toAccount() która zamienia go w ten bazodanowy obiekt, to może miałoby to minimalny sens, ale tutaj nie bardzo rozumiem po co jest przelotka przez kolejne obiekty.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
LU
No właśnie czy jest sens tworzyć obiekty, które "trzymają" to samo co DTO tylko dlatego żeby klasy z DTO w nazwie nie przewijały się w domenie?
Shalom
Ale tutaj nic się nie przewija w żadnej domenie. Ktoś w jednym miejscu robi linijka po linijce x->y->z zamiast x->z.
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około 2 godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4707
2

W tym fragmencie kodu może sensu nie widać na UserName i Password, ale to akurat normalne(*) żeby wydzielić takie typy. Nie każdy String sie nadaje na user name, nie każda tablica bajtów nadaje się na hasło. Do tego zwykle są jakieś specyficzne metody związane z tymi typami.

* - normalne to jest w językach, które nie karają za wprowadzanie typów bo mają jakieś wrappery typu value class lub typealiasy.


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 1x, ostatnio: jarekr000000
Zobacz pozostałe 4 komentarze
jarekr000000
@lookacode1: tak, jak pisałem - to się nigdy nie powinno zdarzyć, więc w teorii taka dodatkowa asercja nie ma sensu. W praktyce i tak się takie wsadza - rzadko, ale bywa. Z różnym kodem pracujesz, z różnymi ludźmi. Czasem ktoś za 2 lata zacznie coś przerabiać...
LU
Nie chodzi mi o samą asercję tylko jak mamy email i zwalidowaliśmy go na poziomie weba to czy warto jeszcze robić taki domenowy wrapper Email i tam też walidować i jeżeli niepoprawny rzucać wyjątkiem?
jarekr000000
Wrapper na email warto (przeważnie - zależy od systemu oczywiście). Powinno sie to tak zrobić, żeby niepoprawnego się nie dało skonstruować (czyli w jakiejś metodzie fabrycznej walidacja). W bardzo wyjątkowych przypadkach daje się nadmiarowe asercje.
LU
I wtedy ta metoda fabryczna zwracałaby u Ciebie Either? Ja bym chyba po prostu rzucił wyjątkiem bo dla mnie to byłby błąd programisty, który dopuścił do sytuacji, że przekazywane dane nie są po poprawne (tj. np. niezwalidowane gdzieś wyżej w warstwie web).
jarekr000000
A ja bym zwracał either tam gdzie dokonuje tej walidacj Either<Error, UserName>
SA
  • Rejestracja:ponad 3 lata
  • Ostatnio:ponad 2 lata
  • Postów:94
0

@Shalom: @jarekr000000:

Tak wyglądają UserName i Password.

Kopiuj
public final class UserName {

    public final String text;

    public UserName(String text) {
        this.text = text;
    }

    public static Either<Error,UserName> create(String userName,UserNameUniquenessChecker userNameUniquenessChecker) {

        final var minUserNameLength = 3;

        final var maxUserNameLength = 10;

        final var isUserNameLengthWrong =
                userName.length() < minUserNameLength || userName.length() > maxUserNameLength;

        if (isUserNameLengthWrong) {

            return Either.left(ErrorFactory.createWrongUserNameLength(
                    userName,
                    minUserNameLength,
                    maxUserNameLength));
        }

        else if (userNameUniquenessChecker.isDuplicate(userName)) {

            return Either.left(ErrorFactory.createNotUniqueUserNameError(userName));
        }

        else {

            return Either.right(new UserName(userName));
        }
    }
}
Kopiuj
public final class Password {

    public final String text;

    private Password(String text) {
        this.text = text;
    }

    public static Either<Error, Password> create(String text) {

        final var minPasswordLength = 5;

        return text.length() < minPasswordLength ?

                Either.left(ErrorFactory.createWrongPasswordLengthError(text,minPasswordLength)) :

                Either.right(new Password(text));
    }
}

Tak jak napisałem. To są value objecty.

jarekr000000
Publiczny konstruktor w UserName to w takim układzie coś dziwnego.
SA
Oczywiście miał być prywatny. Drobny błąd się wkradł, dzięki.
Shalom
  • Rejestracja:około 21 lat
  • Ostatnio:prawie 3 lata
  • Lokalizacja:Space: the final frontier
  • Postów:26433
1

Przyznam że ja bym w kodzie tak nie hardkodował tych zakresów na min i max length, tylko wyciągnął to do jakiegoś konfiga.


"Nie brookliński most, ale przemienić w jasny, nowy dzień najsmutniejszą noc - to jest dopiero coś!"
SA
  • Rejestracja:ponad 3 lata
  • Ostatnio:ponad 2 lata
  • Postów:94
0

@Shalom: Tzn? Np. abstrakcyjna klasa ze stałymi?

Shalom
Nie. Żeby te wartości przychodziły z jakiegoś .properties a nie były wpisane w kodzie.
jarekr000000
@Shalom - Pervertus Maximus. Chyba właśnie zostaś korpoarchitektem.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)