Quantcast
Channel: The Explorer » patterns
Viewing all articles
Browse latest Browse all 10

O walidacji słów kilka

$
0
0

W dyskusji pod wpisem na blogu Matiego poruszyłem temat „walidatorów” w kontekście komponentu Zend Form w Zend Frameworku i tego, że mam zastrzeżenia co do ich koncepcji. Żeby być precyzyjnym nt. przedmiotu o którym będę się rozwodził pozwolę sobie po pierwsze odpowiedzieć na pytanie – czym jest walidacja ?

Otóż słowo walidacja, jest spolszczeniem angielskiego czasownika „to validate”, który pochodzi od rzeczownika „valid”. „Valid” oznacza po prostu „poprawny”, a więc intuicyjnie „walidacja” to sprawdzanie poprawności. W przypadku oprogramowania walidacja, oznacza sprawdzenie poprawności danych w kontekście aplikacji, w której są one wykorzystane.

Najpowszechniejsze zastosowanie walidacji w aplikacjach opartych na ZF ma miejsce przy wszelkiego rodzaju formularzach. Zwykle w akcji kontrolera tworzony jest obiekt formularza potrzebnej klasy, w którym do kolejnych jego elementów „przypięte” są walidatory. Gdy wystąpi interesujące nas zdarzenie, zwykle jest nim wysłanie formularza metodą „POST”, następuje przekazanie danych do formularza, który sprawdza ich poprawność. Następnie dane są z niego wyciągane i przekazywane do jakiegoś modelu, który już zajmuje się nimi dalej.

Taki typowy flow prezentuje poniższy kod:

class Article_Form extends Zend_Form {

	public function init(){

		$oTitle = new Zend_Form_Element_Text('title');
		$oTitle->addValidator(new Zend_Validator_NotEmpty);

		$this->addElement($oTitle);
//...inne elementy
	}

}

class ArticleController extends Zend_Controller_Action {

	public function createAction(){

		$oForm = new Article_Form();
		if($this->_request->isPost()){
				$aPost = $this->_request->getPost();
				if($oForm->isValid($aPost)){
					$oArticleTable = new Article_Table;
					$oArticleTable->insert($oForm->getValues());

					$this->_redirect('/somepage');
				}

		}
		$this->view->form = $oForm;
	}

//... inne metody

}

Rozwiązanie to świetnie się sprawdza, gdy całe działanie naszej strony sprowadza się do operacji typu CRUD (Create Retrieve Update Delete). Typowym przykładem takiej aplikacji jest dowolny blog, czy prosty firmowy CMS.

Problem z tego typu walidacją pojawia się wtedy, kiedy nasza aplikacja zaczyna robić się skomplikowana. Klient potrzebuje zaimplementować złożona logikę biznesową i działanie naszego softu nie sprowadza się już do przeprowadzania podstawowych operacji na bazie danych. Zadania jakie są przed nim postawione wymagają dogłębnej znajomości procesów biznesowych klienta.

Uczestniczyłem w takim projekcie (ecommerce) dla dużego hurtowego dostawcy artykułów papierniczych. Możecie mi wierzyć albo i nie, ale zamówienie długopisu albo żółtych samoprzylepnych karteczek może być bardzo skomplikowanym procesem, w którym zaangażowane jest kilka osób/użytkowników systemu.

Wróćmy jednak do meritum, czyli naszych walidatorów. Co jest nie tak z kodem, który został zaprezentowany wcześniej ? Otóż wraz ze wzrostem złożoności warunków, które spełniać ma flow naszego kodu, rośnie jego zależność od kontekstu w jakim zostaje wykonywany. Niestety okazuje się, że walidatory, którymi możemy sprawdzać poprawność pojedynczego pola przestają wystarczyć. Oczywiście można napisać taki walidator, który sprawdza poprawność pola w kontekście innych pól naszego formularza. Można też napisać walidator, który sięgnie do bazy danych by sprawdzić jakieś dodatkowe informacje. Zawsze jednak koniec końców, kończymy z wielką zagmatwaną siecią if-ów.

Stosując taką strategię walidacji dorobimy się całego wianuszka walidatorów albo też alternatywnie zaczniemy przenosić część walidacji do akcji kontrolera. Jeżeli jeszcze dodatkowo chcemy wykorzystać dany formularz w innym miejscu systemu, gdzie wygląd formularza jest taki sam, natomiast kontekst jest trochę inny, dołożymy kolejne „ify” i skończymy z nieczytelnym i nierozwiązywalnym węzłem gordyjskim.

Raz już spotkałem się z taką sytuacją i jako, że pośrednio sam się do niej przyczyniłem, postanowiłem poszukać jakiegoś rozwiązania, które pozwoli mi uniknąć takiej sytuacji w przyszłości. Po wielu poszukiwaniach w internecie trafiłem na metodykę DDD (Domain Driven Design). DDD proponuje rozwiązanie opcji walidacji w bardzo elegancki sposób.

Całą idee przestawię prosto na przykładzie aplikacji do rejestrowania pacjentów w placówkach polskiej tzw. „służby zdrowia”.

Encją jak wiemy z moich poprzednich wpisów jest taka klasa obiektów, które mają własną tożsamość. Pacjent bez dwóch zdań tożsamość ma a więc będzie się do nich zaliczał. Value object w naszym przykładzie, będzie to numer PESEL, który unikanie identyfikuje nam pacjentów. Natomiast usługą będzie klasa „RejestrPacjentów”, która jak nazwa mówi, będzie służyła nam do rejestracji pacjentów na wizyty czy też zabiegi. Spójrzmy na kod:

Klasa Pacjent (encja):


class Pacjent {

    private $_pesel;
    private $_firstName;
    private $_lastName;
    private $_birthDate;

    public function __construct(Pesel $pesel, $firstName, $lastName, DateTime $birthDate){
        $this->_pesel = $pesel;
        $this->_setName($firstName, $lastName);
        $this->_birthDate = $birthDate;
    }

    private function _setName($firstName, $lastName){
        //walidacja
    }

    public function pesel(){
        return $this->_pesel;
    }

    public function firstName(){
        return $this->_firstName;
    }

    public function lastName(){
        return $this->_lastName;
    }

    public function lastVisit(){
        //kod
    }
}

Klasa Pesel (Value Object):


final class Pesel {

    private $_pesel;

    public function __construct($pesel){
        //walidacja
        $this->_pesel = $pesel;
    }

    public function  __toString() {
        return $this->_pesel;
    }
}

Klasa RejestratorPacjentow (Usługa):


class RejestratorPacjentow {

    public function zarejestrujWizyte(Pacjent $pacjent, Lekarz $lekarz, DateTime $data){
        //walidacja
    }

    public function zarejestrujZabieg(Pacjent $pacjent, Zabieg $zabieg){
        //walidacja
    }

    public function sprawdzTerminy(DateTime $data){

    }
}

Mamy już cały zarys kodu. Po pobieżnej analizie, pierwsze co rzuca się w oczy to wszechobecne typowanie argumentów funkcji. Dlaczego taki zabieg został zastosowany ? Metodyka DDD mówi, że obiekty występujące w domenie, zawsze powinny mieć poprawny swój wewnętrzny stan, co oznacza, że nie będą akceptować żadnych danych, które nie są poprawne w ich kontekście. Dzięki temu mamy gwarancję, że jeżeli utworzymy nowy obiekt klasy „Pacjent” to będzie on zawsze „valid”.

Przyjrzyjmy się klasie „Pesel”. W konstruktorze zaznaczyłem, że przed ustawieniem swojej wartości następuje walidacja, czy string który jest podany jako argument jest poprawnym numerem PESEL. Jeżeli nie to powinien wyrzucony być wyjątek.

Walidacja występuje również w metodach klasy RejestratorPacjentow, która jak to w przypadku usług bywa nie posiada żadnego wewnętrznego stanu, dlaczego tak jest ? Otóż możliwe jest, że oprócz tego, że obiekty przekazane do metody są wewnętrznie poprawne, to występuje jakiś dodatkowy kontekst operacji, który ma zostać sprawdzony. Natomiast w tych metodach w przeciwieństwie do walidacji w encjach i vo nie zostanie wyrzucony wyjątek, lecz metoda zwróci błąd w postaci czy to tekstowej czy jakiejś tablicy/obiektu z jego szczegółami.

Jak z powyższych „klocków” złożyć działająca aplikację ? Jak mają się one do formularzy oraz informowania użytkowników o błędach? Zilustruje to poniższy kod:

Kontroler:


class RejestratorController extends Zend_Controller_Action {

    public function nowyPacjentAction() {
        $oForm = new Pacjent_Form;

        if($this->_request->isPost()) {
            $aPost = $this->_request->getPost();

            $oFacade = new PrzychodniaFacade;
            $aErrors = $oFacade->nowyPacjent($aPost);

            $oForm->setErrors($aErrors);
        }

        $this->view->form = $oForm;
    }

    public function zarejestrujWizyteAction() {
        $oFacade = new PrzychodniaFacade;
        $oForm = new ZarejestrujWizyte_Form;

        $aData = array(
                'pacjenci' => $oFacade->listaPacjentow(),
                'lekarze' => $oFacade->listaLekarzy()
        );

        $oForm->populate($aData);
        if($this->_request->isPost()) {
            $aPost = $this->_request->getPost();
            $aErrors = $oFacade->zarejestrujWizyte($aPost);
            $oForm->setErrors($aErrors);
        }

        $this->view->form = $oForm;
    }
    //inne metody
}

Fasada:


class PrzychodniaFacade {

    //inny kod

    public function listaPacjentow(){
        return $this->pacjenci->findAll()->toDictionary();
    }

    public function listaLekarzy(){
        return $this->lekarze->findAll()->toDictionary();
    }

    public function zarejestrujWizyte(array $data){
        $oPacjent = $this->pacjenci->find($data['pacjent_id']);
        $oLekarz = $this->lekarze->find($data['lekarz_id']);
        $oDate = new DateTime($data['data']);

        $mResult = $this->rejestrPacjentow->zarejestrujWizyte($oPacjent, $oLekarz, $oDate);
        if(is_array($mResult)){
            return $mResult;
        }
        return array();
    }

    public function nowyPacjent($data){
        try {
            $oPesel = new Pesele($data['pesel']);
            $oBirthDate = new DateTime($data['time']);
            $oPacjent = new Pacjent($oPesel, $data['firstName'], $data['lastName'], $oBirthDate);

            $this->pacjenci->persist($oPacjent);
        } catch(ValidationException $e){
            return $e->toArray();
        }
        //kod
    }
}

Jak widzimy w powyższym przykładzie, Zend_Form również znalazł swoje zastosowanie. Stał się zwykłym komponentem wizualnym, do którego przekazywane są dane w celu prezentacji. Dzięki temu, może być on również wykorzystywany w innych miejscach systemu, ponieważ nie zależy od niego żadna logika aplikacji.

Walidacja następuje natomiast tam, gdzie jest jej miejsce – w modelu, jest ona w końcu częścią logiki aplikacji.

By dopełnić obraz całości – kilka słów o tym, czym i po co jest fasada. Klasa fasady, jest klasą, która rozdziela warstwę kontrolera od warstwy naszej domeny. Jest to swego rodzaju API, by warstwa kontrolera nie musiała być zależna od wielu klas, które występują w naszej domenie, a tylko od jednej, która zajmuje się „tłumaczeniem” z „domenowego” na „kontrolerowy”. Stosowanie fasady ma jeszcze tą zaletę, że dzięki temu, że jej metody przyjmują tylko dane w postaci prostej (tablice i wartości skalarne) nadaje się doskonale by wystawić ją jako web service, zamiast stosowania w kontrolerze.

Podsumowując ten przepastny wpis – zawsze należy dobrać odpowiednie narzędzia do problemu. Klasyczna walidacja przy pomocy Zend_Form sprawdza się gdy mamy do czynienia z prostymi przypadkami. Natomiast, gdy sytuacja robi się złożona, dobrze jest zdecydować się na podejście w stylu DDD, ponieważ jest dużo bardziej czytelniejsze i lepiej odpowiada skali skomplikowanych i dużych aplikacji.

Linki:


Viewing all articles
Browse latest Browse all 10

Latest Images

Trending Articles