Value objecty - walidacja

Odpowiedz Nowy wątek
2019-08-12 10:50
0

Dzień dobry
Value objecty to świetny sposób na modelowanie domeny, np. klasa Pesel jest o wiele bardziej prezycyjna niż String i łatwiej będzie zrozumiec o co chodzi. Jednak pojawia się pewien "problem".
Jesli mamy taka klase:


public final class Pesel {
 private final String value;

private Pesel(String value) {
  this.value = value;
 }

public static Pesel of(String pesel) {
  return new Pesel(pesel);
 }
}

Czy to nie powinniśmy w takiej metodze wytwórczej sprawdzać czy wartośc jest prawidłowa? Jesli tak, to co zrobić jeśli jest to wartośc nieprawidłowa? Wyrzucac runtimowy expetion?


Nie pomagam przez PM. Pytania zadaje się na forum.
Pokaż pozostałe 16 komentarzy
@danek: no i właśnie to miałem na myśli pisząc o przesłanianiu. - Wibowit 2019-08-12 15:08
Nie używam jak nie muszę, bo mi się nie chcę, ale uważam, że z this jest czytelniej. Narzucanie innym swojej estetyki w tak mało istotnej kwestii jest słabe. Ważne by było spójnie w całym projekcie. I też nie zawsze przeglądam kod w IDE, aż takie to dziwne? - nalik 2019-08-12 15:43
Ależ przeżywacie. Na szczęście z nikim nie musiałem się dogadywać w takiej kwestii, bo nie miałem w projekcie nikogo, kto by nadmiarowe this. wklepywał. W Scali to już absolutnie się takich ludzi nie spodziewam. Nie widziałem ani jednego kawałka Scalowego kodu z nadmiarowym this. - Wibowit 2019-08-12 15:47
W swoich własnych projektach wybieram konwencję z this, w pracowych jak wyjdzie, jest generalnie pół na pół. - somekind 2019-08-12 16:17
Co do review - jak znam taska (bo nad nim na bieżąco współpracowałem albo konsultowałem), to nie muszę kodu uruchamiać. Większość tasków jest jednak na tyle trywialna, że wystarczy rzut oka na GH, żeby stwierdzić, czy jest OK. Poza tym ja zazwyczaj robię PR Hindusom, więc najpierw odrzucam tak długo, aż doprowadzą go do stanu czytelności. Kodu niespełniającego konwencji, norm estetycznych albo zawierającego elementarne błędy (np. brak walidacji wejścia) nie ma sensu pobierać ani uruchamiać. - somekind 2019-08-12 16:17

Pozostało 580 znaków

2019-08-12 13:14
1

@tdudzik: po 1 Path było zanim było Optional, po 2 to że tak jest w API Javy to nie znaczy ze jest dobre. Calendar tez był a raczej dalej jest w API Javy...


Nie pomagam przez PM. Pytania zadaje się na forum.
edytowany 1x, ostatnio: scibi92, 2019-08-12 13:15
Czasem poprawne i spójne jest lepsze niż dobre i niespójne. Poza tym podałem już kilka przykładów, gdzie Optional powoduje problemy. - tdudzik 2019-08-12 13:17
gdzie podałeś? - danek 2019-08-12 13:18
Pod Twoim postem. Btw. co do po 1 Path było zanim było Optional <- w Scali Option przecież był chyba od początku, mimo to stosuje się podobne podejście. - tdudzik 2019-08-12 13:21
Scala powstawała jako swojego rodzaju eksperyment, który miał pokazać, że można połączyć dwa różne paradygmaty w jednym języku. Z tego powodu można dość łatwo natknąć się na API, które na przykład rzuca błędy. To czy tak się pisze to już kwestia tego do jakiego zespołu trafisz, jeśli do takiego, który kładzie duży nacisk na FP to zapewne nikt nie będzie wykorzystywać takich metod - DisQ 2019-08-12 13:43
W sumie jeśli założymy że najpierw walidujemy to jako Stringa z frontendu a dopiero później gdzies w środku używamy value objectu to można wyrzucac wyjątki bo wtedy rzeczywiście coś jest nie tak i to jest błąd systemu że wartośc nie przeszła walidacji z zewnątrz - scibi92 2019-08-12 13:46

Pozostało 580 znaków

2019-08-12 14:44
0

Skoro mowa o Value objectach to mowa o DDD. Więc nie powinno się dać stworzyć niepoprawnego Peselu, np "1234" albo "Ala ma kota". Także owszem, w konstruktorze powinieneś to sprawdzać. Albo w fabryce, jeśli zasady dla stworzenia poprawnego obiektu są dość skomplikowane i chcesz je gdzieś wydzielić.

Także oprócz sprawdzenia klasyka, typu null, pusty string potrzebujesz jeszcze poprawności logicznej, może nawet sprawdzenia sumy kontrolnej w wypadku numeru pesel.


Pozostało 580 znaków

2019-08-12 14:58
0

Skoro mowa o Value objectach to mowa o DDD.

Wybacz szczerość, ale skąd ta absurdalna teza?


Nie pomagam przez PM. Pytania zadaje się na forum.
VO to jeden z klocków DDD, więc nie taka absurdalna :D co nie zmienia faktu, że VO bez DDD też sobie może żyć - baant 2019-08-12 15:06
Idąc tym tokiem rozumowania że skoro mowa o funkcjach to mowa o FP. Albo inaczej, skoro mowa o prostokątach to mowa o kwadratach - scibi92 2019-08-12 15:09
Bo ta implikacja chyba powinna być w drugą stronę. Wtedy miałoby to sens. - nalik 2019-08-12 15:52
No tak, DDD zakłada korzystanie z VO, ale korzystanie z VO nie zakłada korzystania z DDD - scibi92 2019-08-12 16:05
Nie spotkałem się, żeby ktoś kiedyś używał terminu Value Object nie w kontekście DDD. Bo i po co? - somekind 2019-08-12 17:24

Pozostało 580 znaków

2019-08-12 16:54
0

Bo:

  1. Pojawia się dużo tematów na temat DDD.
  2. Napisałeś o jednym z jego klocków.
  3. Napisałeś o modelowaniu domeny - do czego się zwykle używa DDD.

Więc uznałem za rozsądne takie założenie. Swoją drogę, nawet jeśli pytasz w innym kontekście, to moja odp będzie taka sama. Wolę nie móc stworzyć nie poprawnego obiektu, niż móc to spartaczyć, bo ktoś zapomni wywołać walidatora wcześniej.


pojęcia domeny używa się w każdym projekcie większym niż crud ;) - danek 2019-08-12 18:11

Pozostało 580 znaków

2019-08-12 17:39
0

Prawde powiedziawszy pytałem w kontekście archiektury cebulowej czyli hexagonalnej


Nie pomagam przez PM. Pytania zadaje się na forum.
Czyli kłamczuchowałeś o absurdalności.... nu nu nu! ;-) - AreQrm 2019-08-12 18:09

Pozostało 580 znaków

2019-08-12 23:14
cs
0

Późno jest, więc pomysły dziwne mi przychodzą do głowy, więc może zamiast oklepanych metod może taka, która tworzy "kulawe" PESELE jako obiekty osobnej klasy:

class Pesel{
    public static final class Valid extends Pesel{
        private Valid(String peselStr) {
            super(peselStr);
        }

        public String birthYear(){
            return pesel.substring(0,2);
        }

        public String birthMonth(){
            return pesel.substring(2,4);
        }

        public String birthDay(){
            return pesel.substring(4, 6);
        }

        @Override
        public String toString() {
            return "Valid{" +
                    "pesel='" + pesel + '\'' +
                    '}';
        }
    }

    public static final class Wrong extends Pesel{
        private Wrong(String peselStr) {
            super(peselStr);
        }

        public String errors(){
            return "Wrong length";
        }

        @Override
        public String toString() {
            return "Wrong{" +
                    "pesel='" + pesel + '\'' +
                    '}';
        }
    }

    protected String pesel;

    private Pesel(String peselStr){
        pesel = peselStr;
    }
    public static Pesel of(String peselStr){
        return valid(peselStr) ? new Valid(peselStr) : new Wrong(peselStr);
    }

    private static boolean valid(String peselStr){
        return peselStr.length() == 11;
    }

    public boolean isValid(){
        return this instanceof Valid;
    }

    public boolean isWrong(){
        return this instanceof Wrong;
    }

    public String getPesel(){
        return pesel;
    }
}

public class ValueObjectDemo {
    public static void main(String[] args) {

        List<Pesel> list = Arrays.asList(Pesel.of("69110828130"), Pesel.of("4573945387"), Pesel.of("7891827981739"));
        System.out.println("Valid PESEL's");
        list.stream().filter(Pesel::isValid).map(a -> (Pesel.Valid) a).forEach(p -> System.out.println(p + ", year: " + p.birthYear() + ", month: " + p.birthMonth()));
        System.out.println("Wrong PESEL's");
        list.stream().filter(Pesel::isWrong).map(a-> (Pesel.Wrong) a).forEach(p -> System.out.println(p.errors()));
    }
}
edytowany 1x, ostatnio: cs, 2019-08-13 10:58
Dokładnie tak działa Either tylko jest generyczny ;) - danek 2019-08-12 23:14
Abstrahując już od tego, że ten kod wcale nie waliduje poprawnie numeru PESEL. - somekind 2019-08-13 03:10
Poza tym, że nie waliduje numeru PESEL, to jeszcze nie zwraca poprawnie wartości roku urodzenia czy miesiąca. Dużo kodu i dyskusji technicznej, a efekt biznesowy mizerny. Czasem warto skupić się na tym, żeby kod poprawnie robił to co powinien, a nie deliberować nad aspektami technicznymi. - yarel 2019-08-13 16:16
Raczej założeniem tego kawałka kodu nie było przeprowadzanie poprawnej walidacji PESEL tylko pokazanie sposobu na rozwiązanie konkretnego problemu ;) - DisQ 2019-08-13 16:21
Gdyby chodziło o pokazanie sposobu, to nie byłoby tylu szczegółów implementacyjnych. - somekind 2019-08-13 16:22
Te szczegóły (nie koniecznie implementowane poprawnie z punktu widzenia biznesu) w tym przypadku mają sens. Żeby przekazać ideę trzeba było pokazać, że walidacja może zwrócić dwa typy, a każdy z nich ma zdefiniowane specyficzne metody które można sobie później wywoływać. Z mojego punktu widzenia, te szczegóły są potrzebne, żeby przykład miał ręce i nogi. - DisQ 2019-08-13 16:28

Pozostało 580 znaków

2019-08-12 23:24
1

@cs: Takie rozwiązanie na pierwszy rzut oka ma co najmniej dwa problemy:
Pierwszym z nich jest to, że ktoś przyjdzie, zrobie refactoring, usunie bądź zmieni pozycję filter(Pesel::isWrong) i kod może rzucać błędami.
Drugim jest fakt, że z takim podejściem może być problematyczne dodawanie kolejnych błędów. Każdy kolejny błąd to kolejna metoda sprawdzająca (isChecksumValid, isOnlyDigits i tak dalej), kolejne rzutowania i kolejne pola do błędów.

Rozwiązaniem mogłoby być zamodelowanie błędów jako ADT, a typ zwracany przez metodę tworzącą mógłby mieć sygnaturę Either<PeselValidationError, Pesel> bądź Validation<List<PeselValidationError>>, Pesel>

Pozostało 580 znaków

2019-08-12 23:51
0

Dokładnie, są tylko 3 rozwiązania:
1)rzucanie wyjątków
2)Optional (pusty dla nieprawidłowej wartości)
3)Either
inne sa po prostu złe


Nie pomagam przez PM. Pytania zadaje się na forum.
Mała myśl przed kawą: może warto wprowadzić ValidatedPesel oraz NotValidatedPesel. Z UI / networkingu byłby pobierany NotValidatedPesel który potem wpadałby do ~PeselValidator i tam albo byłby na wyjściu ValidatedPesel i z nim do Core albo inna ścieżka np info do użytkownika hola hola ale to nie pesel. Coś ala bounded contexts. - lubie_programowac 2019-08-13 09:31

Pozostało 580 znaków

2019-08-13 00:05
2

@scibi92: Przy czym dodałbym:

wyjątek - autor API zakłada, że nikt złych danych nie powinien przekazać. Jak przekaże to prawdopodobnie inna logika zawiodła (walidacja / GUI). Rzuca wyjątek by podnieść alarm, zaczerwienić monitoring - zmusić kogoś do działania. W pewnych bardzo rzadkich kontekstach może też rzucić checked exception by dać wołającemu szansę na naprawę stanu i ponowienie.

optional / either - autor API zakłada, że złe dane będą przekazywane i nie należy panikować. Rzucanie wyjątków nie ma tutaj sensu. Wyjątki są wolne, nie optymalizowane przez JIT, tworzą stacktracey na heapie.


"Gdy się nie wie, co się robi, to się dzieją takie rzeczy, że się nie wie, co się dzieje"


edytowany 2x, ostatnio: nie100sowny, 2019-08-13 00:07

Pozostało 580 znaków

2019-08-13 07:52
cs
0
scibi92 napisał(a):

Dokładnie, są tylko 3 rozwiązania:
1)rzucanie wyjątków
2)Optional (pusty dla nieprawidłowej wartości)
3)Either
inne sa po prostu złe

@scibi92: Nie byłbym taki kategoryczny, u nas we wiosce mówią: jakbyś nie obrócił to i tak d..a zawsze zostanie z tyły. Każde z rozwiązań ma swoje "ale":
1) rzucanie wyjątków - cóż to za wyjątkowa sytuacja, że user się powyli, wyjątki wprowadzono dawno temu jako jednolitą formę reakcji na tak różne błędy od krytycznych po drobnostki
2) Optional - ok ale jak chcemy dowiedzieć się dlaczego nie udało się obiektu utworzyć to brak informacji o przyczynie
3) Either - ma to do siebie, że w razie niepowodzenia można zwrócić wszystko, co dusza zapragnie, chcę utworzyć pesel a dostaje opakowanie w którym może być pesel albo nie.

Jak dla mnie rozwiązanie zależy od sytuacji, czyli w którą stronę chcesz się obrócić i co tam chcesz zobaczyć. Szukanie rozwiązania, że nam z tyłu d..a zniknie jest utopią.

@danek
Problem z Either jest taki, że metoda wytwórcza klasy A powinna tworzyć obiekt tej klasy, a tu wyskakuje jakiś Either, który może zawierać żądany obiekt albo coś innego. Czy to jest intuicyjne?

To o co mi chodziło w tym rozwiązaniu to to, żeby metoda wytwórcza zwracała zawsze obiekt danego typu, a przez dziedziczenie segreguje, który obiekt jest poprawny a który kulawy. Poprawny ma metody przetwarzające dane, o których wiemy, że są na 100% poprawne, a błędny może mieć metody informujące, dlaczego nie jest taki, czego mu brakuje i zawiera surowe dane.

Pozostało 580 znaków

Odpowiedz
Liczba odpowiedzi na stronę

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