Sprawdzanie poprawności danych: oddzielona klasa czy nie?

16

Kiedy mam dużo danych, które należy zweryfikować, czy powinienem utworzyć nową klasę wyłącznie w celu sprawdzania poprawności, czy też powinienem trzymać się sprawdzania poprawności metodą?

Mój szczególny przykład dotyczy turnieju i klasy wydarzenia / kategorii: Tournamenti Eventktóra modeluje turniej sportowy, a każdy turniej ma jedną lub wiele kategorii.

W tych klasach jest wiele rzeczy do zweryfikowania: gracze powinni być puści, niepowtarzalni, liczba meczów, w które każdy gracz powinien zagrać, liczba graczy w każdym meczu, predefiniowane pojedynki i naprawdę duża itd. złożone zasady.

Jest też kilka części, które muszę zweryfikować jako całość, jak na przykład integracja klas. Na przykład jednolite sprawdzanie poprawności Playermoże być w porządku, ale jeśli zdarzenie ma dwukrotnie tego samego gracza, jest to błąd sprawdzania poprawności.

A co powiesz na to ?: Zapominam o absolutnie jakiejkolwiek kontroli wstępnej, gdy używam ustawiaczy klas modelu i podobnych metod do dodawania danych, a zamiast tego pozwalam klasom walidacyjnym sobie z tym poradzić.

Będziemy mieli coś w rodzaju EventValidatorz elementem Eventczłonkowskim i validate()metodę, która sprawdza poprawność całego obiektu, plus pojedyncze metody sprawdzania poprawności reguł wszystkich członków.

Następnie, przed utworzeniem wystąpienia ważnego obiektu, wykonam sprawdzanie poprawności, aby zapobiec nielegalnym wartościom.

Czy mój projekt jest poprawny? Czy powinienem zrobić coś inaczej?

Czy powinienem również używać logicznych metod sprawdzania poprawności? A może po prostu rzucić wyjątek, jeśli sprawdzanie poprawności się nie powiedzie? Wydaje mi się, że najlepszą opcją byłyby zwracające boolowskie metody i rzucały wyjątek, gdy obiekt zostanie utworzony w postaci instancji, na przykład:

public Event() {
    EventValidator eventValidator = new EventValidator(this);
    if (!eventValidator.validate()) {
        // show error messages with methods defined in the validator
        throw new Exception(); // what type of exception would be best? should I create custom ones?
    }
}
dabadaba
źródło

Odpowiedzi:

7

Można delegować dowolną logikę za pomocą kompozycji, jeśli logika ta zmieni się dynamicznie podczas wykonywania programu. Złożone walidacje, takie jak te, które objaśniasz, są równie dobrym kandydatem do delegowania na inną klasę poprzez skład.

Pamiętaj jednak, że walidacje mogą wystąpić w różnych momentach.

Tworzenie konkretnego walidatora jak w twoim przykładzie jest złym pomysłem, ponieważ łączy on klasę Event z tym konkretnym walidatorem.

Załóżmy, że nie używasz żadnego środowiska DI.

Możesz dodać walidator do konstruktora lub wstrzyknąć go za pomocą metody ustawiającej. Sugeruję, aby metoda twórcy w fabryce tworzyła zdarzenie i moduł sprawdzania poprawności, a następnie przekazała je do konstruktora zdarzenia lub za pomocą metody setValidator.

Oczywiście interfejs Walidatora i / lub klasa abstrakcyjna powinny być napisane, aby Twoje klasy zależały od niego, a nie od konkretnego walidatora.

Wykonywanie metody sprawdzania poprawności w konstruktorze może być problematyczne, ponieważ cały stan, który chcesz sprawdzić, może jeszcze nie istnieć.

Sugeruję, aby utworzyć interfejs Validable i pozwolić, aby Twoje klasy go wdrożyły, aby interfejs ten mógł mieć metodę validate ().

W ten sposób górne komponenty aplikacji wywołują dowolną metodę sprawdzania poprawności (która z kolei jest delegowana do członka sprawdzającego).

==> IValidable.java <==

import java.util.List;

public interface IValidable {
    public void setValidator(IValidator<Event> validator_);
    public void validate() throws ValidationException;
    public List<String> getMessages();
}

==> IValidator.java <==

import java.util.List;

public interface IValidator<T> {
    public boolean validate(T e);
    public List<String> getValidationMessages();
}

==> Event.java <==

import java.util.List;

public class Event implements IValidable {

    private IValidator<Event> validator;

    @Override
    public void setValidator(IValidator<Event> validator_) {
        this.validator = validator_;
    }

    @Override
    public void validate() throws ValidationException {
        if (!this.validator.validate(this)){
            throw new ValidationException("WTF!");
        }
    }

    @Override
    public List<String> getMessages() {
        return this.validator.getValidationMessages();
    }

}

==> SimpleEventValidator.java <==

import java.util.ArrayList;
import java.util.List;

public class SimpleEventValidator implements IValidator<Event> {

    private List<String> messages = new ArrayList<String>();
    @Override
    public boolean validate(Event e) {
        // do validations here, by accessing the public getters of e
        // propulate list of messages is necessary
        // this example always returns false    
        return false;
    }

    @Override
    public List<String> getValidationMessages() {
        return this.messages;
    }

}

==> ValidationException.java <==

public class ValidationException extends Exception {
    public ValidationException(String message) {
        super(message);
    }

    private static final long serialVersionUID = 1L;
}

==> Test.java <==

public class Test {
    public static void main (String args[]){
        Event e = new Event();
        IValidator<Event> v = new SimpleEventValidator();
        e.setValidator(v);
        // set other thins to e like
        // e.setPlayers(player1,player2,player3)
        // e.setNumberOfMatches(3);
        // etc
        try {
            e.validate();
        } catch (ValidationException e1) {
            System.out.println("Your event doesn't comply with the federation regulations for the following reasons: ");
            for (String s: e.getMessages()){
                System.out.println(s);
            }
        }
    }
}
Tulains Córdova
źródło
Jeśli chodzi o hierarchię klas, nie różni się to zbytnio od tego, co zasugerowałem, prawda? Zamiast tworzenia instancji i wywoływania walidatora wewnątrz konstruktora, zostałby on utworzony i wywołany, gdy jest to potrzebne w przepływie wykonania, jest to główna różnica, którą widzę.
dabadaba
Moja odpowiedź jest w zasadzie taka, że ​​OK można utworzyć inną klasę do wykonywania złożonej weryfikacji. Właśnie dodałem kilka rad, aby uniknąć twardego łączenia i uzyskać większą elastyczność.
Tulains Córdova
Gdybym miał dodać komunikaty o błędach do listy lub słownika, czy powinien on być w, Validatorczy w Validable? Jak mogę pracować z tymi komunikatami w połączeniu z ValidationException?
dabadaba 21.04.16
1
@dabadaba Lista powinna należeć do implementatorów IValidator, ale IValidable powinien dodać dostęp do tej metody, więc implementatorzy delegują ją. Zakładałem, że można zwrócić więcej niż jedną wiadomość sprawdzającą poprawność. Kliknij link edytowany n minut temu u dołu, aby zobaczyć różnice. Lepiej jest korzystać z widoku obok siebie (bez znaczników).
Tulains Córdova 21.04.16
3
Prawdopodobnie jest pedantyczny, ale zakładam, że Validatablejest to znacznie lepsza nazwa niż Validablejest to prawdziwy przymiotnik
user919426
4

Zapominam o absolutnie jakiejkolwiek kontroli wstępnej, gdy używam ustawiaczy klas modelu i podobnych metod do dodawania danych

To jest problem. Idealnie powinieneś zapobiegać stanom niepoprawnych obiektów: nie zezwalaj na tworzenie instancji z niepoprawnym stanem, a jeśli musisz mieć obiekty ustawiające i inne metody zmieniające stan, rzuć tutaj wyjątek.

zamiast tego pozwalam klasom sprawdzania poprawności obsłużyć sprawdzanie poprawności.

To nie jest sprzeczne. Jeśli logika sprawdzania poprawności jest na tyle złożona, że ​​gwarantuje własną klasę, nadal możesz przekazać sprawdzanie poprawności. A jeśli rozumiem, że masz rację, właśnie to robisz teraz:

Następnie, przed utworzeniem wystąpienia ważnego obiektu, wykonam sprawdzanie poprawności, aby zapobiec nielegalnym wartościom.

Jeśli nadal masz ustawienia, które mogą powodować nieprawidłowy stan, sprawdź poprawność przed faktyczną zmianą stanu. W przeciwnym razie pojawi się komunikat o błędzie, ale nadal pozostawisz swój obiekt w nieprawidłowym stanie. Znowu: nie dopuść, aby Twoje obiekty miały nieprawidłowy stan

Czy powinienem również używać logicznych metod sprawdzania poprawności? A może po prostu rzucić wyjątek, jeśli sprawdzanie poprawności się nie powiedzie? Wydaje mi się, że najlepszą opcją byłyby metody zwracające wartość logiczną i rzucały wyjątek, gdy obiekt zostanie utworzony

Brzmi dla mnie dobrze, ponieważ walidator oczekuje poprawnych lub niepoprawnych danych wejściowych, więc z ich perspektywy nieprawidłowe dane wejściowe nie są wyjątkiem.

Aktualizacja: jeśli „system jako całość” stanie się nieważny, przyczyną jest to, że jakiś obiekt musiał się zmienić (np. Odtwarzacz), a jakiś obiekt prawdopodobnie wyższego poziomu (np. Zdarzenie), który odwołuje się do tego obiektu, ma warunek, który nie jest już spełniony . Teraz rozwiązaniem byłoby niedopuszczenie bezpośrednich zmian w odtwarzaczu, które mogłyby spowodować, że coś będzie nieważne na wyższym poziomie. Tutaj pomagają niezmienne obiekty . Zatem zmieniony gracz jest innym przedmiotem. Gdy gracz zostanie powiązany z wydarzeniem, możesz go zmienić tylko od samego zdarzenia (ponieważ będziesz musiał zmienić odniesienie do nowego obiektu) i ponownie natychmiast sprawdzić poprawność.

Fabian Schmengler
źródło
Co powiesz na brak jakichkolwiek kontroli w instancji i ustawieniach oraz sprawdzanie poprawności jako operacji na boku? Sugerowało coś takiego jak Tulains Córdova. Ponieważ sprawdzanie poprawności i zgłaszanie wyjątku w seterze lub konstruktorze może działać dla tego elementu lub obiektu, ale nie pomaga sprawdzić, czy system jako całość jest w porządku.
dabadaba
@dabadaba moją odpowiedzią było pragnienie komentarza, proszę zobaczyć aktualizację powyżej
Fabian Schmengler
Nie chcę jednak używać niezmiennych obiektów, chcę móc modyfikować swoje klasy. I nie wiem, jak tworzyć niezmienne obiekty, chyba że odwołujesz się do końcowego słowa kluczowego. W takim przypadku musiałbym bardzo zmienić projekt, ponieważ częściowo konstruuję wydarzenia i turnieje z dodatkowymi seterami (ponieważ te dane są dodatkowe i konfigurowalne).
dabadaba
3
Aby stworzyć niezmienne obiekty, usuń wszystkie obiekty ustawiające i używaj tylko konstruktorów do ustawiania wartości. Jeśli masz opcjonalne dane lub dane, które nie są jednocześnie dostępne, rozważ użycie wzorca konstruktora: developer.amd.com/community/blog/2009/02/06/…
Fabian Schmengler