Wzorzec projektowy Filtr i Specyfikacja

Wszyscy znamy wzorce projektowe. W książe i wikipedii opisano ich sporo. Ale czy to koniec? Czy nie ma innych ciekawych i wartych omówienia wzorców? Oczwiście są. Dziś zajmiemy się jednym z mało znanych wzorców czyli wzorcem Filtr zwanym też Specyfikacja.

Filtrowanie list pojawia się prawie wszędzie. Praktycznie w każdym systemie mamy jakieś zestawy obiektów, które wyświetlamy bazując na filtrach. Większość programistów robi to „na czuja” lub korzysta z gotowych rozwiązań. Tym czasem mamy bardzo ciekawy wzorzec pomagający filtrować dane.

Wzorzec opiszę na przykładzie bazy zawierającej np. pojazdy. Wiemy jak wyglądają samochody, autobusy – będzie łatwo wyobrazić sobie działanie filtra. Przykłady będą w PHP. Zakładamy istnienie klasy „Car” definiującej pojazd.

Stary sposób filtrowania

W wielu systemach tworzymy klasę podającą nam zestawy obiektów. Taka klasa dla naszego przykładu wyglądałaby tak:

class Cars {
   
   // pobierz wszystkie auta pasujące do nazwy
   public function getCarsByName($name) {}
   
   // pobierz wszystkie auta danego modelu
   public function getCarsByModel($model) {}
}

Gdy system się rozrasta, samochody robią się coraz bardziej skomplikowane. Będziemy dodawać do tej klasy kolejne metody, np. szukanie wg rodzaju silnika, liczby pasażerów itp. Z czasem pojawią się metody szukające według kilku warunków:

class Cars {
   
   // pobierz wszystkie auta pasujące do nazwy
   public function getCarsByName($name) {}
   
   // pobierz wszystkie auta danego modelu
   public function getCarsByModel($model) {}
   
   // pobierz auta według rodzaju silnika
   public function getCarsByEngine($type) {}
   
   // pobierz auta według liczby pasażerów
   public function getCarsByPassangersCount($passangers) {}
   
   // pobierz według modelu i nazwy
   public function getCarsByModelAndName($model,$name) {}
   
   // pobierz według modelu, nazwy i silnika
   public function getCarsByModelAndNameAndEngine($model,$name,$type) {}
}

Lista metod rośnie. Gdy chcemy kolejny warunek musimy dopisać metodę. Klasa robi się coraz bardziej zawiła, o błędy coraz łatwiej.

Tworzenie filtrów

Rozwiązaniem jest właśnie wzorzec projektowy „Filtr”. W tym wzorcu wyrzucamy na zewnątrz wszystkie warunki wyszukiwania. Sama klasa zajmuje się tylko obsługą samochodów.

Na początek stworzymy sobie interfejs. Ten interfejs dopasowuje pojedynczy samochód do jakiegoś (zdefiniowanego później) warunku:

interface MatchCar {
   // jedyna funkcja mówi nam, czy samochód pasuje do warunku
   // zwraca true/false
   public function Match($car);
}

Teraz nasza klasa z autami będzie wyglądała prościej (korzystam z generatora PHP czyli komenty yield dla uproszczenia kodu):

class Cars {
   
   // pobierz auta według zadanego filtra
   public function getByFilter($filter) {
   
      // filtrujemy wewnętrzną listę samochodów
      foreach ($this->allCars as $car) {
         if ($filter->Match($car)) yield $car;
      }
   
   }
}

W ten sposób załatwiliśmy wszystkie możliwe wyszukania według dowolnego warunku. Zostało tylko napisanie klasy sprawdzającej warunek. Np. dla nazwy auta:

class MatchName implements Match {
   
   var $name;
   
   // podczas tworzenia warunku musimy podać nazwę, do której dopasujemy
   public function __construct($name) {
      $this->name = $name;
   }
   
   // sprawdzanie czy auto pasuje do naszego warunku
   public function Match($car) {
      return ($car->getName() == $this->name);
   }
}

Podobnie utworzymy klasę z warunkiem dla modelu (MatchModel), silnika (MatchEngine) i inne. Możemy sprawdzać, czy samochód jest osobowy (przewozi nie więcej niż 9 osób) według liczby pasażerów itp.

Jeśli teraz zechcey wyszukać auta według nazwy, wystarczy stworzyć obiekt z warunkiem. Pełny kod:

// nasz obiekt z samochodami
$cars = new Cars();
   
// tworzymy warunek szukający samych Opli
$opel = new MatchName("Opel");
   
// a teraz czas na wyszukiwanie
$allOpelCars = $cars->getByFilter($opel);
   
// ... i wypisujemy
foreach ($allOpelCars as $car) {
   printf("%s %s\n",$car->getName(),$car->getModel());
}

Powyższy kod powinien wypisać nam nazwy wszystkie ople wraz z nazwami modelu.

Jak połączyć warunki

Na razie możemy wyszukiwać według jednego warunku. Nie możemy znaleźć samochdów o wybranej nazwie i modelu (AND), wybrać dwóch różnych modeli (OR) albo znaleźć wszystkie auta oprócz jednego (NOT). Żeby to zrobić stworzymy trzy klasy łączące warunki.

Klasa MatchAnd zamiast tekstu (jak wyżej) będzie przyjmowała w konstruktorze dwa warunki do połączenia:

class MatchAnd implements MatchCar {
   
   var $match1, $match2;
   
   // konstruujemy obiekt podając mu dwa warunki (obiekty o interfejsie MatchCar)
   public function __construct($match1, $match2) {
      $this->match1 = match1;
      $this->match2 = match2;
   }
   
   // dopasowanie to sprawdzenie czy spełnione są oba warunki
   public function Match($car) {
      return ($this->match1->Match($car) AND $this->match2->Match($car));
   }
   
}

Klasa MatchOr będzie prawie identyczna. Jedynie operator AND zmieni się na OR. Za to klasa MatchNot będzie miała tylko jeden warunek w parametrze:

class MatchNot implements MatchCar {
   
   var $match;
   
   // konstruujemy obiekt podając mu warunek (obiekt o interfejsie MatchCar)
   public function __construct($match) {
      $this->match = $match;
   }
   
   // dopasowanie to sprawdzenie czy nie spełniony jest warunek
   public function Match($car) {
      return (!$this->match->Match($car));
   }
   
}

Możemy teraz wykorzystać te klasy do budowania złożonych warunków. Szukamy auta według marki i modelu:

$opel = new MatchName("Opel");
$model = new MatchModel("Frontera");
   
$opelFrontera = new MatchAnd($opel,$frontera);

Albo prościej bez zmiennych pośrednich. Jeśli chcemy znaleźć wszystkie auta za wyjątkiem Renault:

$allButRenault = new MatchNot(new MatchName("Renault"));

Takie warunki podajemy naszej wcześniejszej metodzie filtrującej:

// a teraz czas na wyszukiwanie
$myCars = $cars->getByFilter($opelFrontera);
$otherCars = $cars->getByFilter($allButRenault);

Wady i zalety

Zaleta – mamy przejrzysty kod, który łatwo testować. Testy jednostkowe pokazują swoją siłę na obiektach z interfejsem MatchCar. Możemy też łatwo dodawać nowe warunki filtrowania.

Wada – musimy pobrać i stworzyć wszystkie obiekty filtrowane. Jeśli utworzenie obiektu jest kosztowne to wykonujemy dużo pracy niepotrzebnie. Wiele stworzonych obiektów jest potem odrzucanych. W typowym przypadku gdy mamy bazę SQL wczytanie wszystkich rekordów z tabeli, utworzenie obiektów aby następnie odrzucić wiele z nich jest ogromnym marnotrawstwem czasu i pracy systemu.

Wyobrażamy sobie jednak system, gdzie nie mamy dostępu do danych inaczej niż przez pobranie obiektów. Nie możemy wcześniej poznać własności naszych rekordów. Jeśli np będzie to baza rozproszonych zasobów RESTowych, które na dodatek nie nadają sie do trzymania w cache to tu nasz filtr ma zastosowanie.

Gdy przejrzystość i poprawność systemu ma priorytet przed czasem działania tez możemy użyć tego wzorca. Jeśli czas dostarczenia oprogramowania jest dużo ważniejszy od jego optymalnosci to tez warto pamietać o wzorca filtr.

Wzorzec oczywiście nie nadaje sie do wielkich danych. Możemy jednak potraktować go jako wzorzec rozproszony. Obiekty warunków filtrowania można rozesłać do podsystemów zajmujących się częścią listy. Podsystemy przefiltrują dane u siebie po czym zwrócą nam listy, które połączymy w jedną całość.

Co dalej?

Ten wzorzec projektowy ma kilka nazw. Czasem znajdziemy go pod nazwą „Filtr” (Filter) gdzie klasy dopasowujące nazywane są kryteriami (Criteria). Można też trafić na wzorzec „Specyfikacja” (Specification) gdzie zamiast kryteriów mówimy o specyfikacjach warunków biznesowych. Jest to ten sam wzorzec zaaplikowany na listach obiektów.

Wzorzec kładzie duży nacisk na jasność zarzadzania kryteriami wyszukiwania. Dlatego czasem nazywany jest też wzorcem „kryteria”.

Filtrowanie listy różnorodnych obiektów tez jest możliwe. Możemy filtrować obiekty z jednej rodziny (dziedziczące ze wspólnej klasy) lub nawet mające wspólny interfejs.

Wzorca filtr nie ma ani w książce wzorców ani w Wikipedii. To jeden z mniej znanych wzorców, który warto znać i przypomnieć sobie gdy następnym razem będziemy mieć do czynienia z filtrowaniem danych.

2 myśli na temat “Wzorzec projektowy Filtr i Specyfikacja”

  1. chyba mały kruczek się wkradł:
    $filter.Match($car)

    wszędzie indziej wywołujesz metody przez operator „->” a tutaj przez kropkę.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *