Zamiast rozwodzić się nad definicjami i pojęciami zacznę od pewnego problemu, z którym się spotkałem. Jakiś czas temu do generowania raportów używaliśmy biblioteki JasperReports. To naprawdę rewelacyjna biblioteka, której ideę działania przedstawia Rysunek 1:
Przy całej swej wspaniałości korzystanie z tej (i pewnie z innych) biblioteki do generowania raportów ma następujące konsekwencje:
- Szybko generuje się tylko płaskie raporty czyli takie, w których wynikowy raport można „pociąć” na części generowane z jednego szablonu np. spis treści, lista płac, podsumowanie. Jeśli chcielibyśmy zagnieżdżać raport w raporcie, to oczywiście JasperReports daje taką możliwość, ale o całe przygotowanie danych, odpowiednią konfigurację definicji wyglądu i dostarczanie silnikowi danych w odpowiednich porcjach należy zadbać własnoręcznie.
- Jeżeli raport musi odwzorowywać wiedzę dziedzinową, np. raportowane są wyniki testu IQ i związana jest z tym złożona merytoryka, to albo bardzo skomplikowane jest przygotowywanie danych a konfiguracja definicji wyglądu w miarę prosta albo odwrotnie. Trudno znaleźć złoty środek.
- Ubogacanie raportów wykresami jest możliwe, lecz bardzo pracochłonne.
- Jeśli automatyzowane są raporty, które do tej pory były tworzone ręcznie, to...czeka nas baaaardzo dużo pracy.
Framework raportowy
Powstał pomysł, aby problemom zaradzić. Stworzone rozwiązanie powinno zapewniać następujące funkcjonalności:
- Definiowanie fragmentów raportu (podraportów), które można ze sobą łączyć sekwencyjnie (jeden za drugim) lub poprzez agregację (jeden w drugim)
- Raport można zasilać danymi z: zapytania SQL, zapytania MDX, wyrażenia XPath lub Expression Language
- Raport można wzbogacić wykresem z dostępnego zbioru wykresów.
- I najważniejsze – wszystko wyżej wymienione powinno odbywać się deklaratywnie.
Została stworzona nadbudówka (warstwa abstrakcji – uwielbiam to wyrażenie!) na JasperReports, która pozwoli deklaratywnie tworzyć raporty. Jak zatem opisywać (deklarować) raporty tak, aby sprostać wymaganiom? Wybór padł na XML. Rysunek 2 prezentuje szkic rozwiązania.
Pojawiły się nowe elementy całego rozwiązania: plik XML, w którym będzie deklarowany raport (a konkretnie podraporty oraz zależności pomiędzy nimi) oraz nadbudówka, która przejmuje kontrolę nad silnikiem JasperReports. Oczywiście pojawia się dodatkowa warstwa pośrednicząca pomiędzy klientem, który chce wygenerować raport, a biblioteką. Wszystko ma swoją cenę. W tym przypadku prostota tworzenia raportów kosztuje dodatkową pracę przy implementacji warstwy pośredniczącej.
Spójrzmy na zamieszczony przykład pliku opisującego raport (na potrzeby artykułu nieco uproszczonego).
<reportConfiguration>
<datasources>
<datasource name="hsql" default="true" type="sql">
<property name="driver" value="org.hsqldb.jdbcDriver" />
<property name="url" value="jdbc:hsqldb:hsql://localhost/mydb" />
<property name="username" value="sa" />
<property name="password" value="" />
</datasource>
</datasources>
<tableOfContents enabled="true" position="begin" designFile="tableOfContents.jrxml" />
<subreports>
<subreport type="static" name="introduction" designFile="introduction.jrxml" />
<subreport type="sql" name="employeesList" designFilePath="employeesList.jrxml"
query="select * name,surname from emplyees" />
</subreports>
</reportConfiguration>
Pojawiły się tu słowa (w postaci znaczników XML) jak reportConfiguration, tableOfContents, subreport, desigFilePath. Nie są to słowa związane z językiem programowania, lecz przeznaczone do opisu jednego konkretnego problemu – deklaratywnego tworzenia raportów. Przy użyciu XML zdefiniowaliśmy język opisu raportów, język opisu specyficznego problemu – Domain Specific Language (DSL).
DSL - implementacja oparta o XML
Z punktu widzenia użytkownika opisującego raport XML jest wygodnym rozwiązaniem. W tym przypadku użytkownikami byli programiści, którzy dodawali funkcjonalność generowania raportów do systemów nad którymi pracowali. Ale czy takie rozwiązanie jest najlepsze z perspektywy całego zespołu, w tym grupy odpowiedzialnej za rozwijanie Framework raportowego? Aby odpowiedzieć na to pytanie należy przyjrzeć się sposobowi zaimplementowania tego rozwiązania.
Pierwszym krokiem jest odczyt konfiguracji z pliku XML. Wygląda to na dość żmudne zadanie. Ale zauważmy, że składnia XML ma charakter obiektowy. Znacznik odpowiada obiektowi, a atrybuty znacznika, atrybutom obiektu. Powstaje pytanie: czy istnieje biblioteka pozwalająca automatycznie przekształcić plik XML do odpowiadającego mu modelu obiektowego? Istnieje – Apache Betwixt (być może są też inne). Rozwiązanie przybiera następującą postać ja na Rysunek 3.
Każdemu znacznikowi z pliku XML odpowiada klasa Java. Biblioteka Betwixt jest odpowiedzialna za przekształcenie struktury XML do analogicznej struktury klas. Aby wykonać to zadanie potrzebna jest dodatkowa konfiguracja. W przypadku tej biblioteki konfigurowanie polega na utworzeniu dla każdej klasy modelu obiektowego pliku .betwixt, w którym zawarte są informacje o mapowaniach znacznik XML – obiekt. Rozwiązanie to przedstawia Rysunek 4.
Zauważmy, że struktura obiektowa odpowiada strukturze pliku XML (Listing 1). DSL w postaci pliku XML ma następujące zalety:
- Jest intuicyjny i jego utrzymywaniem mogą zajmować się nieprogramiści;
- Opis raportu jest oddzielony od kodu aplikacji;
- Można stworzyć elastyczne reguły walidacji składni w oparciu o plik XSD;
- Oparcie na standardzie XML daje możliwość edytowania pliku w już istniejących narzędziach WYSIWYG lub stworzenia nowego narzędzia do graficznego opisywania raportu;
- Konwersja do nowej wersji DSL jest łatwa do zaimplementowania, np. za pomocą transformat XSLT lub po stronie języka Java.
Jednocześnie to rozwiązanie ma następujące wady:
- Występuje dodatkowa warstwa pośrednicząca w postaci biblioteki Betwixt;
- Framework korzysta już z 3 rodzajów konfiguracji:
- Pliki jrxml – wymagane przez bibliotekę JasperReports definiują wygląd graficzny podraportów;
- Pliki betwixt – wymagane przez bibliotekę Betwixt mapują znaczniki XML na obiekty Java;
- Plik konfiguracyjny tworzonego frameworka – opisuje zależności pomiędzy podrap ortami, jest to punkt centralny naszego rozwiązania;
- Podczas dodawania nowych funkcjonalności najbardziej pracochłonne są modyfikacje związane z plikiem konfiguracyjnym: mapowania, konwersje danych, obsługa błędów;
- Ze względu na narzut prac nad plikiem konfiguracyjnym czas oczekiwania na nową wersję biblioteki jest dość długi;
- Refaktoryzacja kodu na styku kod Java plik XML jest żmudna i trudna do przeprowadzenia;
- Testowanie konfiguracji XML jest niewygodne – bardzo szybko rozmnażają się testowe pliki konfiguracyjne zawierające różne wersje konfiguracji do przetestowania;
- Refaktoryzacja testów jest bardzo pracochłonna ze względu na istnienia bardzo wielu plików konfiguracyjnych;
DSL - implementacja oparta o Fluent Interface
W poszukiwaniu alternatywy do implementacji DSL we frameworku raportowym przyjrzyjmy się koncepcji Fluent Interface. Fluent Interface to sposób projektowania API, który łatwo można zobrazować na przykładzie biblioteki JMock.
mock.expects(once()).method("method").withAnyArguments().isVoid();
Trudno o bardziej trafną nazwę niż Fluent Inteface. Wywołania metod można przeczytać jak zdanie w języku naturalnym. Sam koncept nie jest nowy, ja doszukiwałbym się korzeni w kaskadowym korzystaniu ze strumieni w C++ za pomocą operatorów >.
Zaprojektujmy zatem Fluent API do konfiguracji raportu. Powinno ono spełniać następujące założenia:
- Wywołania kaskady metod czyta się jak zdanie w języku naturalnym;
- API powinno sugerować właściwą kolejność wywoływania metod.
- Nowe API nie powinno ingerować w istniejący model obiektowy.
Na początek zajmijmy się klasą DatasourceBean. Potrzebujemy udostępnić metody o odpowiednich nazwach, które będą konfigurowały obiekty tej klasy. Jednocześnie API nie powinno ingerować w już istniejące rozwiązanie. Wymóg separacji API od modelu reprezentującego opis raportu wynika z tego, że nie ma potrzeby, aby obiekty wiedziały w jaki sposób są konfigurowane. W terminologii angielskiej używane jest przy tej okazji słowo agnostic.
Rozwiązaniem spełniającym powyższe kryteria jest wzorzec budowniczego
Odpowiedzialnością klasy DatasourceBeanBuilder jest udostępnienie Fluent API do konfigurowania obiektu klasy DatasourceBean. Praca z API mogłaby wyglądać następująco:
builder.isSqlDatasource()
.withDriver( "org.hsqldb.jdbcDriver" )
.withUrl( "jdbc:hsqldb:hsql://localhost/mydb" )
.withUsername( "sa" )
.withPassword( "" )
.isDefault();
Zapewne domyślasz się, że sztuczka pozwalająca na kaskadowe wywoływanie metod polega na zwracaniu referencji do budowniczego w każdej metodzie. Poniżej implementacja tego budowniczego.
public class DatasourceBuilder {
private DatasourceBean bean = new DatasourceBean();
public DatasourceBuilder withUsername(String username) {
bean.addProperty( new PropertyBean("username", username) );
return this;
}
public DatasourceBuilder withPassword(String password) {
bean.addProperty( new PropertyBean("password", password) );
return this;
}
public DatasourceBuilder withUrl(String url) {
bean.addProperty( new PropertyBean("url", url) );
return this;
}
public DatasourceBuilder withDriver(String driver) {
bean.addProperty( new PropertyBean("driver", driver) );
return this;
}
public DatasourceBuilder isSqlDatasource() {
bean.setType( "sql" );
return this;
}
public DatasourceBuilder isDefault() {
bean.setDefaultDatasource( true );
return this;
}
}
Dla każdej klasy modelu obiektowego będzie istniał budowniczy odpowiedzialny za udostępnienie API do konfiguracji obiektu (Rysunek 6).
Jednym z założeń projektowanego interfejsu było, aby sugerował on programiście kolejność wywoływania metod. Osiągnęliśmy to poprzez wprowadzenie wielu budowniczych. W ten sposób użytkownik, w danym momencie pracuje tylko z jednym budowniczym i ma dostęp do metod, które mają sens w danym kontekście.
Centralnym punktem API jest ReportConfigurationBeanBuilder, który udostępnia użytkownikowi bardziej szczegółowych budowniczych za pomocą swoich metod (Rysunek 7).
Przy obecnym rozwiązaniu użytkownik korzysta z API w następujący:
ReportBuilder report = new ReportBuilder();
report.withMasterDesing( "master.jrxml" )
report.newDataSource( "hsql" )
.isSqlDatasource()
.withDriver( "org.hsqldb.jdbcDriver" )
.withUrl( "jdbc:hsqldb:hsql://localhost/mydb" )
.withUsername( "sa" )
.withPassword( "" )
.isDefault();
report.withTableOfContents()
.designFrom( "tableOfContents.jrxml" )
.atTheBegining();
report.addStaticSubreport( "introduction" )
.designFrom( "introduction.jrxml" )
report.addSqlSubreport( "employeeList" )
.designFrom( "employeeList.jrxml" )
.withQuery( "select * name,surname from emplyees" )
.getDataFrom( "hsql" );
Możemy połączyć konfigurację w jeden ciąg wywołań dodając do każdego budowniczego metodę
public ReportBuilder and()
. W ten sposób możliwe jest korzystanie z API w następujący sposób:
ReportBuilder report = new ReportBuilder();
report .withMasterDesing( "master.jrxml" )
.newDataSource( "hsql" )
.isSqlDatasource()
.withDriver( "org.hsqldb.jdbcDriver" )
.withUrl( "jdbc:hsqldb:hsql://localhost/mydb" )
.withUsername( "sa" )
.withPassword( "" )
.isDefault()
.and()
.withTableOfContents()
.designFrom( "tableOfContents.jrxml" )
.atTheBegining()
.and()
.addStaticSubreport( "introduction" )
.designFrom( "introduction.jrxml" )
.and()
.addSqlSubreport( "employeeList" )
.designFrom( "employeeList.jrxml" )
.withQuery( "select * name,surname from emplyees" )
.getDataFrom( "hsql" );
Wydaje się jednak, że to rozwiązanie jest mniej czytelne niż poprzednie.
DSL oparty o Fluent Interface ma następujące zalety:
- Jest bardzo czytelny i wygodny w użyciu;
- Łatwość tworzenia i utrzymania testów jednostkowych – ponieważ wszystko napisane jest w Javie (innym języku);
- Wsparcie dla refaktoryzacji kodu i testów jednostkowych;
- Zdecydowanie szybsza implementacja poprawek i nowych funkcjonalności w porównaniu z rozwiązaniem opartym na XML.
Przy powyższych niewątpliwych zaletach należy mieć świadomość następujących konsekwencji tego rozwiązania:
- Tylko programista może konfigurować raport;
- Zaprojektowanie dobrego Fluent API wymaga sporo czasu i wysiłku;
- Stworzenie narzędzia WYSIWYG do konfigurowania raportu wymaga znacznego nakładu prac.
Krótkie podsumowanie
Zarówno XML jaki Fluent Interface mają swoje plusy dodatnie i ujemne. Można próbować określić zakresy przydatności każdego z rozwiązań. W omawianym przykładzie framework raporty był biblioteką programistyczną. Jedynymi jej użytkownikami byli programiści. Gdybym jeszcze raz brał udział w tym projekcie z pewnością przychylałbym się ku Fluent Interface. Głównym moim argument są korzyści wynikające z łatwości testowania, refaktoryzacji i możliwości szybkiej rozbudowy narzędzia. Gdyby jednak framework miał być wykorzystywany przez zewnętrzne systemy np. narzędzia WYSIWYG to XML, mimo narzutów implementacyjnych, jest rozwiązaniem bezkonkurencyjnym.
Przedstawione tu rozwiązania to nie jedyny sposób tworzenia DSL. Gdy tworzyliśmy framework raportowy pojawiały się pomysły napisania języka do opisu raportów przy użyciu Ruby. Wtedy jednak nie nazywaliśmy tego jeszcze DSL. Zainteresowanych tematem odsyłam do bliki Martina Fowlera.