Pokazywanie postów oznaczonych etykietą Domain Specific Language. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą Domain Specific Language. Pokaż wszystkie posty

wtorek, 6 października 2009

Domain Specific Query

Zgodnie z obowiązującym trendem polegającym na prześciganiu się w publikowaniu bardziej lub jeszcze_bardziej oczywistych odkryć - dziś kolej na mnie i na coś na co wpadłem już jakiś czas temu.

Tytułowy wynalazek ma roboczą nazwę DSQ, którą triumfalnie nadałem mu kojarząc dwie modne techniki: Domain Specific Language i Command-query Separation.

NIEKRÓTKIE WYJAŚNIENIE
O CQS pisałem 2 miesiące temu w >tym< poście. W skrócie jest to styl architektoniczny, który zakłada separację API systemu na dwie wyraźne części. Jedna obsługuje (przyjmuje wysyłane do API) Command - rozkazy wykonania operacji. Druga natomiast obsługuje Query - obiekty specyfikujące kryteria pobierania/wyszukiwania danych.

W dzisiejszym poście skupimy się właśnie na tej drugiej części, czyli ogólnie mówiąc na wyszukiwarkach danych. Wyszukiwarki są dosyć proste - ot zwykłe servisy, pod którymi nie znajdują się już żadne kolejne warstwy - chyba, że potrzebujemy jeszcze "dodać coś od siebie" do pobranych z pewnego źródła danych.


DSL to koncepcja dużo bardziej popularna (w sensie ilości publikacji) niż CQS, więc nie będę się zbytnio rozpisywał na jej temat. Ogólnie rzecz ujmując chodzi o to aby kod logiki biznesowej czytać niemal jak język naturalny. Definiujemy sobie język zorientowany na konkretną domenę biznesową a później to już fraszka - program piszę się niemal sam;)
W statycznych językach typu Java nie mamy zbyt szerokiego pola do popisu jeżeli chodzi o zdefiniowanie własnego języka domenowego.

Jedyne co możemy zrobić w Javie to zastosować Fluent Interface i opisowe (samoopisujące wręcz) nazwy metod. Czyli jest to jedynie namiastka DSL, ale cieszy:)

Koncepcja Fluent Interface jest prosta: chodzi o to aby metody danej klasy zwracały instancję obiektu na rzecz którego zostały wykonane. Dzięki temu możemy zamiast pisać tradycyjnie:

Klasa obiekt = new Klasa();
obiekt.metoda1();
obiekt.metoda2();
obiekt.metoda3();

możemy zmienić styl na bardziej fluent:

Klasa obiekt = new Klasa();
obiekt.metoda1().metoda2().metoda3();

Jeżeli teraz zamiast nadawać metodom autystyczne nazwy (w stylu Bruce Eckela), wykażemy się odrobiną inwencji i słowiańskiej fantazji możemy pisać niemalże poetycko:

FabrykaWierszy.stworzUtwor().subtelny().zNutkaDekadencji().rymyNiechBedaCzestochowskie().strof(10);

Chyba lepiej czyta się kod w takim stylu...


DO RZECZY
Po tym przydługim wstępie powinniśmy już mniej więcej czuć czym jest CQS i DSL...

Wyobraźmy sobie następujący problem :

W systemie przechowujemy dokumenty. Dokumenty mają szereg atrybutów: kilka typów statusów (związanych z przepływem dok. jak i technicznych), różnego rodzaju daty, przypisanie do różnych zasobów itp.

Domena biznesowa standardowa i może niezbyt wyszukana, ale dzięki temu wiadomo o co chodzi i możemy skupić się na samej technice.

Serwis, który miałby wyszukiwać dokumenty musiałby mieć metodę o kilkunastu parametrach. Pierwszym problem z taką metodą to wysoki ujemny współczynnik czytalności. Drugi problem to ciągłe zmiany sygnatury metody gdy pojawiają się nowe atrybuty dokumentu.

Zatem dosyć oczywiste jest, że stworzymy sobie klasę przechowującą atrybuty wyszukiwania a nasz serwis będzie miał metodę, do której przekazujemy paczkę kryteriów - po prostu jakiś rodzaj Query z CQS:


public class SearchDocumentsQuery implements Serializable{
//atrybuty wyszukiwania + get/set
}



public class DocumentsFinder{
public Collection<Document> search(SearchDocumentsQuery criteria){
//..
}


Jak do tej pory standard. (Query najpewniej będzie prędzej czy później serializowany.)

Teraz idąc dalej wg utartych szablonów dodalibyśmy do SearchDocumentsQuery kilkanaście pół prywatnych oraz oczywiście nasze ulubione gettery i settery.

Kod klasy klienckiej przy pomocy setterów ustawia kryteria wyszukiwania a kod metody DocumentsFinder.search przy pomocy getterów pobiera te parametry i klei sobie zapytanie w SQL, HQL tudzież mój ulubiony Hibernate Criteria API.

A gdzie miejsce dla DSL? No więc właśnie przy tak ordynarnym podejściu nie uświadczymy.
Załóżmy teraz, że chcemy hermetyzować kod klas klienckich (wywołujących DocumentsFinder)od szczegółów atrybutów Dokumentu.

Klient zamiast operować szczegółami:

SearchDocumentsQuery criteria = new SearchDocumentsQuery();
criteria.setStatus(Status.ACTIVE);
criteria.setCreateDate(...)
criteria.set...
...

Collection<Document> result = documentsFinder.search(criteria);


Wyraża jedynie intencje co do chęci otrzymania dokumentów spełniających zestaw określonych specyfikacji biznesowych:

SearchDocumentsQuery criteria = new SearchDocumentsQuery();
criteria.current().contains("lorem ipsum");

Collection<Document> result = documentsFinder.search(criteria);


Gdzie przykładowo metody DSL wyglądają mniej więcej tak:


public class SearchDocumentsQuery implements Serializable{
private Status status;
private Date epiryDate;
private String[] titleWords;
private String[] contentWords;

public SearchDocumentsQuery current(){
status = Status.ACTIVE;
expiryDate = //data jutrzejsza
return this;
}

public SearchDocumentsQuery contains(String phrase){
String[] words = phrase.split(" ");//lub bardziej wyrafinowy split
titleWords = words;
contentWords = words;
return this;
}

}


Jak interpretować powyższe metody ustawiające kryteria:
current - oznacza, ze życzymy sobie dokumentów będących aktualnie w obiegu.
Co to znaczy od strony modelu: że maja określony status i że wygasają np najwcześniej jutro.

contains - oznacza, że dokument ma zawierać określoną frazę. Z punktu widzenia modelu oznacza to, że tytuł lub treść dokumentu zawiera wszystkie słowa frazy.

Cała idea polega na tym, że klient nie wie nic o polach status, expiryDate, words, ...
Klient wyraża intencję.

Natomiast pola te są dostępne przez gettery dla DocumentsFinder, który to analizuje je i odpowiedzi składa zapytanie. Aby osiągnąć pożądaną widoczność getterów należałoby zdefiniować SearchDocumentsQuery jako klasę statyczną wewnętrzną w DocumentsFinder. Wówczas DocumentsFinder będzie widział metody prywatne, bo takie powinny być rozważane gettery. Statyczną ponieważ chcemy tworzyć Query bez "wywodzenia" jej z instancji Findera.



public class DocumentsFinder{

public static class SearchDocumentsQuery implements Serializable{
private Status status;
private Date epiryDate;
private String[] titleWords;
private String[] contentWords;

//metoda contains i current - pominięto

private Status getStatus(){
return status;
}
}

public Collection<Document> search(SearchDocumentsQuery criteria){
if (criteria.getStatus() != null)//wywołanie prywatnej metody
//dołożenie do zapytania warunku na status
...
}
}



Kuszące może wydawać się dodanie do SearchDocumentsQuery metody buildEecutable(), która produkuje na podstawie stanu swoich wewnętrznych pól (statys, words,...) coś co można wykonać. Tym "czymś" może być PreparedStatement z JDBC, Criteria z Criteria API Hibenrate, Query z JPA, czy zwykły String zawierający SQL albo HQL.

Ale zastanówmy się: czy klasa, która jest widoczna przez klienta (np zdalnego) powinna mieć w sobie kod, który operuje na jakimś API np Hibernate (Criteria API) albo czy powinna wiedzieć coś o jakimś SQL? Przecież Finder może szukać w XMLu lub poprzez web service. Klient nie może tego wiedzieć - ba nie powinien.

//====================

Pomysł może mało odkrywczy i zapewne wiele osób wpadło na niego równolegle, ale jeszcze nie spotkałem nigdzie opisu.

Pomysł oczywiście nie będzie miał zastosowania w aplikacjach z małą abstrakcją pomiędzy bazą danych a GUI - tak zwanych przeglądarkach danych. W tego typu aplikacjach kontrolki na GUI (w tym pola filtrujące) odpowiadają zwykle niemal dosłownie kolumnom w bazie. W tego typu przypadkach próba abstrahowania od parametrów modelu jest niepotrzebnym uprawianiem dyscypliny pod tytułem nakładanie gaci przez głowę;)

Czekam na opinie, sugestie, krytykę - może być niekonstruktywna:)