Jednym ze wzorców, który pojawia się często w kontekście Domain Driven Design jest wzorzec specyfikacji. Jest to wzorzec, przekształcający reguły biznesowe na logikę Boole’a. Dzięki wzorcowi specyfikacji, możemy w elastyczny sposób sprawdzić, czy dany obiekt spełnia nasze regułu biznesowe.
Tłumacząc go na język interfejsów dostajemy coś takiego:
interface ISpecification {
/**
* @return boolean
*/
public function isSatisfiedBy($candidate);
/**
* @return ISpecification
*/
public function andSatisfiedBy(ISpecification $otherSpec);
/**
* @return ISpecification
*/
public function orSatisfiedBy(ISpecifiaction $otherSpec);
public function not();
}
Metoda isSatisfiedBy
sprawdza czy dany obiekt spełnia warunki reguły. Metoda not()
neguje nam warunek, natomiast metody „andSatisfiedBy” i „orSatisfiedBy” umożliwiają nam łączenie reguł w łańcuchy. Łańcuchy te będą połączone odpowiednimi operatorami logicznymi – „and” albo „or”.
Poniżej zaprezentuje implementacje na przykładzie kawałka wyimaginowanej aplikacji do obsługi poborowych zrealizowanej w metodyce DDD.
Nasz model będzie składał się z trzech klas:
/**
* @entity
*/
class CPoborowy {
protected $_pesel;
protected $_imie;
protected $_nazwisko;
protected $_waga;
protected $_wzrost;
protected $_ksiazeczkaWojskowa;
public function __construct($pesel, $imie, $nazwisko, CWaga $waga, CWzrost $wzrost){
$this->_pesel = $pesel;
$this->_imie = $imie;
$this->_nazwisko = $nazwisko;
$this->_waga = $waga;
$this->_wzrost = $wzrost;
}
public function getWzrost(){
return $this->_wzrost;
}
public function getWaga(){
return $this->_waga;
}
public function zrobJaskolke(){
echo 'robie jaskolke';
}
public function odbierzKsiazeczkeWojskowa(CKsiazeczkaWojskowa $kw){
if(empty($this->_ksiazeczkaWojskowa)){
$this->_ksiazeczkaWojskowa = $kw;
return 'Dziękuje uprzejmie';
}
return 'Mam juz ksiazeczke';
}
}
Klasa „poborowy” reprezentuje nam osobę poborowego, który określony jest kilkoma parametrami ważnymi dla członków komisji uzupełnień – imię, nazwisko, wzrost i waga. Posiada konstruktor, dwa getter’y oraz metody „odberzKsiazeczkeWojskowa” oraz „zrobJaskolke”, które są niezbędne dla logiki biznesowej rekrutacji do wojska .
Jak pewnie zauważyliście, w konstruktorze przy parametrach „wzrost” i „waga” mamy typehint’y na CWzrost i CWaga.
/**
* @value
*/
class CWzrost {
protected $_wartosc;
protected $_jednostka;
public function __construct($wartosc, $jednostka){
$jednostka = strtolower($jednostka);
if($wartosc <= 0){
throw new InvalidArgumentException('Wzrost musi byc wiekszy od 0!');
}
if(!in_array($jednostka,array('cm','mm','m'))){
throw new InvalidArgumentException('Dozwolone jednostki to cm, mm i m');
}
$this->_wartosc = $wartosc;
$this->_jednostka = $jednostka;
}
public function __toString(){
return $wartosc.' '.$jednostka;
}
}
/**
* @value
*/
class CWaga {
protected $_wartosc;
protected $_jednostka;
public function __construct($wartosc, $jednostka){
$jednostka = strtolower($jednostka);
if($wartosc <= 0){
throw new InvalidArgumentException('Waga musi byc wiekszy od 0!');
}
if($jednostka != 'kg'){
throw new InvalidArgumentException('Dozwolona jednostka to kilogram');
}
$this->_wartosc = $wartosc;
$this->_jednostka = $jednostka;
}
public function __toString(){
return $wartosc.' '.$jednostka;
}
}
Obie klasy są tzw. ValueObject’s. W odróżnieniu od poborowego, który jest encją i posiada swój unikalny identyfikator w postaci książeczki wojskowej czy pesel-u, wzrost i waga są bytami nieidentyfikowalnymi – można je rozróżnić tylko po wartości, ubranie tych wartości w klasy ma za zadanie:
- wyróżnienie ich jako ważnej części języka domeny i modelu
- walidacje
gdy przyjrzymy się głębiej, widzimy, że w konstruktorach obu klas zawarta jest logika walidacji – zarówno waga jak i wzrost nie mogą być mniejsze od zera, są też sprawdzane jednostki obu wielkości, w końcu nasza komisja poborowa jest w Polsce gdzie mamy jednostki układu SI.
Przejdźmy jednak do sedna, czyli wzorca specyfikacji. Załóżmy, że komisja poborowa kwalifikuje do wojska tylko poborowych o wzroście równym 180 cm i wadze 80 kg. Te dwa warunki tworzą naszą specyfikacje, implementacja będzie wyglądać tak:
class CWzrostSpec extends ASpecification {
protected $_wzrost;
public function __construct($wzrost){
$this->_wzrost = $wzrost;
}
public function isSatisfiedBy($candidate){
if($candidate->getWzrost() == $this->_wzrost){
if(isset($this->_and)){
return $this->_and->isSatisfiedBy($candidate);
}
return true;
} elseif(isset($this->_or)) {
return $this->_or->isSatisfiedBy($candidate);
}
return false;
}
}
class CWagaSpec extends ASpecification {
protected $_waga;
public function __construct($waga){
$this->_waga = $waga;
}
public function isSatisfiedBy($candidate){
if($candidate->getWaga() == $this->_waga){
if(isset($this->_and)){
return $this->_and->isSatisfiedBy($candidate);
}
return true;
} elseif(isset($this->_or)) {
return $this->_or->isSatisfiedBy($candidate);
}
return false;
}
}
Przykład użycia:
//okreslamy specyfikacje
$oSpec = new CWzrostSpec(new CWzrost(180,'cm'));
$oSpec->andSatisfiedBy(new CWagaSpec(new CWaga(80,'kg')));
//tworzymy poborowych
$oPoborowy1 = new CPoborowy('123456','Franek', 'Gondala', new CWaga(80,'kg'), new CWzrost(180,'cm'));
$oPoborowy2 = new CPoborowy('789123', 'Max', 'Perreira', new CWaga(72,'kg'), new CWzrost(176,'cm'));
//sprawdzamy czy poborowi spelniaja nasze kryterium
var_dump($oSpec->isSatisfiedBy($oPoborowy1));
var_dump($oSpec->isSatisfiedBy($oPoborowy2));
Dzięki zastosowaniu wzorca specyfikacji, gdy komisja poborowa dostanie nowe wytyczne z Ministerstwa Obrony, będzie można w łatwy sposób zmienić lub dołożyć nowe warunki.
Jak widzimy wzorzec specyfikacji przydaje się przy walidacji obiektów, do walidacji formularzy wystarczyło by jeszcze dodanie metody, która dała by jakąś zwrotną informację o tym, w którym miejscu obiekt nie spełnia specyfikacji. Ponadto wzorca specyfikacji używa się jeszcze w kontekście budowania zapytań do bazy danych a dokładniej do repozytorium. Jak wspomniałem we wcześniejszych artykułach z serii o DDD, repozytorium, jest klasą, która pozwala nam wyszukiwać wcześniej utrwalone obiekty wg pewnych kryteriów. Zamiast więc w klasie repozytorium tworzyć metody typu „findByFirstName”, „findByLastName”, „findBy[a-z0-9_]+” można mieć jedną metodę „find”, która jako argument będzie pobierała obiekt specyfikacji i na jego podstawie tworzyła zapytanie do bazy danych.
Jak zwykle czekam na wszelki feedback .
Czytaj dalej tutaj (rozwija treść wpisu)
Czytaj dalej na blogu autora...
Zwiń
Czytaj na blogu autora...